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;
}