1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
#![doc(html_root_url = "https://docs.rs/lignin/0.1.0")] #![no_std] #![warn(clippy::pedantic)] #![warn(missing_docs)] //! `lignin`, named after the structural polymer found in plants, is a lightweight but comprehensive VDOM data type library for use in a wider web context. //! //! # About the Documentation //! //! DOM API terms are ***bold italic*** and linked to the MDN Web Docs. //! (Please file an issue if this isn't the case somewhere.) //! //! # Implementation Contract //! //! > **This is not a soundness contract**. Code using this crate must not rely on it for soundness. //! > However, it is free to panic when encountering an incorrect implementation. //! //! ## Security //! //! See also the implementation contracts on [`Node::Text::text`], [`Node::Comment::comment`] and [`Attribute::name`]. //! //! When rendering the VDOM as HTML text, extra care **must** be taken to syntactically validate everything according to [the specification](https://html.spec.whatwg.org/multipage/syntax.html)! //! //! HTML renderers should error rather than panic when encountering a VDOM that they can't guarantee will be parsed as intended (assuming any syntax errors potentially cause undefined behavior *somewhere*). //! However, renderers are free to be lenient in this regard by adjusting their output to be syntactically valid in a way that's unlikely to cause a changed user experience. (That is: Feel free to substitute illegal character sequences in comments and such.) //! //! ## Correctness //! //! The DOM may contain extra siblings past the nodes mentioned in the VDOM. Renderers must ignore them. //! //! Similarly, the DOM may contain extra attributes and event bindings. Renderers must ignore them unless attributes collide. //! Components must clean up extra attributes and event listeners they have previous added to the DOM via the DOM API on teardown. //! //! > This simplifies renderers and allows reuse of DOM nodes between components, which in turn reduces the amount of DOM API calls necessary. //! //! See also the implementation contracts on [`DomRef`] and [`Node::Keyed`]. //! //! ## Performance //! //! While the order of [attributes](https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) reported by the DOM API in browsers isn't specified and event listeners can't be examined this way, //! components *should* stick to a relatively consistent order here and place conditional attributes and event bindings past always present ones in the respective slices. //! //! When adding or removing [`Node`]s dynamically between updates, components should wrap lists in [`Node::Multi`] and otherwise insert an empty [`Node::Multi([&[])`](`Node::Multi`) as placeholder for an absent element. //! //! > Each of these suggestions allows better and easier diff optimization in renderers, but otherwise mustn't be a strict requirement for compatibility. //! //! # Deep Comparisons //! //! All [`core`] comparison traits ([`PartialEq`], [`Eq`], [`PartialOrd`] and [`Ord`]) are implemented recursively where applicable. //! //! Note that [`CallbackRef`]s derived from separate instances of [`CallbackRegistration`] are still considered distinct, //! regardless of the `receiver` and `handler` used to make them. //! However, without the `"callbacks"` feature, all [`CallbackRef`] instances are inactive **and indistinct**. //! //! For shallow comparisons, access and compare fields directly or [memoize](`Node::Memoized`) parts of the GUI. //! //! # Features //! //! ## `"callbacks"` //! //! Enables DOM callback support. Requires [`std`](https://doc.rust-lang.org/stable/std/index.html). //! //! Without this feature, most of the callback API is still available but stand-in types in [`web`] are vacant and will materialize into **any** type. //! //! Always test VDOM generators with the `"callbacks"` feature enabled if they make use of them at all, but only depend on it in order to *invoke* callbacks. //! //! # Notes on Performance //! //! ## Clone //! //! [`Clone`] is always implemented via [`Copy`] in this crate, since none of the instances provide heap storage. //! //! ## Comparisons and Hashing //! //! As shallow hashes would easily collide for most applications where VDOM hashing comes up, //! [`Hash`](`core::hash::Hash`) is implemented recursively in this crate and is potentially expensive. //! The same applies to [`PartialEq`], [`Eq`], [`PartialOrd`] and [`Ord`]. //! //! As an exception, [`Node::Memoized`] instances are compared only by their [`state_key`](`Node::Memoized::state_key`). //! Their [`content`](`Node::Memoized::content`) is ignored for comparisons and does not factor into their [hash](`core::hash`). //! //! **`lignin` does not implement hash caching by itself**, so users of [`HashMap`](https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html) or similar containers should wrap node graphs in a "`HashCached<T>`" type first. //! //! # Limitations //! //! As `lignin` targets HTML and DOM rather than XML, it does not support [***processing instructions***](https://developer.mozilla.org/en-US/docs/Web/API/ProcessingInstruction) or [***CDATA sections***](https://developer.mozilla.org/en-US/docs/Web/API/CDATASection). //! //! For the same reason, there is formally no information about VDOM identity, which could be used to render self-referential XML documents. //! //! > In practice, it **may** be possible to determine identity by comparing pointers, but this would require some workarounds regarding `lignin`'s slices-of-values to be general. //! > //! > The implementation itself would be quite error-prone on types that are [`Copy`] due to implicit by-value copies there. Proceed with caution if you must! //! //! Element and attribute names are always plain `&str`s, which isn't ideal for software that renders its GUI more directly than through a web browser. //! I'm open to maintaining a generic fork if there's interest in this regard. //! //! ## with `"callbacks"` feature //! //! While the limit is relatively high at [`u32::MAX`], the total number of [`CallbackRegistration`]s that can be created over the program's lifetime is still limited¹. //! //! For this reason, you should hold onto [`CallbackRegistration`] instances as long a possible and avoid recreating them for each VDOM update. //! //! > However, **you must not make assumptions about when the respective `callback` is invoked in relation to a component being rendered**, as [`CallbackRef`]s can legally be kept over multiple VDOM updates. //! //! See [`CallbackRegistration`] for storage hints. //! //! ¹ APIs exist in [`callback_registry`] to partially or completely reset this limit, but they have additional safety requirements to the application as a whole. //! //! ## without `"callbacks"` feature //! //! While the `"callbacks"` feature is disabled, all callback management is erased. //! This makes `lignin` faster and removes all instantiation limits on [`CallbackRegistration`], but removes unique identities from [`CallbackRegistration`] and [`CallbackRef`], which affects comparisons and hashing. //! //! [MathML](https://developer.mozilla.org/en-US/docs/Web/MathML) support is rudimentary due lack of direct support in web-sys. //! //! # Usage Notes //! //! ## Optional Tags //! //! > This is only a suggestion. Renderers should normally not depend on certain tags to be present or absent. //! //! While HTML allows certain elements, like [***`<body>`***](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body) //! to be partially or entirely implied, [`lignin`](`crate`) is not granular enough to model this accurately. //! //! Implied elements are also still present in the browser DOM, even if both their start and end tag have been omitted. //! //! As such, these elements should normally be explicit in the VDOM. //! HTML renderers may omit tags from the serialised document or fragment [according to the HTML specification](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags). #[cfg(doctest)] pub mod readme { doc_comment::doctest!("../README.md"); } pub mod auto_safety; pub mod callback_registry; mod remnants; pub mod web; use callback_registry::CallbackSignature; pub use callback_registry::{CallbackRef, CallbackRegistration}; pub use web::{DomRef, Materialize}; mod ergonomics; use core::{convert::Infallible, fmt::Debug, hash::Hash, marker::PhantomData}; use remnants::RemnantSite; use sealed::Sealed; /// [`Vdom`] A single generic VDOM node. /// /// This should be relatively small: /// /// ```rust /// # use core::mem::size_of; /// # use lignin::{Node, ThreadSafe}; /// if size_of::<usize>() == 8 { /// assert!(size_of::<Node<ThreadSafe>>() <= 24); /// } /// /// // e.g. current Wasm /// if size_of::<usize>() == 4 { /// assert!(size_of::<Node<ThreadSafe>>() <= 16); /// } /// ``` #[allow(clippy::type_complexity)] // `Option<CallbackRef<S, fn(DomRef<&'_ …>)>>` appears to be a little much. pub enum Node<'a, S: ThreadSafety> { /// Represents a [***Comment***](https://developer.mozilla.org/en-US/docs/Web/API/Comment) node. Comment { /// The comment's body, as unescaped plaintext. /// /// Renderers shouldn't insert padding whitespace around it, except as required by e.g. pretty-printing. /// /// # Implementation Contract /// /// > **This is not a soundness contract**. Code using this crate must not rely on it for soundness. /// > However, it is free to panic when encountering an incorrect implementation. /// /// ## **Security** /// /// This field may contain arbitrary character sequences, some of which are illegal in [***Comment***](https://developer.mozilla.org/en-US/docs/Web/API/Comment)s at least when serialized as HTML. /// See <https://html.spec.whatwg.org/multipage/syntax.html#comments> for more information. /// /// Renderers **must** either refuse or replace illegal-for-target comments with ones that are inert. /// /// Not doing so opens the door for [XSS](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) /// and/or format confusion vulnerabilities. comment: &'a str, /// Registers for [***Comment***](https://developer.mozilla.org/en-US/docs/Web/API/Comment) reference updates. /// /// See [`DomRef`] for more information. dom_binding: Option<CallbackRef<S, fn(dom_ref: DomRef<&'_ web::Comment>)>>, }, /// Represents a single [***HTMLElement***](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). HtmlElement { /// The [`Element`] to render. element: &'a Element<'a, S>, /// Registers for [***HTMLElement***](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) reference updates. /// /// See [`DomRef`] for more information. dom_binding: Option<CallbackRef<S, fn(dom_ref: DomRef<&'_ web::HtmlElement>)>>, }, /// Represents a single [***MathMLElement***](https://developer.mozilla.org/en-US/docs/Web/API/MathMLElement). /// /// Note that [distinct browser support for these is really quite bad](https://developer.mozilla.org/en-US/docs/Web/API/MathMLElement#browser_compatibility) /// and [correct styling isn't much more available](https://developer.mozilla.org/en-US/docs/Web/MathML#browser_compatibility). /// /// However, [MathML *is* part of the HTML standard](https://html.spec.whatwg.org/multipage/embedded-content-other.html#mathml), so browsers should at least parse it correctly, and styling can be polyfilled. MathMlElement { /// The [`Element`] to render. element: &'a Element<'a, S>, /// Registers for [***Element***](https://developer.mozilla.org/en-US/docs/Web/API/Element) reference updates. /// /// See [`DomRef`] for more information. dom_binding: Option<CallbackRef<S, fn(dom_ref: DomRef<&'_ web::Element>)>>, }, /// Represents a single [***SVGElement***](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). /// /// Note that even outermost `<SVG>` elements are [***SVGElement***](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)s! SvgElement { /// The [`Element`] to render. element: &'a Element<'a, S>, /// Registers for [***SVGElement***](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) reference updates. /// /// See [`DomRef`] for more information. dom_binding: Option<CallbackRef<S, fn(dom_ref: DomRef<&'_ web::SvgElement>)>>, }, /// DOM-transparent. This variant uses shallow comparison and hashes based on its `state_key` only. /// /// A (good enough) `content` [hash](`core::hash`) makes for a good `state_key`, but this isn't the only possible scheme and may not be the optimal one for your use case. /// /// # Implementation Contract (reminder) /// /// Even if [`state_key`](`Node::Memoized::state_key`) is unchanged between two VDOM iterations, the full contents must still be present in the second. /// /// > When skipping the memoized [`content`](`Node::Memoized::content`), a renderer may still require this information to, for example, advance its DOM cursor. /// /// Note that when diffing a non-[`Memoized`](`Node::Memoized`) [`Node`] into a [`Node::Memoized`] (and vice-versa), renderers must still behave as if the DOM tree was recreated, which means cycling all [***Node***](https://developer.mozilla.org/en-US/docs/Web/API/Node) reference bindings even if they match. /// /// > However, this often happens with matching or near-matching fragments during hydration of a web app. /// > /// > *If you already have a function to strip subscriptions* (e.g. [***Node***](https://developer.mozilla.org/en-US/docs/Web/API/Node) reference bindings) from a DOM and VDOM tree, /// > or even just one to strip all callbacks (but this is less efficient), it's likely more efficient to do so and then recurse. /// > /// > Make sure the trees are actually somewhat compatible first, or you may end up processing the old VDOM twice for nothing. Memoized { /// A value that's (very likely to be) distinct between VDOM graphs where the path of two [`Node::Memoized`] instances matches but their [`Node::Memoized::content`] is distinct. /// /// Consider using a (good enough) hash of [`content`](`Node::Memoized::content`) for this purpose. state_key: u64, /// The VDOM tree memoized by this [`Node`]. content: &'a Node<'a, S>, }, /// DOM-transparent. Represents a sequence of VDOM nodes. /// /// Used to hint diffs in case of additions and removals. Multi(&'a [Node<'a, S>]), /// A sequence of VDOM nodes that's transparent at rest, but encodes information on how to reuse and reorder elements when diffing. /// /// **List indices are bad [`ReorderableFragment::dom_key`] values** unless reordered along with the items! /// Use the [`Multi`](`Node::Multi`) variant instead if you don't track component identity. /// /// # Implementation Contract /// /// > **This is not a soundness contract**. Code using this crate must not rely on it for soundness. /// > However, it is free to panic when encountering an incorrect implementation. /// /// The [`ReorderableFragment::dom_key`] values **must be unique** within a slice referenced by a [`Node::Keyed`] instance. /// /// /// If a [`dom_key`](`ReorderableFragment::dom_key`) value appears both in the initial and target slice of a [`ReorderableFragment::dom_key`] diff, /// those [`ReorderableFragment`] instances are considered path-matching and any respective [***Node***](https://developer.mozilla.org/en-US/docs/Web/API/Node)(s!) **must** /// be moved to their new location without being recreated. /// /// > These rules do not apply between distinct [`ReorderableFragment`] slices, even if they overlap in memory or one is reachable from the other. /// /// > The recursive diff otherwise proceeds as normal. /// > There are no rules on whether it happens before, during or after the reordering. /// /// # Usage Notes /// /// [`ReorderableFragment::dom_key`] is of type [`u32`] and intentionally doesn't fit [`Hasher`](`core::hash::Hasher`) output. /// It may be tempting to use a hash as easily-made DOM key, but this would violate the uniqueness constraint in the implementation contract above. /// /// To derive unique `dom_key`s from your application's native IDs, you may want to use a symbol interner like [intaglio](https://docs.rs/intaglio/1). /// When doing so, consider using a custom ([`Build`](`core::hash::BuildHasher`))[`Hasher`](`core::hash::Hasher`) to reduce the code size of your compiled program. /// A "bad" hash won't hurt your app if individual symbol tables are small. /// /// ## Example /// /// ``` /// use bumpalo::Bump; /// // CRC32 is about as simple a hash as is compatible with `core::hash::Hash`. /// use crc32fast::Hasher; /// use intaglio::SymbolTable; /// use lignin::{Node, ReorderableFragment, ThreadSafe}; /// use std::{borrow::Cow, hash::BuildHasherDefault}; /// // `.unwrap_throw()` produces smaller executables when targeting Wasm. /// // However, the error message may be less specific. /// use wasm_bindgen::UnwrapThrowExt as _; /// /// // Persist this in your component. /// let mut key_map: SymbolTable<BuildHasherDefault<Hasher>> = SymbolTable::default(); /// /// // Fake data: /// let items = ["Prepare for trouble!", "And make it double!"]; /// /// // Fake render parameter: /// let bump = Bump::new(); /// /// // Render: /// let _ = Node::Keyed::<ThreadSafe>(bump.alloc_slice_fill_iter(items.iter().map(|item| { /// ReorderableFragment { /// dom_key: key_map.intern(Cow::Borrowed(*item)).unwrap_throw().id(), /// content: Node::Text { /// text: item, /// dom_binding: None, /// }, /// } /// }))); /// ``` /// /// # Motivation /// /// > You may have noticed that the implementation contract for [`Node::Keyed`] is unusually strict compared to other frameworks. /// > /// > There are two reasons for this: /// > /// > ### Ease of Implementation /// > /// > Without having to account for duplicate keys, it's easier to implement an efficient DOM differ. /// > /// > ### Accessibility and Glitch Avoidance /// > /// > More importantly, duplicate DOM keys can introduce subtle UX glitches into your app, which can shift [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus) /// > in unexpected ways inside a list. (This can most easily occur when dismissing a duplicate item with contained button.) /// > /// > These glitches are likely imperceptible to the majority of your users, unless they navigate by keyboard, or use a screen reader, or your focus style is quite visible, or items animate in or out or to their new position, … the list of edge cases is really quite long. /// > The point is that **these issues may be subtle during early development but can surface in force later, when it's difficult to fix them thoroughly**. **`lignin` avoids this** by denying the necessary circumstance (duplication of keys) for them to occur in the first place. Keyed(&'a [ReorderableFragment<'a, S>]), /// Represents a [***Text***](https://developer.mozilla.org/en-US/docs/Web/API/Text) node. Text { /// The [`Text`](`Node::Text`)'s [***Node.textContent***](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent). /// /// # Implementation Contract /// /// > **This is not a soundness contract**. Code using this crate must not rely on it for soundness. /// > However, it is free to panic when encountering an incorrect implementation. /// /// ## **Security** /// /// This field contains unescaped *plaintext*. Renderers **must** escape **all** control characters and sequences. /// /// Not doing so opens the door for [XSS](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) vulnerabilities. /// /// In order to support e.g. formatting instructions, apps should (carefully) parse user-generated content and translate it into a matching VDOM graph. /// /// Live components also have the option of using for example [`Node::HtmlElement::dom_binding`] to set [***Element.innerHTML***](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), /// but this is not recommended due to the difficulty of implementing allow-listing with such an approach. text: &'a str, /// Registers for [***Text***](https://developer.mozilla.org/en-US/docs/Web/API/Text) reference updates. /// /// See [`DomRef`] for more information. dom_binding: Option<CallbackRef<S, fn(dom_ref: DomRef<&'_ web::Text>)>>, }, /// Currently unused. /// /// The plan here is to allow fragments to linger in the DOM after being diffed out, which seems like the most economical way to enable e.g. fade-out animations. //[not `doc`] There should be a callback for this occasion, and they should be placed in such a way in the DOM that, by default, they are rendered *in front* of a replacement in the same location. RemnantSite(&'a RemnantSite), } /// [`Vdom`] A VDOM node that has its DOM identity preserved during DOM updates even after being repositioned within a (path-)matching [`Node::Keyed`]. /// /// For more information, see [`Node::Keyed`]. pub struct ReorderableFragment<'a, S: ThreadSafety> { /// A key uniquely identifying a [`ReorderableFragment`] within any directly spanning [`Node::Keyed`]. /// /// Note [`Node::Keyed`]'s usage notes! pub dom_key: u32, /// The [`Node`] to render from this [`ReorderableFragment`]. pub content: Node<'a, S>, } #[allow(clippy::doc_markdown)] /// [`Vdom`] Represents a single [***HTMLElement***](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) as `name`, `attributes`, `content` and `event_bindings`. pub struct Element<'a, S: ThreadSafety> { /// The [***Element.tag_name***](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName). /// /// Unlike in the browser, this is generally treated case-*sensitively*, meaning for example `"div"` doesn't equal `"DIV"`. /// /// Since browsers will generally return the canonical uppercase name, it's recommended to generate the VDOM all-uppercase too, to avoid unnecessary mismatches. pub name: &'a str, /// Controls the ***options*** parameter of [***Document.createElement()***](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) /// *or* (currently only) the global [***is***](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is) attribute. pub creation_options: ElementCreationOptions<'a>, /// The [***Element.attributes***](https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes). /// /// Note that while this collection is unordered in the browser, reordering attributes will generally affect diffing performance. pub attributes: &'a [Attribute<'a>], /// Maps to [***Node.childNodes***](https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes). pub content: Node<'a, S>, /// DOM event bindings requested by a component. /// /// See [`EventBinding`] for more information. pub event_bindings: &'a [EventBinding<'a, S>], } /// [`Vdom`] Maps to ***options*** parameter values of [***Document.createElement()***](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) /// (including ***undefined***) *or* (currently only) the global [***is***](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is) attribute. /// /// # Options /// /// ## `is` /// /// The ***tag name*** of a previously [defined](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) /// [***customized built-in element***](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#customized_built-in_elements) /// to instantiate over a built-in HTML element. /// /// When rendering HTML, this controls the global [***is***](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is) attribute. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ElementCreationOptions<'a> { is: Option<&'a str>, } impl<'a> Default for ElementCreationOptions<'a> { fn default() -> Self { Self::new() } } #[allow(clippy::inline_always)] // Trivial getters and setters. impl<'a> ElementCreationOptions<'a> { /// Creates a new [`ElementCreationOptions`] with all fields set to [`None`]. #[inline(always)] #[must_use] pub const fn new() -> Self { Self { is: None } } /// Indicates whether this [`ElementCreationOptions`] instance can be omitted entirely in a [***Document.createElement()***](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) /// call. #[inline(always)] #[must_use] pub const fn matches_undefined(&self) -> bool { matches!(self, Self { is: None }) } /// Retrieves the ***tag name*** of a previously [defined](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) /// [***customized built-in element***](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#customized_built-in_elements) /// to use. #[inline(always)] #[must_use] pub const fn is(&self) -> Option<&'a str> { self.is } /// Sets the ***tag name*** of a previously [defined](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) /// [***customized built-in element***](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#customized_built-in_elements) /// to use. #[inline(always)] pub fn set_is(&mut self, is: Option<&'a str>) { self.is = is } /// Sets the ***tag name*** of a previously [defined](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) /// [***customized built-in element***](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#customized_built-in_elements) /// to use. #[inline(always)] #[must_use] pub const fn with_is(self, is: Option<&'a str>) -> Self { #[allow(clippy::needless_update)] Self { is, ..self } } } /// [`Vdom`] Represents a single DOM event binding with `name` and `callback`. /// #[allow(clippy::doc_markdown)] /// Renderers usually should either manage these through [***EventTarget.addEventListener***](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)/[***….removeEventListener***](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) /// or ignore them entirely. See [`web`] for a bit more information on that. /// /// Note that the running total of [`CallbackRegistration`]s made can be limited to [`u32::MAX`] or around four billion. /// (See [`callback_registry`] for information on how to get around this, if necessary.) /// /// While this limit is likely hard to hit, economizing registrations a little will still (indirectly) improve app performance. /// Lazily registering callbacks for events only when rendering is also the easiest way for framework developers to use [pinning](core::pin) to avoid heap allocations. pub struct EventBinding<'a, S: ThreadSafety> { /// The event name. pub name: &'a str, /// A callback reference created via [`CallbackRegistration`]. pub callback: CallbackRef<S, fn(event: web::Event)>, /// Controls the ***options*** parameter of [***EventTarget.addEventListener()***](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). /// /// Note that [`EventBindingOptions`] is created with the [`EventBindingOptions.passive()`] flag already enabled! pub options: EventBindingOptions, } /// [`Vdom`] Maps to ***options*** parameter values of [***EventTarget.addEventListener()***](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). /// /// Note that all constructors initialize instances with [`.passive()`](`EventBindingOptions::passive()`) set to true. /// /// Also note that these flags aren't part of any soundness contract! Don't rely on them for memory safety. /// /// # Flags /// /// ## `capture` /// /// Controls whether a [`web::Event`] should be dispatched while bubbling down rather than up along the DOM. /// /// ## `once` /// /// Controls whether an associated [`CallbackRef`] should be invoked at most once for this [`EventBinding`]. /// /// This carries over for as long as the [`EventBinding`]'s VDOM identity doesn't change. /// /// ## `passive` (default) /// /// Controls whether a callback is disallowed from calling [`web_sys::Event::prevent_default()`](https://docs.rs/web-sys/0.3.48/web_sys/struct.Event.html#method.prevent_default). /// /// Calling that method while this flag is enabled shouldn't produce any effects other than printing a warning to a browser's JavaScript console. /// /// [This flag can significantly improve performance when applied to certain events.](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners) /// /// > ***passive: true*** isn't always the default in web browsers for backwards compatibility reasons. /// > /// > As `lignin` is a new framework, it's able to break with that tradition for more consistency and a better default. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct EventBindingOptions(u8); mod event_bindings_impl { #![allow(clippy::inline_always)] // Trivial bit manipulation. #![allow(clippy::trivially_copy_pass_by_ref)] // Erased by inlining. #[allow(unused_imports)] // Largely for documentation. use crate::{web, CallbackRef, EventBinding, EventBindingOptions}; pub const CAPTURE: u8 = 0b_0001; pub const ONCE: u8 = 0b_0010; pub const PASSIVE: u8 = 0b_0100; impl Default for EventBindingOptions { /// Creates a new [`EventBindingOptions`] instance with [`.passive()`] already set to `true`. [See more.](`Default::default`) #[inline(always)] fn default() -> Self { Self::new() } } #[allow(clippy::match_bool)] impl EventBindingOptions { /// Creates a new [`EventBindingOptions`] instance with [`.passive()`] already set to `true`. #[inline(always)] #[must_use] pub const fn new() -> Self { Self(PASSIVE) } /// Indicates whether a [`web::Event`] should be dispatched while bubbling down rather than up along the DOM. #[inline(always)] #[must_use] pub const fn capture(&self) -> bool { self.0 & CAPTURE == CAPTURE } /// Sets whether a [`web::Event`] should be dispatched while bubbling down rather than up along the DOM. #[inline(always)] pub fn set_capture(&mut self, capture: bool) { *self = self.with_capture(capture) } /// Sets whether a [`web::Event`] should be dispatched while bubbling down rather than up along the DOM. #[inline(always)] #[must_use] pub const fn with_capture(self, capture: bool) -> Self { Self(match capture { true => self.0 | CAPTURE, false => self.0 & !CAPTURE, }) } /// Indicates whether an associated [`CallbackRef`] should be invoked at most once for this [`EventBinding`]. [See more.](#once) #[inline(always)] #[must_use] pub const fn once(&self) -> bool { self.0 & ONCE == ONCE } /// Sets whether an associated [`CallbackRef`] should be invoked at most once for this [`EventBinding`]. [See more.](#once) #[inline(always)] pub fn set_once(&mut self, once: bool) { *self = self.with_once(once) } /// Sets whether an associated [`CallbackRef`] should be invoked at most once for this [`EventBinding`]. [See more.](#once) #[inline(always)] #[must_use] pub const fn with_once(self, once: bool) -> Self { Self(match once { true => self.0 | ONCE, false => self.0 & !ONCE, }) } /// `(default)` Indicates whether a callback is disallowed from calling [`web_sys::Event::prevent_default()`](https://docs.rs/web-sys/0.3.48/web_sys/struct.Event.html#method.prevent_default). /// [See more.](#passive) #[inline(always)] #[must_use] pub const fn passive(&self) -> bool { self.0 & PASSIVE == PASSIVE } /// `(default)` Sets whether a callback is disallowed from calling [`web_sys::Event::prevent_default()`](https://docs.rs/web-sys/0.3.48/web_sys/struct.Event.html#method.prevent_default). /// [See more.](#passive) #[inline(always)] pub fn set_passive(&mut self, passive: bool) { *self = self.with_passive(passive) } /// `(default)` Sets whether a callback is disallowed from calling [`web_sys::Event::prevent_default()`](https://docs.rs/web-sys/0.3.48/web_sys/struct.Event.html#method.prevent_default). /// [See more.](#passive) #[inline(always)] #[must_use] pub const fn with_passive(self, passive: bool) -> Self { Self(match passive { true => self.0 | PASSIVE, false => self.0 & !PASSIVE, }) } } } /// [`Vdom`] Represents a single HTML [***Attr***](https://developer.mozilla.org/en-US/docs/Web/API/Attr) with `name` and `value`. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] pub struct Attribute<'a> { /// The [***name***](https://developer.mozilla.org/en-US/docs/Web/API/Attr#properties). /// /// # Implementation Contract /// /// ## Security /// /// While applications should generally avoid it, [`Attribute::name`] may contain [characters that are unexpected in this position](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2). /// /// Renderers may only process these verbatim iff they can expect this to not cause security issues. /// /// > For example: Passing an invalid attribute name to a DOM API *isolated in a dedicated parameter* is *probably* okay, /// > as long as something along the way validates it doesn't contain `'\0'`. /// > /// > Serializing an invalid attribute name to HTML is a **very** bad idea, so renderers must never do so. pub name: &'a str, /// The unescaped [***value***](https://developer.mozilla.org/en-US/docs/Web/API/Attr#properties). pub value: &'a str, } mod sealed { use super::{ThreadBound, ThreadSafe}; use crate::{ callback_registry::CallbackSignature, remnants::RemnantSite, web, Attribute, CallbackRef, CallbackRegistration, DomRef, Element, ElementCreationOptions, EventBinding, EventBindingOptions, Node, ReorderableFragment, ThreadSafety, }; pub trait Sealed {} impl Sealed for fn(web::Event) {} impl<T> Sealed for fn(DomRef<&'_ T>) {} impl Sealed for ThreadBound {} impl Sealed for ThreadSafe {} impl<'a> Sealed for Attribute<'a> {} impl<'a> Sealed for ElementCreationOptions<'a> {} impl Sealed for EventBindingOptions {} impl<R, C: CallbackSignature> Sealed for CallbackRegistration<R, C> {} impl<S: ThreadSafety, C: CallbackSignature> Sealed for CallbackRef<S, C> {} impl<'a, S: ThreadSafety> Sealed for Element<'a, S> {} impl<'a, S: ThreadSafety> Sealed for EventBinding<'a, S> {} impl<'a, S: ThreadSafety> Sealed for Node<'a, S> {} impl<'a, S: ThreadSafety> Sealed for ReorderableFragment<'a, S> {} impl Sealed for RemnantSite {} } /// Marker trait for thread-safety tokens. pub trait ThreadSafety: Sealed + Into<ThreadBound> where Self: Sized + Debug + Clone + Copy + PartialEq + Eq + PartialOrd + Ord + Hash, { } /// [`ThreadSafety`] marker for `!Send + !Sync`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ThreadBound( /// Neither [`Send`] nor [`Sync`]. pub PhantomData<*const ()>, /// [Uninhabited.](https://doc.rust-lang.org/nomicon/exotic-sizes.html#empty-types) pub Infallible, ); /// [`ThreadSafety`] marker for `Send + Sync`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ThreadSafe( /// This field here technically doesn't matter, but I went with something to match [`ThreadBound`]. pub PhantomData<&'static ()>, /// [Uninhabited.](https://doc.rust-lang.org/nomicon/exotic-sizes.html#empty-types) pub Infallible, ); impl ThreadSafety for ThreadBound {} impl ThreadSafety for ThreadSafe {} /// Marker trait for VDOM data types, which (almost) all vary by [`ThreadSafety`]. /// /// Somewhat uselessly implemented on [`Attribute`], [`ElementCreationOptions`] and [`EventBindingOptions`], which are always [`ThreadSafe`]. pub trait Vdom: Sealed where Self: Sized + Debug + Clone + Copy + PartialEq + Eq + PartialOrd + Ord + Hash, { /// The [`ThreadSafety`] of the [`Vdom`] type, either [`ThreadSafe`] or [`ThreadBound`]. /// /// This comes from a generic type argument `S`, but [`Attribute`] and [`EventBindingOptions`] are always [`ThreadSafe`]. type ThreadSafety: ThreadSafety; } impl<'a> Vdom for Attribute<'a> { type ThreadSafety = ThreadSafe; } impl<'a> Vdom for ElementCreationOptions<'a> { type ThreadSafety = ThreadSafe; } impl Vdom for EventBindingOptions { type ThreadSafety = ThreadSafe; } macro_rules! vdom_impls { ($($name:ident),*$(,)?) => {$( impl<'a, S> Vdom for $name<'a, S> where S: ThreadSafety, { type ThreadSafety = S; } )*}; } vdom_impls!(Element, EventBinding, Node, ReorderableFragment); impl<S, C> Vdom for CallbackRef<S, C> where S: ThreadSafety, C: CallbackSignature, { type ThreadSafety = S; }