#![deny(missing_docs)]
#![forbid(unsafe_code)]
use futures::{Stream, StreamExt};
use leptos::{
attr::{any_attribute::AnyAttribute, NextAttribute},
component,
logging::debug_warn,
oco::Oco,
reactive::owner::{provide_context, use_context},
tachys::{
dom::document,
html::{
attribute::Attribute,
element::{ElementType, HtmlElement},
},
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
},
IntoView,
};
use send_wrapper::SendWrapper;
use std::{
fmt::Debug,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, LazyLock,
},
};
use wasm_bindgen::JsCast;
use web_sys::HtmlHeadElement;
mod body;
mod html;
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;
mod title;
pub use body::*;
pub use html::*;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
pub use style::*;
pub use stylesheet::*;
pub use title::*;
#[derive(Clone, Debug)]
pub struct MetaContext {
pub(crate) title: TitleContext,
pub(crate) cursor: Arc<LazyLock<SendWrapper<Cursor>>>,
}
impl MetaContext {
pub fn new() -> Self {
Default::default()
}
}
pub(crate) const HEAD_MARKER_COMMENT: &str = "HEAD";
const COMMENT_NODE: u16 = 8;
impl Default for MetaContext {
fn default() -> Self {
let build_cursor: fn() -> SendWrapper<Cursor> = || {
let head = document().head().expect("missing <head> element");
let mut cursor = None;
let mut child = head.first_child();
while let Some(this_child) = child {
if this_child.node_type() == COMMENT_NODE
&& this_child.text_content().as_deref()
== Some(HEAD_MARKER_COMMENT)
{
cursor = Some(this_child);
break;
}
child = this_child.next_sibling();
}
SendWrapper::new(Cursor::new(
cursor
.expect(
"no leptos_meta HEAD marker comment found. Did you \
include the <MetaTags/> component in the <head> of \
your server-rendered app?",
)
.unchecked_into(),
))
};
let cursor = Arc::new(LazyLock::new(build_cursor));
Self {
title: Default::default(),
cursor,
}
}
}
#[derive(Clone, Debug)]
pub struct ServerMetaContext {
pub(crate) title: TitleContext,
pub(crate) html: Sender<String>,
pub(crate) body: Sender<String>,
#[allow(unused)] pub(crate) elements: Sender<String>,
}
#[must_use = "If you do not use the output, adding meta tags will have no \
effect."]
#[derive(Debug)]
pub struct ServerMetaContextOutput {
pub(crate) title: TitleContext,
html: Receiver<String>,
body: Receiver<String>,
elements: Receiver<String>,
}
impl ServerMetaContext {
pub fn new() -> (ServerMetaContext, ServerMetaContextOutput) {
let title = TitleContext::default();
let (html_tx, html_rx) = channel();
let (body_tx, body_rx) = channel();
let (elements_tx, elements_rx) = channel();
let tx = ServerMetaContext {
title: title.clone(),
html: html_tx,
body: body_tx,
elements: elements_tx,
};
let rx = ServerMetaContextOutput {
title,
html: html_rx,
body: body_rx,
elements: elements_rx,
};
(tx, rx)
}
}
impl ServerMetaContextOutput {
pub async fn inject_meta_context(
self,
mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send {
leptos::task::tick().await;
let mut first_chunk = stream.next().await.unwrap_or_default();
let title = self.title.as_string();
let title_len = title
.as_ref()
.map(|n| "<title>".len() + n.len() + "</title>".len())
.unwrap_or(0);
let meta_buf = self.elements.try_iter().collect::<String>();
let html_attrs = self.html.try_iter().collect::<String>();
let body_attrs = self.body.try_iter().collect::<String>();
let mut modified_chunk = if title_len == 0 && meta_buf.is_empty() {
first_chunk
} else {
let mut buf = String::with_capacity(
first_chunk.len() + title_len + meta_buf.len(),
);
let head_loc = first_chunk
.find("</head>")
.expect("you are using leptos_meta without a </head> tag");
let marker_loc = first_chunk
.find("<!--HEAD-->")
.map(|pos| pos + "<!--HEAD-->".len())
.unwrap_or_else(|| {
first_chunk.find("</head>").unwrap_or(head_loc)
});
let (before_marker, after_marker) =
first_chunk.split_at_mut(marker_loc);
buf.push_str(before_marker);
buf.push_str(&meta_buf);
if let Some(title) = title {
buf.push_str("<title>");
buf.push_str(&title);
buf.push_str("</title>");
}
buf.push_str(after_marker);
buf
};
if !html_attrs.is_empty() {
if let Some(index) = modified_chunk.find("<html") {
let insert_pos = index + "<html".len();
modified_chunk.insert_str(insert_pos, &html_attrs);
}
}
if !body_attrs.is_empty() {
if let Some(index) = modified_chunk.find("<body") {
let insert_pos = index + "<body".len();
modified_chunk.insert_str(insert_pos, &body_attrs);
}
}
futures::stream::once(async move { modified_chunk }).chain(stream)
}
}
pub fn provide_meta_context() {
if use_context::<MetaContext>().is_none() {
provide_context(MetaContext::new());
}
}
pub fn use_head() -> MetaContext {
match use_context::<MetaContext>() {
None => {
debug_warn!(
"use_head() is being called without a MetaContext being \
provided. We'll automatically create and provide one, but if \
this is being called in a child route it may cause bugs. To \
be safe, you should provide_meta_context() somewhere in the \
root of the app."
);
let meta = MetaContext::new();
provide_context(meta.clone());
meta
}
Some(ctx) => ctx,
}
}
pub(crate) fn register<E, At, Ch>(
el: HtmlElement<E, At, Ch>,
) -> RegisteredMetaTag<E, At, Ch>
where
HtmlElement<E, At, Ch>: RenderHtml,
{
RegisteredMetaTag { el }
}
struct RegisteredMetaTag<E, At, Ch> {
el: HtmlElement<E, At, Ch>,
}
struct RegisteredMetaTagState<E, At, Ch>
where
HtmlElement<E, At, Ch>: Render,
{
state: <HtmlElement<E, At, Ch> as Render>::State,
}
impl<E, At, Ch> Drop for RegisteredMetaTagState<E, At, Ch>
where
HtmlElement<E, At, Ch>: Render,
{
fn drop(&mut self) {
self.state.unmount();
}
}
fn document_head() -> HtmlHeadElement {
let document = document();
document.head().unwrap_or_else(|| {
let el = document.create_element("head").unwrap();
let document = document.document_element().unwrap();
_ = document.append_child(&el);
el.unchecked_into()
})
}
impl<E, At, Ch> Render for RegisteredMetaTag<E, At, Ch>
where
E: ElementType,
At: Attribute,
Ch: Render,
{
type State = RegisteredMetaTagState<E, At, Ch>;
fn build(self) -> Self::State {
let state = self.el.build();
RegisteredMetaTagState { state }
}
fn rebuild(self, state: &mut Self::State) {
self.el.rebuild(&mut state.state);
}
}
impl<E, At, Ch> AddAnyAttr for RegisteredMetaTag<E, At, Ch>
where
E: ElementType + Send,
At: Attribute + Send,
Ch: RenderHtml + Send,
{
type Output<SomeNewAttr: Attribute> =
RegisteredMetaTag<E, <At as NextAttribute>::Output<SomeNewAttr>, Ch>;
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
RegisteredMetaTag {
el: self.el.add_any_attr(attr),
}
}
}
impl<E, At, Ch> RenderHtml for RegisteredMetaTag<E, At, Ch>
where
E: ElementType,
At: Attribute,
Ch: RenderHtml + Send,
{
type AsyncOutput = Self;
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {
self.el.dry_resolve()
}
async fn resolve(self) -> Self::AsyncOutput {
self }
fn to_html_with_buf(
self,
_buf: &mut String,
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
#[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
self.el.to_html_with_buf(
&mut buf,
&mut Position::NextChild,
false,
false,
vec![],
);
_ = cx.elements.send(buf); } else {
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{msg}");
}
}
fn hydrate<const FROM_SERVER: bool>(
self,
_cursor: &Cursor,
_position: &PositionState,
) -> Self::State {
let cursor = use_context::<MetaContext>()
.expect(
"attempting to hydrate `leptos_meta` components without a \
MetaContext provided",
)
.cursor;
let state = self.el.hydrate::<FROM_SERVER>(
&cursor,
&PositionState::new(Position::NextChild),
);
RegisteredMetaTagState { state }
}
fn into_owned(self) -> Self::Owned {
RegisteredMetaTag {
el: self.el.into_owned(),
}
}
}
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
where
E: ElementType,
At: Attribute,
Ch: Render,
{
fn unmount(&mut self) {
self.state.unmount();
}
fn mount(
&mut self,
_parent: &leptos::tachys::renderer::types::Element,
_marker: Option<&leptos::tachys::renderer::types::Node>,
) {
self.state.mount(&document_head(), None);
}
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
self.state.elements()
}
}
#[component]
pub fn MetaTags() -> impl IntoView {
MetaTagsView
}
#[derive(Debug)]
struct MetaTagsView;
impl Render for MetaTagsView {
type State = ();
fn build(self) -> Self::State {}
fn rebuild(self, _state: &mut Self::State) {}
}
impl AddAnyAttr for MetaTagsView {
type Output<SomeNewAttr: Attribute> = MetaTagsView;
fn add_any_attr<NewAttr: Attribute>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
self
}
}
impl RenderHtml for MetaTagsView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn to_html_with_buf(
self,
buf: &mut String,
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
buf.push_str("<!--HEAD-->");
}
fn hydrate<const FROM_SERVER: bool>(
self,
_cursor: &Cursor,
_position: &PositionState,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub(crate) trait OrDefaultNonce {
fn or_default_nonce(self) -> Option<Oco<'static, str>>;
}
impl OrDefaultNonce for Option<Oco<'static, str>> {
fn or_default_nonce(self) -> Option<Oco<'static, str>> {
#[cfg(feature = "nonce")]
{
use leptos::nonce::use_nonce;
match self {
Some(nonce) => Some(nonce),
None => use_nonce().map(|n| Arc::clone(n.as_inner()).into()),
}
}
#[cfg(not(feature = "nonce"))]
{
self
}
}
}