initial work on meta

This commit is contained in:
Greg Johnston 2024-03-11 21:19:50 -04:00
parent 30c1cd921b
commit 39607adc94
15 changed files with 599 additions and 287 deletions

View file

@ -7,6 +7,7 @@ members = [
"const_str_slice_concat",
"either_of",
"next_tuple",
"oco",
"or_poisoned",
"routing_utils",
@ -58,6 +59,7 @@ leptos_router = { path = "./router", version = "0.6.5" }
leptos_server = { path = "./leptos_server", version = "0.6.5" }
leptos_meta = { path = "./meta", version = "0.6.5" }
next_tuple = { path = "./next_tuple" }
oco = { path = "./oco" }
or_poisoned = { path = "./or_poisoned" }
reactive_graph = { path = "./reactive_graph" }
routing = { path = "./routing" }

View file

@ -14,7 +14,7 @@ actix-web = "4"
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_macro = { workspace = true, features = ["actix"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
server_fn = { workspace = true, features = ["actix"] }

View file

@ -17,7 +17,7 @@ http-body-util = "0.1"
leptos = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
parking_lot = "0.12"

View file

@ -12,7 +12,7 @@ rust-version.workspace = true
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_hot_reload = { workspace = true }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1.37"

View file

@ -7,7 +7,6 @@ license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
readme = "../README.md"
rust-version.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
@ -17,7 +16,8 @@ leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
leptos-spin-macro = { version = "0.1", optional = true }
leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true }
oco = { workspace = true }
paste = "1"
reactive_graph = { workspace = true, features = ["serde"] }
tachys = { workspace = true, features = ["reactive_graph"] }
@ -37,31 +37,29 @@ web-sys = { version = "0.3.63", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen = { version = "0.2" }
[features]
default = ["serde"]
csr = [
"leptos_macro/csr",
"leptos_reactive/csr",
"dep:wasm-bindgen",
]
csr = ["leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
hydrate = [
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"dep:wasm-bindgen",
"leptos_server/hydrate",
]
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
rustls = ["leptos_server/rustls", "server_fn/rustls"]
ssr = [
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
"server_fn/ssr",
]
nightly = [
"leptos_dom/nightly",
"leptos_macro/nightly",
"leptos_reactive/nightly",
"leptos_server/nightly",
"tachys/nightly",
]
serde = ["leptos_reactive/serde"]
@ -96,8 +94,6 @@ denylist = [
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"experimental-islands",
]
skip_feature_sets = [
[

View file

@ -157,6 +157,7 @@ pub mod component;
mod for_loop;
mod hydration_scripts;
mod show;
pub mod text_prop;
pub use for_loop::*;
pub use hydration_scripts::*;
pub use leptos_macro::*;
@ -174,10 +175,12 @@ mod into_view;
pub use into_view::IntoView;
pub use leptos_dom;
pub use tachys;
pub mod logging;
mod mount;
pub use any_spawner::Executor;
pub use mount::*;
pub use oco;
/*mod additional_attributes;
pub use additional_attributes::*;
mod await_;
@ -206,10 +209,6 @@ pub use leptos_dom::{
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
/// Types to make it easier to handle errors in your application.
pub mod error {

102
leptos/src/logging.rs Normal file
View file

@ -0,0 +1,102 @@
//! Utilities for simple isomorphic logging to the console or terminal.
use wasm_bindgen::JsValue;
/// Uses `println!()`-style formatting to log something to the console (in the browser)
/// or via `println!()` (if not in the browser).
#[macro_export]
macro_rules! log {
($($t:tt)*) => ($crate::logging::console_log(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
/// or via `eprintln!()` (if not in the browser).
#[macro_export]
macro_rules! warn {
($($t:tt)*) => ($crate::logging::console_warn(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log errors to the console (in the browser)
/// or via `eprintln!()` (if not in the browser).
#[macro_export]
macro_rules! error {
($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string()))
}
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
#[macro_export]
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
{
$crate::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{
($($x)*)
}
}
}
}
const fn log_to_stdout() -> bool {
cfg!(not(all(
target_arch = "wasm32",
not(any(target_os = "emscripten", target_os = "wasi"))
)))
}
/// Log a string to the console (in the browser)
/// or via `println!()` (if not in the browser).
pub fn console_log(s: &str) {
#[allow(clippy::print_stdout)]
if log_to_stdout() {
println!("{s}");
} else {
web_sys::console::log_1(&JsValue::from_str(s));
}
}
/// Log a warning to the console (in the browser)
/// or via `println!()` (if not in the browser).
pub fn console_warn(s: &str) {
if log_to_stdout() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
/// Log an error to the console (in the browser)
/// or via `println!()` (if not in the browser).
#[inline(always)]
pub fn console_error(s: &str) {
if log_to_stdout() {
eprintln!("{s}");
} else {
web_sys::console::error_1(&JsValue::from_str(s));
}
}
/// Log an error to the console (in the browser)
/// or via `println!()` (if not in the browser), but only in a debug build.
#[inline(always)]
pub fn console_debug_warn(s: &str) {
#[cfg(debug_assertions)]
{
if log_to_stdout() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
#[cfg(not(debug_assertions))]
{
let _ = s;
}
}

75
leptos/src/text_prop.rs Normal file
View file

@ -0,0 +1,75 @@
use oco::Oco;
use std::sync::Arc;
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
#[derive(Clone)]
pub struct TextProp(Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>);
impl TextProp {
/// Accesses the current value of the property.
#[inline(always)]
pub fn get(&self) -> Oco<'static, str> {
(self.0)()
}
}
impl core::fmt::Debug for TextProp {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("TextProp").finish()
}
}
impl From<String> for TextProp {
fn from(s: String) -> Self {
let s: Oco<'_, str> = Oco::Counted(Arc::from(s));
TextProp(Arc::new(move || s.clone()))
}
}
impl From<&'static str> for TextProp {
fn from(s: &'static str) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Arc::new(move || s.clone()))
}
}
impl From<Arc<str>> for TextProp {
fn from(s: Arc<str>) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Arc::new(move || s.clone()))
}
}
impl From<Oco<'static, str>> for TextProp {
fn from(s: Oco<'static, str>) -> Self {
TextProp(Arc::new(move || s.clone()))
}
}
// TODO
/*impl<T> From<T> for MaybeProp<TextProp>
where
T: Into<Oco<'static, str>>,
{
fn from(s: T) -> Self {
Self(Some(MaybeSignal::from(Some(s.into().into()))))
}
}*/
impl<F, S> From<F> for TextProp
where
F: Fn() -> S + 'static + Send + Sync,
S: Into<Oco<'static, str>>,
{
#[inline(always)]
fn from(s: F) -> Self {
TextProp(Arc::new(move || s().into()))
}
}
impl Default for TextProp {
fn default() -> Self {
Self(Arc::new(|| Oco::Borrowed("")))
}
}

View file

@ -9,8 +9,8 @@ description = "Tools to set HTML metadata in the Leptos web framework."
rust-version.workspace = true
[dependencies]
cfg-if = "1"
leptos = { workspace = true }
or_poisoned = { workspace = true }
tracing = "0.1"
wasm-bindgen = "0.2"
indexmap = "2"
@ -21,14 +21,6 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features]
default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]
denylist = ["nightly"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View file

@ -1,72 +1,76 @@
use cfg_if::cfg_if;
use leptos::*;
#[cfg(feature = "ssr")]
use std::collections::HashMap;
#[cfg(feature = "ssr")]
use std::{cell::RefCell, rc::Rc};
use crate::{use_head, ServerMetaContext};
use indexmap::IndexMap;
use leptos::{
component,
oco::Oco,
reactive_graph::{effect::RenderEffect, owner::use_context},
tachys::{
dom::document,
error::Result,
html::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
Attribute,
},
hydration::Cursor,
reactive_graph::RenderEffectState,
renderer::{dom::Dom, Renderer},
view::{Mountable, Position, PositionState, Render, RenderHtml},
},
text_prop::TextProp,
IntoView,
};
use or_poisoned::OrPoisoned;
use std::{
cell::RefCell,
collections::HashMap,
mem,
rc::Rc,
sync::{Arc, RwLock},
};
use web_sys::{HtmlBodyElement, HtmlElement};
/// Contains the current metadata for the document's `<body>`.
#[derive(Clone, Default)]
pub struct BodyContext {
#[cfg(feature = "ssr")]
class: Rc<RefCell<Option<TextProp>>>,
#[cfg(feature = "ssr")]
id: Rc<RefCell<Option<TextProp>>>,
#[cfg(feature = "ssr")]
attributes: Rc<RefCell<HashMap<&'static str, Attribute>>>,
class: Arc<RwLock<Option<TextProp>>>,
attributes: Arc<RwLock<Vec<AnyAttribute<Dom>>>>,
}
impl BodyContext {
/// Converts the `<body>` metadata into an HTML string.
#[cfg(any(feature = "ssr", doc))]
pub fn as_string(&self) -> Option<String> {
let class = self.class.borrow().as_ref().map(|val| {
format!(
"class=\"{}\"",
leptos::leptos_dom::ssr::escape_attr(&val.get())
)
});
///
/// This consumes the list of `attributes`, and should only be called once per request.
pub fn to_string(&self) -> Option<String> {
let mut buf = String::from(" ");
if let Some(class) = &*self.class.read().or_poisoned() {
buf.push_str("class=\"");
buf.push_str(&class.get());
buf.push_str("\" ");
};
let id = self.id.borrow().as_ref().map(|val| {
format!(
"id=\"{}\"",
leptos::leptos_dom::ssr::escape_attr(&val.get())
)
});
let attributes = self.attributes.borrow();
let attributes = (!attributes.is_empty()).then(|| {
attributes
.iter()
.filter_map(|(n, v)| {
v.as_nameless_value_string().map(|v| {
format!(
"{}=\"{}\"",
n,
leptos::leptos_dom::ssr::escape_attr(&v)
)
})
})
.collect::<Vec<_>>()
.join(" ")
});
let attributes = mem::take(&mut *self.attributes.write().or_poisoned());
let mut val = [id, class, attributes]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
if val.is_empty() {
for attr in attributes {
attr.to_html(
&mut buf,
&mut String::new(),
&mut String::new(),
&mut String::new(),
);
buf.push(' ');
}
if buf.trim().is_empty() {
None
} else {
val.insert(0, ' ');
Some(val)
Some(buf)
}
}
}
impl core::fmt::Debug for BodyContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("TitleContext").finish()
f.debug_struct("BodyContext").finish_non_exhaustive()
}
}
@ -96,7 +100,7 @@ impl core::fmt::Debug for BodyContext {
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Body(
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
@ -106,49 +110,137 @@ pub fn Body(
id: Option<TextProp>,
/// Arbitrary attributes to add to the `<body>`
#[prop(attrs)]
attributes: Vec<(&'static str, Attribute)>,
mut attributes: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
cfg_if! {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
use wasm_bindgen::JsCast;
if let Some(meta) = use_context::<ServerMetaContext>() {
*meta.body.class.write().or_poisoned() = class.clone();
let el = document().body().expect("there to be a <body> element");
// these can safely be taken out if the server context is present
// server rendering is handled separately, not via RenderHtml
*meta.body.attributes.write().or_poisoned() = mem::take(&mut attributes)
}
if let Some(class) = class {
create_render_effect({
let el = el.clone();
move |_| {
let value = class.get();
_ = el.set_attribute("class", &value);
BodyView { class, attributes }
}
struct BodyView {
class: Option<TextProp>,
attributes: Vec<AnyAttribute<Dom>>,
}
struct BodyViewState {
el: HtmlElement,
class: Option<RenderEffect<Oco<'static, str>>>,
attributes: Vec<AnyAttributeState<Dom>>,
}
impl Render<Dom> for BodyView {
type State = BodyViewState;
type FallibleState = BodyViewState;
fn build(self) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let class = self.class.map(|class| {
RenderEffect::new({
let el = el.clone();
move |prev| {
let next = class.get();
if prev.as_ref() != Some(&next) {
if let Err(e) = el.set_attribute("class", &next) {
web_sys::console::error_1(&e);
}
}
});
}
next
}
})
});
let attributes = self
.attributes
.into_iter()
.map(|attr| attr.build(&el))
.collect();
if let Some(id) = id {
create_render_effect({
let el = el.clone();
move |_| {
let value = id.get();
_ = el.set_attribute("id", &value);
BodyViewState {
el,
class,
attributes,
}
}
fn rebuild(self, state: &mut Self::State) {
// TODO rebuilding dynamic things like this
}
fn try_build(self) -> Result<Self::FallibleState> {
Ok(self.build())
}
fn try_rebuild(self, state: &mut Self::FallibleState) -> Result<()> {
self.rebuild(state);
Ok(())
}
}
impl RenderHtml<Dom> for BodyView {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let class = self.class.map(|class| {
RenderEffect::new({
let el = el.clone();
move |prev| {
let next = class.get();
if prev.is_none() {
return next;
}
});
}
for (name, value) in attributes {
leptos::leptos_dom::attribute_helper(el.unchecked_ref(), name.into(), value);
}
} else if #[cfg(feature = "ssr")] {
let meta = crate::use_head();
*meta.body.class.borrow_mut() = class;
*meta.body.id.borrow_mut() = id;
meta.body.attributes.borrow_mut().extend(attributes);
} else {
_ = class;
_ = id;
_ = attributes;
#[cfg(debug_assertions)]
crate::feature_warning();
if prev.as_ref() != Some(&next) {
if let Err(e) = el.set_attribute("class", &next) {
web_sys::console::error_1(&e);
}
}
next
}
})
});
let attributes = self
.attributes
.into_iter()
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
.collect();
BodyViewState {
el,
class,
attributes,
}
}
}
impl Mountable<Dom> for BodyViewState {
fn unmount(&mut self) {}
fn mount(
&mut self,
parent: &<Dom as Renderer>::Element,
marker: Option<&<Dom as Renderer>::Node>,
) {
}
fn insert_before_this(
&self,
parent: &<Dom as Renderer>::Element,
child: &mut dyn Mountable<Dom>,
) -> bool {
true
}
}

View file

@ -47,11 +47,10 @@
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
use cfg_if::cfg_if;
use indexmap::IndexMap;
use leptos::{
leptos_dom::{debug_warn, html::AnyElement},
*,
debug_warn,
reactive_graph::owner::{provide_context, use_context},
};
use std::{
cell::{Cell, RefCell},
@ -62,21 +61,21 @@ use std::{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
mod body;
mod html;
/*mod html;
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;
mod title;
mod title;*/
pub use body::*;
pub use html::*;
/*pub use html::*;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
pub use style::*;
pub use stylesheet::*;
pub use title::*;
pub use title::*;*/
/// Contains the current state of meta tags. To access it, you can use [`use_head`].
///
@ -84,108 +83,37 @@ pub use title::*;
/// [`provide_meta_context`].
#[derive(Clone, Default, Debug)]
pub struct MetaContext {
/// Metadata associated with the `<html>` element
/*/// Metadata associated with the `<html>` element
pub html: HtmlContext,
/// Metadata associated with the `<title>` element.
pub title: TitleContext,
/// Metadata associated with the `<body>` element
pub body: BodyContext,
pub title: TitleContext,*/
/*
/// Other metadata tags.
pub tags: MetaTagsContext,
*/
}
/// Manages all of the element created by components.
#[derive(Clone, Default)]
pub struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<
RefCell<
IndexMap<
Oco<'static, str>,
(HtmlElement<AnyElement>, Option<web_sys::Element>),
>,
>,
>,
/// Contains the state of meta tags for server rendering.
///
/// This should be provided as context during server rendering.
#[derive(Clone, Default, Debug)]
pub struct ServerMetaContext {
/*/// Metadata associated with the `<html>` element
pub html: HtmlContext,
/// Metadata associated with the `<title>` element.
pub title: TitleContext,*/
/// Metadata associated with the `<body>` element
pub(crate) body: BodyContext,
/*
/// Other metadata tags.
pub tags: MetaTagsContext,
*/
}
impl core::fmt::Debug for MetaTagsContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MetaTagsContext").finish()
}
}
impl MetaTagsContext {
/// Converts metadata tags into an HTML string.
#[cfg(any(feature = "ssr", doc))]
pub fn as_string(&self) -> String {
self.els
.borrow()
.iter()
.map(|(_, (builder_el, _))| {
builder_el.clone().into_view().render_to_string()
})
.collect()
}
#[doc(hidden)]
pub fn register(
&self,
id: Oco<'static, str>,
builder_el: HtmlElement<AnyElement>,
) {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
let element_to_hydrate = document()
.get_element_by_id(&id);
let el = element_to_hydrate.unwrap_or_else({
let builder_el = builder_el.clone();
move || {
let head = document().head().unwrap_throw();
head
.append_child(&builder_el)
.unwrap_throw();
(*builder_el).clone().unchecked_into()
}
});
on_cleanup({
let el = el.clone();
let els = self.els.clone();
let id = id.clone();
move || {
let head = document().head().unwrap_throw();
_ = head.remove_child(&el);
els.borrow_mut().swap_remove(&id);
}
});
self
.els
.borrow_mut()
.insert(id, (builder_el.into_any(), Some(el)));
} else {
self.els.borrow_mut().insert(id, (builder_el, None));
}
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
struct MetaTagId(usize);
impl MetaTagsContext {
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
impl ServerMetaContext {
/// Creates an empty [`ServerMetaContext`].
pub fn new() -> Self {
Default::default()
}
}
@ -207,9 +135,6 @@ pub fn provide_meta_context() {
/// call `use_head()` but a single [`MetaContext`] has not been provided at the application root.
/// The best practice is always to call [`provide_meta_context`] early in the application.
pub fn use_head() -> MetaContext {
#[cfg(debug_assertions)]
feature_warning();
match use_context::<MetaContext>() {
None => {
debug_warn!(
@ -265,9 +190,6 @@ impl MetaContext {
/// # }
/// ```
pub fn dehydrate(&self) -> String {
use leptos::leptos_dom::HydrationCtx;
let prev_key = HydrationCtx::peek();
let mut tags = String::new();
// Title
@ -278,9 +200,6 @@ impl MetaContext {
}
tags.push_str(&self.tags.as_string());
if let Some(prev_key) = prev_key {
HydrationCtx::continue_from(prev_key);
}
tags
}
}
@ -310,10 +229,3 @@ pub fn generate_head_metadata_separated() -> (String, String) {
.unwrap_or_default();
(head, format!("<body{body_meta}>"))
}
#[cfg(debug_assertions)]
pub(crate) fn feature_warning() {
if !cfg!(any(feature = "csr", feature = "hydrate", feature = "ssr")) {
leptos::logging::debug_warn!("WARNING: `leptos_meta` does nothing unless you enable one of its features (`csr`, `hydrate`, or `ssr`). See the docs at https://docs.rs/leptos_meta/latest/leptos_meta/ for more information.");
}
}

View file

@ -1,37 +1,6 @@
//! This module contains the `Oco` (Owned Clones Once) smart pointer,
//! which is used to store immutable references to values.
//! This is useful for storing, for example, strings.
//!
//! Imagine this as an alternative to [`Cow`] with an additional, reference-counted
//! branch.
//!
//! ```rust
//! use oco_ref::Oco;
//! use std::rc::Rc;
//!
//! let static_str = "foo";
//! let rc_str: Rc<str> = "bar".into();
//! let owned_str: String = "baz".into();
//!
//! fn uses_oco(value: impl Into<Oco<'static, str>>) {
//! let mut value = value.into();
//!
//! // ensures that the value is either a reference, or reference-counted
//! // O(n) at worst
//! let clone1 = value.clone_inplace();
//!
//! // these subsequent clones are O(1)
//! let clone2 = value.clone();
//! let clone3 = value.clone();
//! }
//!
//! uses_oco(static_str);
//! uses_oco(rc_str);
//! uses_oco(owned_str);
//! ```
#![forbid(unsafe_code)]
#![deny(missing_docs)]
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
@ -44,24 +13,16 @@ use std::{
rc::Rc,
};
/// "Owned Clones Once": a smart pointer that can be either a reference,
/// an owned value, or a reference-counted pointer. This is useful for
/// "Owned Clones Once" - a smart pointer that can be either a reference,
/// an owned value, or a reference counted pointer. This is useful for
/// storing immutable values, such as strings, in a way that is cheap to
/// clone and pass around.
///
/// The cost of the `Clone` implementation depends on the branch. Cloning the [`Oco::Borrowed`]
/// The `Clone` implementation is amortized `O(1)`. Cloning the [`Oco::Borrowed`]
/// variant simply copies the references (`O(1)`). Cloning the [`Oco::Counted`]
/// variant increments a reference count (`O(1)`). Cloning the [`Oco::Owned`]
/// variant requires an `O(n)` clone of the data.
///
/// For an amortized `O(1)` clone, you can use [`Oco::clone_inplace()`]. Using this method,
/// [`Oco::Borrowed`] and [`Oco::Counted`] are still `O(1)`. [`Oco::Owned`] does a single `O(n)`
/// clone, but converts the object to the [`Oco::Counted`] branch, which means future clones will
/// be `O(1)`.
///
/// In general, you'll either want to call `clone_inplace()` once, before sharing the `Oco` with
/// other parts of your application (so that all future clones are `O(1)`), or simply use this as
/// if it is a [`Cow`] with an additional branch for reference-counted values.
/// variant upgrades it to [`Oco::Counted`], which requires an `O(n)` clone of the
/// data, but all subsequent clones will be `O(1)`.
pub enum Oco<'a, T: ?Sized + ToOwned + 'a> {
/// A static reference to a value.
Borrowed(&'a T),
@ -85,7 +46,7 @@ impl<'a, T: ?Sized + ToOwned> Oco<'a, T> {
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Borrowed("Hello").is_borrowed());
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_borrowed());
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_borrowed());
@ -98,7 +59,7 @@ impl<'a, T: ?Sized + ToOwned> Oco<'a, T> {
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Counted(Rc::from("Hello")).is_counted());
/// assert!(!Oco::<str>::Borrowed("Hello").is_counted());
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_counted());
@ -111,7 +72,7 @@ impl<'a, T: ?Sized + ToOwned> Oco<'a, T> {
/// # Examples
/// ```
/// # use std::rc::Rc;
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// assert!(Oco::<str>::Owned("Hello".to_string()).is_owned());
/// assert!(!Oco::<str>::Borrowed("Hello").is_owned());
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_owned());
@ -169,7 +130,7 @@ impl Oco<'_, str> {
/// Returns a `&str` slice of this [`Oco`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<str>::Borrowed("Hello");
/// let s: &str = oco.as_str();
/// assert_eq!(s, "Hello");
@ -184,7 +145,7 @@ impl Oco<'_, CStr> {
/// Returns a `&CStr` slice of this [`Oco`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::CStr;
///
/// let oco =
@ -202,7 +163,7 @@ impl Oco<'_, OsStr> {
/// Returns a `&OsStr` slice of this [`Oco`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// use std::ffi::OsStr;
///
/// let oco = Oco::<OsStr>::Borrowed(OsStr::new("Hello"));
@ -219,7 +180,7 @@ impl Oco<'_, Path> {
/// Returns a `&Path` slice of this [`Oco`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// use std::path::Path;
///
/// let oco = Oco::<Path>::Borrowed(Path::new("Hello"));
@ -239,7 +200,7 @@ where
/// Returns a `&[T]` slice of this [`Oco`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[u8]>::Borrowed(b"Hello");
/// let s: &[u8] = oco.as_slice();
/// assert_eq!(s, b"Hello");
@ -261,7 +222,7 @@ where
/// # Examples
/// [`String`] :
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<str>::Owned("Hello".to_string());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
@ -269,7 +230,7 @@ where
/// ```
/// [`Vec`] :
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[u8]>::Owned(b"Hello".to_vec());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
@ -293,7 +254,7 @@ where
/// was previously [`Oco::Owned`].
/// # Examples
/// ```
/// # use oco_ref::Oco;
/// # use leptos_reactive::oco::Oco;
/// let mut oco1 = Oco::<str>::Owned("Hello".to_string());
/// let oco2 = oco1.clone_inplace();
/// assert_eq!(oco1, oco2);

View file

@ -0,0 +1,179 @@
use super::{Attribute, NextAttribute};
use crate::renderer::Renderer;
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
pub struct AnyAttribute<R: Renderer> {
type_id: TypeId,
value: Box<dyn Any + Send + Sync>,
to_html:
fn(Box<dyn Any>, &mut String, &mut String, &mut String, &mut String),
build: fn(Box<dyn Any>, el: &R::Element) -> AnyAttributeState<R>,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyAttributeState<R>),
hydrate_from_server: fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
hydrate_from_template:
fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
}
pub struct AnyAttributeState<R>
where
R: Renderer,
{
type_id: TypeId,
state: Box<dyn Any>,
el: R::Element,
rndr: PhantomData<R>,
}
pub trait IntoAnyAttribute<R>
where
R: Renderer,
{
fn into_any_attr(self) -> AnyAttribute<R>;
}
impl<T, R> IntoAnyAttribute<R> for T
where
Self: Send + Sync,
T: Attribute<R> + 'static,
T::State: 'static,
R: Renderer + 'static,
R::Element: Clone,
{
// inlining allows the compiler to remove the unused functions
// i.e., doesn't ship HTML-generating code that isn't used
#[inline(always)]
fn into_any_attr(self) -> AnyAttribute<R> {
let value = Box::new(self) as Box<dyn Any + Send + Sync>;
let to_html = |value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::to_html could not be downcast");
value.to_html(buf, class, style, inner_html);
};
let build = |value: Box<dyn Any>, el: &R::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
rndr: PhantomData,
}
};
let hydrate_from_server = |value: Box<dyn Any>, el: &R::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::hydrate_from_server couldn't downcast");
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
rndr: PhantomData,
}
};
let hydrate_from_template = |value: Box<dyn Any>, el: &R::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::hydrate_from_server couldn't downcast");
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
rndr: PhantomData,
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState<R>| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyAttribute::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
AnyAttribute {
type_id: TypeId::of::<T>(),
value,
to_html,
build,
rebuild,
hydrate_from_server,
hydrate_from_template,
}
}
}
impl<R> NextAttribute<R> for AnyAttribute<R>
where
R: Renderer,
{
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
fn add_any_attr<NewAttr: Attribute<R>>(
self,
new_attr: NewAttr,
) -> Self::Output<NewAttr> {
(self, new_attr)
}
}
impl<R> Attribute<R> for AnyAttribute<R>
where
R: Renderer,
{
const MIN_LENGTH: usize = 0;
type State = AnyAttributeState<R>;
fn to_html(
self,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String,
) {
(self.to_html)(self.value, buf, class, style, inner_html);
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &<R as Renderer>::Element,
) -> Self::State {
if FROM_SERVER {
(self.hydrate_from_server)(self.value, el)
} else {
(self.hydrate_from_template)(self.value, el)
}
}
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
(self.build)(self.value, el)
}
fn rebuild(self, state: &mut Self::State) {
(self.rebuild)(self.type_id, self.value, state)
}
}

View file

@ -1,3 +1,4 @@
pub mod any_attribute;
pub mod aria;
pub mod custom;
pub mod global;

View file

@ -12,6 +12,7 @@ where
{
type_id: TypeId,
value: Box<dyn Any>,
// TODO add async HTML rendering for AnyView
to_html: fn(Box<dyn Any>, &mut String, &mut Position),
build: fn(Box<dyn Any>) -> AnyViewState<R>,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),