feat: optional branch-marking in HTML to support initial work on client-side islands routing

This commit is contained in:
Greg Johnston 2024-07-21 07:26:26 -04:00
parent e3482b433b
commit 8635887ca7
48 changed files with 890 additions and 141 deletions

View file

@ -0,0 +1,92 @@
[package]
name = "islands"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
futures = "0.3"
http = "1.0"
leptos = { path = "../../leptos", features = [
"tracing",
"experimental-islands",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = ["islands-router"], optional = true }
log = "0.4"
serde = { version = "1", features = ["derive"] }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
wasm-bindgen = "0.2"
[features]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"dep:leptos_axum",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "islands"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]

View file

@ -0,0 +1,19 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,135 @@
window.addEventListener("click", async (ev) => {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
ev.preventDefault();
// fetch the new page
const resp = await fetch(url);
const htmlString = await resp.text();
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
// TODO parse from the request stream instead?
const doc = parser.parseFromString(htmlString, 'text/html');
// The 'doc' variable now contains the parsed DOM
const transition = document.startViewTransition(async () => {
const oldDocWalker = document.createTreeWalker(document);
const newDocWalker = doc.createTreeWalker(doc);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo") && newText !== oldText) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0 && newBranches > 0) {
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndBefore(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
} }
}
});
await transition;
window.history.pushState(undefined, null, url);
});

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View file

@ -0,0 +1,59 @@
use leptos::prelude::*;
use leptos_router::{
components::{FlatRoutes, Route, Router},
StaticSegment,
};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options=options islands=true/>
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
view! {
<script src="/routing.js"></script>
<Router>
<header>
<h1>"My Application"</h1>
</header>
<nav>
<a href="/">"Page A"</a>
<a href="/b">"Page B"</a>
</nav>
<main>
<p>
<label>"Home Checkbox" <input type="checkbox"/></label>
</p>
<FlatRoutes fallback=|| "Not found.">
<Route path=StaticSegment("") view=PageA/>
<Route path=StaticSegment("b") view=PageB/>
</FlatRoutes>
</main>
</Router>
}
}
#[component]
pub fn PageA() -> impl IntoView {
view! { <label>"Page A" <input type="checkbox"/></label> }
}
#[component]
pub fn PageB() -> impl IntoView {
view! { <label>"Page B" <input type="checkbox"/></label> }
}

View file

@ -0,0 +1,8 @@
pub mod app;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
leptos::mount::hydrate_islands();
}

View file

@ -0,0 +1,30 @@
use axum::Router;
use islands::app::{shell, App};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
#[tokio::main]
async fn main() {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View file

@ -0,0 +1,3 @@
.pending {
color: purple;
}

View file

@ -28,3 +28,6 @@ send_wrapper = "0.6.0"
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = []

View file

@ -37,6 +37,7 @@ tokio = { version = "1", features = ["net"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
islands-router = []
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View file

@ -676,8 +676,12 @@ where
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
as PinnedStream<String>
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_out_of_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
})
}
@ -723,9 +727,13 @@ where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
Box::pin(async move {
Box::pin(app.to_html_stream_in_order().chain(chunks()))
as PinnedStream<String>
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
})
}
@ -922,7 +930,12 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
@ -971,7 +984,12 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>

View file

@ -274,20 +274,29 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// first, attempt to serialize the children to HTML, then check for errors
let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
let mut new_pos = *position;
self.children
.to_html_with_buf(&mut new_buf, &mut new_pos, escape);
self.children.to_html_with_buf(
&mut new_buf,
&mut new_pos,
escape,
mark_branches,
);
// any thrown errors would've been caught here
if self.errors.with_untracked(|map| map.is_empty()) {
buf.push_str(&new_buf);
} else {
// otherwise, serialize the fallback instead
(self.fallback)(self.errors)
.to_html_with_buf(buf, position, escape);
(self.fallback)(self.errors).to_html_with_buf(
buf,
position,
escape,
mark_branches,
);
}
}
@ -296,6 +305,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -306,6 +316,7 @@ where
&mut new_buf,
&mut new_pos,
escape,
mark_branches,
);
if let Some(sc) = Owner::current_shared_context() {
@ -322,6 +333,7 @@ where
&mut fallback,
position,
escape,
mark_branches,
);
buf.push_sync(&fallback);
}

View file

@ -62,8 +62,10 @@ impl<T: IntoView> RenderHtml<Dom> for View<T> {
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.0.to_html_with_buf(buf, position, escape);
self.0
.to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -71,11 +73,16 @@ impl<T: IntoView> RenderHtml<Dom> for View<T> {
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
self.0
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
self.0.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
)
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -192,8 +192,10 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.fallback.to_html_with_buf(buf, position, escape);
self.fallback
.to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -201,6 +203,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -292,13 +295,19 @@ where
Some(Some(resolved)) => {
Either::<Fal, _>::Right(resolved)
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf, position, escape,
buf,
position,
escape,
mark_branches,
);
}
Some(None) => {
Either::<_, Chil>::Left(self.fallback)
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf, position, escape,
buf,
position,
escape,
mark_branches,
);
}
None => {
@ -308,8 +317,12 @@ where
// wrapped by suspense markers
if OUT_OF_ORDER {
let mut fallback_position = *position;
buf.push_fallback(self.fallback, &mut fallback_position);
buf.push_async_out_of_order(fut, position);
buf.push_fallback(
self.fallback,
&mut fallback_position,
mark_branches,
);
buf.push_async_out_of_order(fut, position, mark_branches);
} else {
buf.push_async({
let mut position = *position;
@ -323,6 +336,7 @@ where
&mut builder,
&mut position,
escape,
mark_branches,
);
builder.finish().take_chunks()
}

View file

@ -266,9 +266,14 @@ mod view_implementations {
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
(move || Suspend::new(async move { self.await }))
.to_html_with_buf(buf, position, escape);
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -276,11 +281,17 @@ mod view_implementations {
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
(move || Suspend::new(async move { self.await }))
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -122,6 +122,7 @@ where
_buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();

View file

@ -122,6 +122,7 @@ where
_buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();

View file

@ -337,6 +337,7 @@ where
&mut buf,
&mut Position::NextChild,
false,
false,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
@ -438,6 +439,7 @@ where
_buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@ -547,6 +549,7 @@ impl RenderHtml<Dom> for MetaTagsView {
buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
buf.push_str("<!--HEAD-->");
}

View file

@ -250,6 +250,7 @@ impl RenderHtml<Dom> for TitleView {
_buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server

View file

@ -483,6 +483,7 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
@ -531,7 +532,7 @@ where
RouteList::register(RouteList::from(routes));
} else {
let view = self.choose_ssr();
view.to_html_with_buf(buf, position, escape);
view.to_html_with_buf(buf, position, escape, mark_branches);
}
}
@ -540,11 +541,17 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let view = self.choose_ssr();
view.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
)
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -88,9 +88,8 @@ impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
#[cfg(test)]
mod tests {
use crate::AsPath;
use super::{PossibleRouteMatch, StaticSegment};
use crate::AsPath;
#[derive(Debug, Clone)]
enum Paths {

View file

@ -246,6 +246,7 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
@ -331,7 +332,7 @@ where
})
}
};
view.to_html_with_buf(buf, position, escape);
view.to_html_with_buf(buf, position, escape, mark_branches);
}
}
@ -340,6 +341,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -381,7 +383,12 @@ where
})
}
};
view.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -131,7 +131,7 @@ where
self,
buf: &mut String,
position: &mut Position,
escape: bool,
escape: bool, mark_branches: bool
) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
@ -156,7 +156,7 @@ where
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
escape: bool, mark_branches: bool
) where
Self: Sized,
{
@ -324,7 +324,7 @@ where
self,
buf: &mut String,
position: &mut Position,
escape: bool,
escape: bool, mark_branches: bool
) {
let MatchedRoute {
search_params,
@ -345,7 +345,7 @@ where
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
escape: bool, mark_branches: bool
) where
Self: Sized,
{

View file

@ -205,7 +205,7 @@ where
self
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
// add routes
@ -272,7 +272,7 @@ where
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
let outer_owner =
@ -701,14 +701,14 @@ where
//(self.inner.read().or_poisoned().html_len)()
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
/*let view = self.inner.read().or_poisoned().view.take().unwrap();
view.to_html_with_buf(buf, position);*/
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
/*let view = self
@ -971,14 +971,14 @@ where
self.view.html_len()
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
buf.reserve(self.html_len());
self.view.to_html_with_buf(buf, position, escape);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
buf.reserve(self.html_len());
@ -1228,7 +1228,7 @@ where
self
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
// add routes
@ -1315,7 +1315,7 @@ where
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
let outer_owner =

View file

@ -36,7 +36,7 @@ macro_rules! attributes {
attributes! {
// HTML
/// The `abbr` attribute specifies an abbreviated form of the element's content.
abbr "abbr",
abbr "abbr",
/// The `accept-charset` attribute specifies the character encodings that are to be used for the form submission.
accept_charset "accept-charset",
/// The `accept` attribute specifies a list of types the server accepts, typically a file type.
@ -582,7 +582,7 @@ attributes! {
/// The `onwheel` attribute specifies the event handler for the wheel event.
onwheel "onwheel",
// MathML attributes
// MathML attributes
/// The `accent` attribute specifies whether the element should be treated as an accent.
accent "accent",
/// The `accentunder` attribute specifies whether the element should be treated as an accent under the base element.

View file

@ -270,6 +270,7 @@ where
buf: &mut String,
position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
// opening tag
buf.push('<');
@ -289,6 +290,7 @@ where
buf,
position,
E::ESCAPE_CHILDREN,
mark_branches,
);
}
@ -305,6 +307,7 @@ where
buffer: &mut StreamBuilder,
position: &mut Position,
_escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -328,6 +331,7 @@ where
buffer,
position,
E::ESCAPE_CHILDREN,
mark_branches,
);
}

View file

@ -115,9 +115,11 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
Self::open_tag(self.component, buf);
self.view.to_html_with_buf(buf, position, escape);
self.view
.to_html_with_buf(buf, position, escape, mark_branches);
Self::close_tag(buf);
}
@ -126,6 +128,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -135,8 +138,12 @@ where
buf.push_sync(&tag);
// streaming render for the view
self.view
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
self.view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
// and insert the closing tag synchronously
tag.clear();
@ -243,9 +250,11 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
Self::open_tag(buf);
self.view.to_html_with_buf(buf, position, escape);
self.view
.to_html_with_buf(buf, position, escape, mark_branches);
Self::close_tag(buf);
}
@ -254,6 +263,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -263,8 +273,12 @@ where
buf.push_sync(&tag);
// streaming render for the view
self.view
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
self.view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
// and insert the closing tag synchronously
tag.clear();

View file

@ -67,6 +67,7 @@ where
buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
buf.push_str("<!DOCTYPE ");
buf.push_str(self.value);

View file

@ -52,8 +52,15 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
<&str as RenderHtml<R>>::to_html_with_buf(&self, buf, position, escape)
<&str as RenderHtml<R>>::to_html_with_buf(
&self,
buf,
position,
escape,
mark_branches,
)
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -103,7 +103,7 @@ macro_rules! render_primitive {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
// add a comment node to separate from previous sibling, if any
if matches!(position, Position::NextChildAfterText) {
buf.push_str("<!>")
@ -264,7 +264,7 @@ where
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
<&str as RenderHtml<R>>::to_html_with_buf(&self, buf, position)
}

View file

@ -144,9 +144,10 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
let value = self.invoke();
value.to_html_with_buf(buf, position, escape)
value.to_html_with_buf(buf, position, escape, mark_branches)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -154,11 +155,17 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let value = self.invoke();
value.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
value.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
fn hydrate<const FROM_SERVER: bool>(
@ -568,9 +575,10 @@ mod stable {
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
let value = self.get();
value.to_html_with_buf(buf, position, escape)
value.to_html_with_buf(buf, position, escape, mark_branches)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -578,12 +586,16 @@ mod stable {
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let value = self.get();
value.to_html_async_with_buf::<OUT_OF_ORDER>(
buf, position, escape,
buf,
position,
escape,
mark_branches,
);
}
@ -730,9 +742,10 @@ mod stable {
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
let value = self.get();
value.to_html_with_buf(buf, position, escape)
value.to_html_with_buf(buf, position, escape, mark_branches)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -740,12 +753,16 @@ mod stable {
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let value = self.get();
value.to_html_async_with_buf::<OUT_OF_ORDER>(
buf, position, escape,
buf,
position,
escape,
mark_branches,
);
}

View file

@ -123,9 +123,12 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.owner
.with(|| self.view.to_html_with_buf(buf, position, escape));
self.owner.with(|| {
self.view
.to_html_with_buf(buf, position, escape, mark_branches)
});
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -133,12 +136,17 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
self.owner.with(|| {
self.view
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
self.view.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
)
});
// if self.owner drops here, it can be disposed before the asynchronous rendering process

View file

@ -175,12 +175,13 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// TODO wrap this with a Suspense as needed
// currently this is just used for Routes, which creates a Suspend but never actually needs
// it (because we don't lazy-load routes on the server)
if let Some(inner) = self.0.now_or_never() {
inner.to_html_with_buf(buf, position, escape);
inner.to_html_with_buf(buf, position, escape, mark_branches);
}
}
@ -189,13 +190,18 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let mut fut = Box::pin(self.0);
match fut.as_mut().now_or_never() {
Some(inner) => inner
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape),
Some(inner) => inner.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
),
None => {
if use_context::<SuspenseContext>().is_none() {
buf.next_id();
@ -218,8 +224,13 @@ where
buf.push_fallback::<(), Rndr>(
(),
&mut fallback_position,
mark_branches,
);
buf.push_async_out_of_order(
fut,
position,
mark_branches,
);
buf.push_async_out_of_order(fut, position);
} else {
buf.push_async({
let mut position = *position;
@ -230,6 +241,7 @@ where
&mut builder,
&mut position,
escape,
mark_branches,
);
builder.finish().take_chunks()
}

View file

@ -15,7 +15,7 @@ use std::{
/// Manages streaming HTML rendering for the response to a single request.
#[derive(Default)]
pub struct StreamBuilder {
sync_buf: String,
pub(crate) sync_buf: String,
pub(crate) chunks: VecDeque<StreamChunk>,
pending: Option<ChunkFuture>,
pending_ooo: VecDeque<PinnedFuture<OooChunk>>,
@ -104,12 +104,18 @@ impl StreamBuilder {
&mut self,
fallback: View,
position: &mut Position,
mark_branches: bool,
) where
View: RenderHtml<Rndr>,
Rndr: Renderer,
{
self.write_chunk_marker(true);
fallback.to_html_with_buf(&mut self.sync_buf, position, true);
fallback.to_html_with_buf(
&mut self.sync_buf,
position,
true,
mark_branches,
);
self.write_chunk_marker(false);
*position = Position::NextChild;
}
@ -156,6 +162,7 @@ impl StreamBuilder {
&mut self,
view: impl Future<Output = Option<View>> + Send + 'static,
position: &mut Position,
mark_branches: bool,
) where
View: RenderHtml<Rndr>,
Rndr: Renderer,
@ -185,6 +192,7 @@ impl StreamBuilder {
&mut subbuilder,
&mut position,
true,
mark_branches,
);
}
let chunks = subbuilder.finish().take_chunks();

View file

@ -38,12 +38,13 @@ where
#[cfg(feature = "ssr")]
html_len: usize,
#[cfg(feature = "ssr")]
to_html: fn(Box<dyn Any>, &mut String, &mut Position, bool),
to_html: fn(Box<dyn Any>, &mut String, &mut Position, bool, bool),
#[cfg(feature = "ssr")]
to_html_async: fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool),
to_html_async:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
#[cfg(feature = "ssr")]
to_html_async_ooo:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool),
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
build: fn(Box<dyn Any>) -> AnyViewState<R>,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
#[cfg(feature = "ssr")]
@ -175,32 +176,46 @@ where
let to_html = |value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool| {
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_with_buf(buf, position, escape);
value.to_html_with_buf(buf, position, escape, mark_branches);
};
#[cfg(feature = "ssr")]
let to_html_async = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool| {
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<false>(buf, position, escape);
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
};
#[cfg(feature = "ssr")]
let to_html_async_ooo = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(buf, position, escape);
};
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
@ -347,9 +362,10 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
#[cfg(feature = "ssr")]
(self.to_html)(self.value, buf, position, escape);
(self.to_html)(self.value, buf, position, escape, mark_branches);
#[cfg(not(feature = "ssr"))]
{
_ = buf;
@ -367,14 +383,27 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
#[cfg(feature = "ssr")]
if OUT_OF_ORDER {
(self.to_html_async_ooo)(self.value, buf, position, escape);
(self.to_html_async_ooo)(
self.value,
buf,
position,
escape,
mark_branches,
);
} else {
(self.to_html_async)(self.value, buf, position, escape);
(self.to_html_async)(
self.value,
buf,
position,
escape,
mark_branches,
);
}
#[cfg(not(feature = "ssr"))]
{

View file

@ -1,6 +1,6 @@
use super::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, MarkBranch, Mountable, Position, PositionState,
Render, RenderHtml,
};
use crate::{
html::attribute::Attribute, hydration::Cursor, renderer::Renderer,
@ -155,11 +155,26 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
match self {
Either::Left(left) => left.to_html_with_buf(buf, position, escape),
Either::Left(left) => {
if mark_branches {
buf.open_branch("0");
}
left.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch("0");
}
}
Either::Right(right) => {
right.to_html_with_buf(buf, position, escape)
if mark_branches {
buf.open_branch("1");
}
right.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch("1");
}
}
}
}
@ -169,14 +184,39 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
match self {
Either::Left(left) => left
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape),
Either::Right(right) => right
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape),
Either::Left(left) => {
if mark_branches {
buf.open_branch("0");
}
left.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch("0");
}
}
Either::Right(right) => {
if mark_branches {
buf.open_branch("1");
}
right.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch("1");
}
}
}
}
@ -317,6 +357,7 @@ where
_buf: &mut String,
_position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
todo!()
}
@ -528,19 +569,35 @@ macro_rules! tuples {
}
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
match self {
$([<EitherOf $num>]::$ty(this) => this.to_html_with_buf(buf, position, escape),)*
$([<EitherOf $num>]::$ty(this) => {
if mark_branches {
buf.open_branch(stringify!($ty));
}
this.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch(stringify!($ty));
}
})*
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
match self {
$([<EitherOf $num>]::$ty(this) => this.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape),)*
$([<EitherOf $num>]::$ty(this) => {
if mark_branches {
buf.open_branch(stringify!($ty));
}
this.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch(stringify!($ty));
}
})*
}
}

View file

@ -157,9 +157,12 @@ where
buf: &mut String,
position: &mut super::Position,
escape: bool,
mark_branches: bool,
) {
match self {
Ok(inner) => inner.to_html_with_buf(buf, position, escape),
Ok(inner) => {
inner.to_html_with_buf(buf, position, escape, mark_branches)
}
Err(e) => {
throw_error::throw(e);
}
@ -171,12 +174,17 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
match self {
Ok(inner) => inner
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape),
Ok(inner) => inner.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
),
Err(e) => {
throw_error::throw(e);
}

View file

@ -90,12 +90,13 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
match self {
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.to_html_with_buf(buf, position, escape)
.to_html_with_buf(buf, position, escape, mark_branches)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -103,6 +104,7 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
@ -110,7 +112,12 @@ where
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
)
}
#[track_caller]
@ -282,13 +289,14 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
let mut children = self.into_iter();
if let Some(first) = children.next() {
first.to_html_with_buf(buf, position, escape);
first.to_html_with_buf(buf, position, escape, mark_branches);
}
for child in children {
child.to_html_with_buf(buf, position, escape);
child.to_html_with_buf(buf, position, escape, mark_branches);
}
buf.push_str("<!>");
}
@ -298,15 +306,26 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let mut children = self.into_iter();
if let Some(first) = children.next() {
first.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
first.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
for child in children {
child.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
child.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
buf.push_sync("<!>");
}

View file

@ -221,10 +221,11 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
for item in self.items.into_iter() {
let item = (self.view_fn)(item);
item.to_html_with_buf(buf, position, escape);
item.to_html_with_buf(buf, position, escape, mark_branches);
*position = Position::NextChild;
}
buf.push_str("<!>");
@ -235,10 +236,16 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
for item in self.items.into_iter() {
let item = (self.view_fn)(item);
item.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
item.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
*position = Position::NextChild;
}
buf.push_sync("<!>");

View file

@ -46,6 +46,40 @@ pub trait Render<R: Renderer>: Sized {
fn rebuild(self, state: &mut Self::State);
}
pub(crate) trait MarkBranch {
fn open_branch(&mut self, branch_id: &str);
fn close_branch(&mut self, branch_id: &str);
}
impl MarkBranch for String {
fn open_branch(&mut self, branch_id: &str) {
self.push_str("<!--bo-");
self.push_str(branch_id);
self.push_str("-->");
}
fn close_branch(&mut self, branch_id: &str) {
self.push_str("<!--bc-");
self.push_str(branch_id);
self.push_str("-->");
}
}
impl MarkBranch for StreamBuilder {
fn open_branch(&mut self, branch_id: &str) {
self.sync_buf.push_str("<!--bo-");
self.sync_buf.push_str(branch_id);
self.sync_buf.push_str("-->");
}
fn close_branch(&mut self, branch_id: &str) {
self.sync_buf.push_str("<!--bc-");
self.sync_buf.push_str(branch_id);
self.sync_buf.push_str("-->");
}
}
/// The `RenderHtml` trait allows rendering something to HTML, and transforming
/// that HTML into an interactive interface.
///
@ -94,7 +128,19 @@ where
Self: Sized,
{
let mut buf = String::with_capacity(self.html_len());
self.to_html_with_buf(&mut buf, &mut Position::FirstChild, true);
self.to_html_with_buf(&mut buf, &mut Position::FirstChild, true, false);
buf
}
/// Renders a view to HTML with branch markers. This can be used to support libraries that diff
/// HTML pages against one another, by marking sections of the view that branch to different
/// types with marker comments.
fn to_html_branching(self) -> String
where
Self: Sized,
{
let mut buf = String::with_capacity(self.html_len());
self.to_html_with_buf(&mut buf, &mut Position::FirstChild, true, true);
buf
}
@ -108,6 +154,24 @@ where
&mut builder,
&mut Position::FirstChild,
true,
false,
);
builder.finish()
}
/// Renders a view to an in-order stream of HTML with branch markers. This can be used to support libraries that diff
/// HTML pages against one another, by marking sections of the view that branch to different
/// types with marker comments.
fn to_html_stream_in_order_branching(self) -> StreamBuilder
where
Self: Sized,
{
let mut builder = StreamBuilder::with_capacity(self.html_len(), None);
self.to_html_async_with_buf::<false>(
&mut builder,
&mut Position::FirstChild,
true,
true,
);
builder.finish()
}
@ -125,25 +189,29 @@ where
&mut builder,
&mut Position::FirstChild,
true,
false,
);
builder.finish()
}
/// Renders a view to an out-of-order stream of HTML with branch markers. This can be used to support libraries that diff
/// HTML pages against one another, by marking sections of the view that branch to different
/// types with marker comments.
fn to_html_stream_out_of_order_branching(self) -> StreamBuilder
where
Self: Sized,
{
let mut builder =
StreamBuilder::with_capacity(self.html_len(), Some(vec![0]));
self.to_html_async_with_buf::<true>(
&mut builder,
&mut Position::FirstChild,
true,
true,
);
builder.finish()
/*let mut b = builder.finish();
let last = b.chunks.pop_back().unwrap();
match &last {
crate::ssr::StreamChunk::Sync(s) => {
println!("actual = {}", s.len())
}
crate::ssr::StreamChunk::Async {
chunks,
should_block,
} => todo!(),
crate::ssr::StreamChunk::OutOfOrder {
chunks,
should_block,
} => todo!(),
}
b.chunks.push_back(last);
b*/
}
/// Renders a view to HTML, writing it into the given buffer.
@ -152,6 +220,7 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
);
/// Renders a view into a buffer of (synchronous or asynchronous) HTML chunks.
@ -160,10 +229,13 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
buf.with_buf(|buf| self.to_html_with_buf(buf, position, escape));
buf.with_buf(|buf| {
self.to_html_with_buf(buf, position, escape, mark_branches)
});
}
/// Makes a set of DOM nodes rendered from HTML interactive.

View file

@ -76,7 +76,7 @@ macro_rules! render_primitive {
self
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, _escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, _escape: bool, mark_branches: bool) {
// add a comment node to separate from previous sibling, if any
if matches!(position, Position::NextChildAfterText) {
buf.push_str("<!>")

View file

@ -191,6 +191,7 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// add a comment node to separate from previous sibling, if any
if matches!(position, Position::NextChildAfterText) {

View file

@ -59,6 +59,7 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
// add a comment node to separate from previous sibling, if any
if matches!(position, Position::NextChildAfterText) {
@ -188,12 +189,14 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
<&str as RenderHtml<R>>::to_html_with_buf(
self.as_str(),
buf,
position,
escape,
mark_branches,
)
}
@ -284,7 +287,7 @@ where
self.len()
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
<&str as RenderHtml<R>>::to_html_with_buf(&self, buf, position)
}
@ -380,8 +383,15 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
<&str as RenderHtml<R>>::to_html_with_buf(&self, buf, position, escape)
<&str as RenderHtml<R>>::to_html_with_buf(
&self,
buf,
position,
escape,
mark_branches,
)
}
fn hydrate<const FROM_SERVER: bool>(
@ -476,8 +486,15 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
<&str as RenderHtml<R>>::to_html_with_buf(&self, buf, position, escape)
<&str as RenderHtml<R>>::to_html_with_buf(
&self,
buf,
position,
escape,
mark_branches,
)
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -91,8 +91,10 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.view.to_html_with_buf(buf, position, escape)
self.view
.to_html_with_buf(buf, position, escape, mark_branches)
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -35,6 +35,7 @@ where
buf: &mut String,
position: &mut Position,
_escape: bool,
mark_branches: bool,
) {
buf.push_str("<!>");
*position = Position::NextChild;
@ -124,8 +125,10 @@ where
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.0.to_html_with_buf(buf, position, escape);
self.0
.to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@ -133,11 +136,16 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
self.0
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
self.0.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
);
}
fn hydrate<const FROM_SERVER: bool>(
@ -238,22 +246,22 @@ macro_rules! impl_view_for_tuples {
$($ty.html_len() +)* $first.html_len()
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
#[allow(non_snake_case)]
let ($first, $($ty,)* ) = self;
$first.to_html_with_buf(buf, position, escape);
$($ty.to_html_with_buf(buf, position, escape));*
$first.to_html_with_buf(buf, position, escape, mark_branches);
$($ty.to_html_with_buf(buf, position, escape, mark_branches));*
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
Self: Sized,
{
#[allow(non_snake_case)]
let ($first, $($ty,)* ) = self;
$first.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape);
$($ty.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape));*
$first.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches);
$($ty.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches));*
}
fn hydrate<const FROM_SERVER: bool>(self, cursor: &Cursor<Rndr>, position: &PositionState) -> Self::State {