Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR: Stringify VNodes (II) #1344

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
145e331
wip
philip-peterson Jun 21, 2020
3c01c22
rustfmt
philip-peterson Jun 21, 2020
169a3e2
VList, VText
philip-peterson Jun 21, 2020
9eb65ce
finish
philip-peterson Jun 21, 2020
47789d9
format
philip-peterson Jun 21, 2020
bf59552
fix bug
philip-peterson Jun 22, 2020
e17cf3c
revise vtag test
philip-peterson Jun 22, 2020
e32c9d6
vtext test
philip-peterson Jun 22, 2020
d46372e
cleanup warnings
philip-peterson Jun 22, 2020
c7d2615
restore unimplemented!()
philip-peterson Jun 22, 2020
051917c
Add test for refs stringifying
philip-peterson Jun 22, 2020
de7a3b0
cargo fmt
philip-peterson Jun 22, 2020
08bfaa5
add comment
philip-peterson Jun 23, 2020
decfd19
PR feedback
philip-peterson Jun 23, 2020
abb01d7
undo whitespace changes
philip-peterson Jun 23, 2020
772211e
TryFrom
philip-peterson Jun 23, 2020
3291ec1
cargo fmt
philip-peterson Jun 23, 2020
0f3b7f0
fix import
philip-peterson Jun 23, 2020
961a507
fix tests
philip-peterson Jun 23, 2020
7373898
cargo fmt
philip-peterson Jun 23, 2020
98a2b83
return errors instead of panicking
philip-peterson Jun 23, 2020
19c7f58
htmlstringifyerror -> htmlrendererror
philip-peterson Jun 26, 2020
cea6a7b
PR feedback
philip-peterson Jun 26, 2020
aad03df
push_str
philip-peterson Jun 26, 2020
2da0666
cargo fmt
philip-peterson Jun 26, 2020
9deee41
add tag validation
philip-peterson Jun 27, 2020
e289b08
fmt
philip-peterson Jun 27, 2020
2e65887
formatting
philip-peterson Jun 27, 2020
f5f7872
move tag data to other crate
philip-peterson Jun 27, 2020
bd555c9
move out of yewtil
philip-peterson Jun 27, 2020
bd2c5ae
SSR -> SMR
philip-peterson Jun 27, 2020
931550b
format
philip-peterson Jun 27, 2020
bd4935a
refactor
philip-peterson Jun 27, 2020
1ba0c88
move lambda
philip-peterson Jun 27, 2020
ee9d8c9
fix
philip-peterson Jun 27, 2020
e9ce780
use once_cell
philip-peterson Jun 27, 2020
cbff3a6
cargo fmt
philip-peterson Jun 27, 2020
6fbce93
use standard library for alphanum
philip-peterson Jun 27, 2020
ee0e1f4
util for is_valid_html_attribute_name
philip-peterson Jun 27, 2020
cd171d6
use attribute validator
philip-peterson Jun 27, 2020
c4175e1
fix formatting
philip-peterson Jun 27, 2020
1f7c915
Add missing dependencies to `yew_stdweb`.
teymour-aldridge Jun 27, 2020
f159803
iff -> when
philip-peterson Jun 27, 2020
ce8523c
add some tests
philip-peterson Jun 27, 2020
5f78a32
use lazy_static
philip-peterson Jun 27, 2020
459aa6d
Merge pull request #2 from teymour-aldridge/pull/1344
philip-peterson Jun 27, 2020
998403b
Merge branch 'peterson/stringify_node_2' of github.com:philip-peterso…
philip-peterson Jun 27, 2020
fb275b4
:/ just realized yew-stdweb doesnt need these, rolling back
philip-peterson Jun 27, 2020
53e23c8
fix tests
philip-peterson Jun 27, 2020
5734cf7
tests were not testing the things their names indicated
philip-peterson Jun 27, 2020
3ccf96e
nit: test naming
philip-peterson Jun 27, 2020
68fac3d
Update yew/src/virtual_dom/mod.rs
philip-peterson Jun 27, 2020
959f36b
Use unwrap_or_else
philip-peterson Jun 28, 2020
98c867a
Use {0}
philip-peterson Jun 28, 2020
324a23a
no checked=true
philip-peterson Jun 28, 2020
bd0091a
Merge branch 'peterson/stringify_node_2' of github.com:philip-peterso…
philip-peterson Jun 28, 2020
1b2602b
use {0}
philip-peterson Jun 28, 2020
a91b402
use & instead of as_ref
philip-peterson Jun 28, 2020
c53419d
fix error
philip-peterson Jun 28, 2020
12e28e6
html.string -> html.html
philip-peterson Jun 28, 2020
e2f1e5d
move all SMR impls to smr.rs
philip-peterson Jun 28, 2020
dc0b2df
Move all vdom smr impls into smr
philip-peterson Jun 28, 2020
32c6032
move vtext tests
philip-peterson Jun 28, 2020
131b60a
move tests to smr.rs
philip-peterson Jun 28, 2020
52f7511
Documentation tweaks, fix build error
philip-peterson Jun 28, 2020
329c14c
fix test
philip-peterson Jun 28, 2020
99057bf
cargo fmt
philip-peterson Jun 28, 2020
f5bc8b4
add SMR to list of doc test features
philip-peterson Jun 28, 2020
858f621
fix link refs
philip-peterson Jun 28, 2020
7804cff
Fix warning
philip-peterson Jun 28, 2020
34e286a
add SMR to yew-stdweb docs
philip-peterson Jun 28, 2020
d5c3c7b
remove as_ref
philip-peterson Jun 28, 2020
acac47d
Rename `Html` to `HtmlString`.
teymour-aldridge Jun 28, 2020
e5fe5c1
Merge pull request #3 from teymour-aldridge/pull/1344
philip-peterson Jun 28, 2020
5c2a60f
Update yew/src/virtual_dom/smr.rs
philip-peterson Jun 28, 2020
12dd296
Implement Clone for HtmlString
philip-peterson Jun 28, 2020
2d3d032
Better names for tests
philip-peterson Jun 28, 2020
93e90f6
fix tests, warnings, tests not running (D:)
philip-peterson Jun 29, 2020
d21a7db
Remove re-export
philip-peterson Jun 29, 2020
a783503
cargo fmt
philip-peterson Jun 29, 2020
215119c
Merge branch 'peterson/stringify_node_2' of github.com:philip-peterso…
philip-peterson Jun 29, 2020
dbcfe49
Remove lists of tags
philip-peterson Jun 29, 2020
ac5e6f2
fix test
philip-peterson Jun 29, 2020
3de8c2f
add more tests
philip-peterson Jun 29, 2020
7d9e907
allow svg style elements
philip-peterson Jun 29, 2020
b79f55b
formatting
philip-peterson Jul 1, 2020
c7986c9
remove code deemed dead
philip-peterson Jul 1, 2020
b5d87b3
PR feedback
philip-peterson Jul 4, 2020
3358255
add structure for yewtil-validation crate
philip-peterson Jul 4, 2020
e31ac77
update comment
philip-peterson Jul 4, 2020
04dbbad
fix up references to yew_validation
philip-peterson Jul 4, 2020
ffca187
remove yew-validation from this PR
philip-peterson Jul 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ci/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ set -euxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_

(cd yew \
&& cargo test --target wasm32-unknown-unknown --features wasm_test \
&& cargo test --target wasm32-unknown-unknown --features wasm_test,sans_mount_render \
&& cargo test --doc --features doc_test,wasm_test,yaml,msgpack,cbor,toml \
&& cargo test --doc --features doc_test,wasm_test,yaml,msgpack,cbor,toml \
--features std_web,agent,services --no-default-features)
--features std_web,agent,services,sans_mount_render --no-default-features)

(cd yew-functional && cargo test --target wasm32-unknown-unknown)

Expand All @@ -22,4 +23,4 @@ set -euxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_

(cd yewtil && cargo test)

(cd yew-components && cargo test)
(cd yew-components && cargo test)
2 changes: 1 addition & 1 deletion yew-stdweb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ msgpack = ["rmp-serde"]
cbor = ["serde_cbor"]

[package.metadata.docs.rs]
features = ["yaml", "cbor", "toml", "msgpack", "doc_test"]
features = ["yaml", "cbor", "toml", "msgpack", "doc_test", "sans_mount_render"]

[workspace]
members = [
Expand Down
4 changes: 3 additions & 1 deletion yew/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ console_error_panic_hook = { version = "0.1", optional = true }
futures = { version = "0.3", optional = true }
gloo = { version = "0.2.1", optional = true }
http = "0.2"
htmlescape = { version = "0.3.1", optional = true }
indexmap = "1.0.2"
js-sys = { version = "0.3", optional = true }
log = "0.4"
Expand Down Expand Up @@ -133,10 +134,11 @@ web_sys = ["console_error_panic_hook", "futures", "gloo", "js-sys", "web-sys", "
doc_test = []
wasm_test = []
services = []
sans_mount_render = ["htmlescape"]
agent = ["bincode"]
yaml = ["serde_yaml"]
msgpack = ["rmp-serde"]
cbor = ["serde_cbor"]

[package.metadata.docs.rs]
features = ["yaml", "cbor", "toml", "msgpack", "doc_test"]
features = ["yaml", "cbor", "toml", "msgpack", "doc_test", "sans_mount_render"]
3 changes: 3 additions & 0 deletions yew/src/virtual_dom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub mod vtag;
#[doc(hidden)]
pub mod vtext;

#[cfg(feature = "sans_mount_render")]
pub mod smr;

use crate::html::{AnyScope, NodeRef};
use cfg_if::cfg_if;
use indexmap::set::IndexSet;
Expand Down
319 changes: 319 additions & 0 deletions yew/src/virtual_dom/smr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
//! This module contains Yew's implementation of Sans-Mount Rendering (SMR), to support
//! future feature work such as Static Site Generation and Server-Side Rendering (SSR).
//! This functionality allows Yew Components to be rendered to a string without needing
//! to be mounted onto a DOM node first.
//!
//! *This module is only available if the `sans_mount_render` feature is enabled.*

use super::{VComp, VList, VNode, VTag, VText};
use htmlescape;
use std::convert::TryFrom;
use std::fmt::{self, Display, Formatter};
use thiserror::Error as ThisError;

/// Represents a block of HTML string content generated via Sans-Mount Rendering
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct HtmlString(String);

impl HtmlString {
fn new(html: String) -> Self {
Self(html)
}
}

impl Display for HtmlString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

/// Represents errors associated with conversion of Yew structures to HTML.
#[derive(Debug, ThisError)]
pub enum HtmlRenderError {
/// Malformed/unserializable attribute name
#[error("cannot serialize invalid attribute name `{0}`")]
InvalidAttributeName(String),

/// Malformed/unserializable tag name
#[error("cannot serialize invalid tag name `{0}`")]
InvalidTagName(String),

/// Unsupported VRef serialization
#[error("cannot serialize VRef because that is unsupported")]
UnserializableVRef,
}

impl TryFrom<VComp> for HtmlString {
type Error = HtmlRenderError;

fn try_from(value: VComp) -> Result<HtmlString, HtmlRenderError> {
let html: String = match &value.scope {
None => "".to_string(),
Some(scope) => match scope.root_vnode() {
None => "".to_string(),
Some(root_vnode) => HtmlString::try_from(root_vnode.clone())?.to_string(),
},
};
Ok(HtmlString::new(html))
}
}

/// HTML output for a VTag is not necessarily deterministic due to the
/// serialization of props which do not have a particular ordering.
impl TryFrom<VTag> for HtmlString {
type Error = HtmlRenderError;

fn try_from(value: VTag) -> Result<HtmlString, HtmlRenderError> {
let mut result = "".to_string();
let tag_name = htmlescape::encode_minimal(&value.tag).to_lowercase();

if !is_valid_sgml_tag(&tag_name) {
return Err(HtmlRenderError::InvalidTagName(tag_name));
}

result.push_str(&format!("<{}", tag_name));

for (key_unclean, value) in &value.attributes {
let key = key_unclean.to_lowercase();
// checked, value (special if textarea), disabled, href?, selected,
// kind -> type if input, disallow ref, disallow LISTENER_SET, class

if !is_valid_html_attribute_name(key.as_str()) {
return Err(HtmlRenderError::InvalidAttributeName(key));
}

// textareas' innerHTML properties are specified via the `value` prop which doesn't
// exist in HTML, so we defer this prop's serialization until later in the process.
if tag_name == "textarea" && key == "value" {
continue;
}

result.push_str(&format!(
" {}=\"{}\"",
htmlescape::encode_minimal(&key),
htmlescape::encode_attribute(&value)
));
}

if value.checked {
result.push_str(&" checked")
}

if tag_name == "input" {
if let Some(kind) = &value.kind {
result.push_str(&format!(
" type=\"{}\"",
htmlescape::encode_attribute(&kind)
));
}
}

let children_html = match tag_name.as_ref() {
"textarea" => {
let vtext = VText::new(value.value.clone().unwrap_or_else(String::new));
HtmlString::try_from(vtext)
}
_ => HtmlString::try_from(value.children),
}?.to_string();

if children_html == "" {
result.push_str(&" />");
} else {
result.push_str(&">");
result.push_str(&children_html);
result.push_str(&format!("</{}>", tag_name));
}

result.shrink_to_fit();
Ok(HtmlString::new(result))
}
}

impl TryFrom<VText> for HtmlString {
type Error = HtmlRenderError;

fn try_from(value: VText) -> Result<HtmlString, HtmlRenderError> {
Ok(HtmlString::new(htmlescape::encode_minimal(&value.text)))
}
}

impl TryFrom<VList> for HtmlString {
type Error = HtmlRenderError;

fn try_from(value: VList) -> Result<HtmlString, HtmlRenderError> {
let mut result = "".to_string();
for child in value.children {
let html = HtmlString::try_from(child)?.to_string();
result.push_str(&html);
}

result.shrink_to_fit();
Ok(HtmlString::new(result))
}
}

impl TryFrom<VNode> for HtmlString {
type Error = HtmlRenderError;

fn try_from(value: VNode) -> Result<HtmlString, HtmlRenderError> {
Ok(match value {
VNode::VTag(vtag) => HtmlString::try_from(*vtag)?,
VNode::VText(vtext) => HtmlString::try_from(vtext)?,
VNode::VComp(vcomp) => HtmlString::try_from(vcomp)?,
VNode::VList(vlist) => HtmlString::try_from(vlist)?,
VNode::VRef(_) => Err(HtmlRenderError::UnserializableVRef)?,
})
}
}

#[cfg(test)]
mod tests_vtext {
use super::HtmlString;
use crate::html;
use std::convert::TryFrom;

#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};

#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);

#[test]
fn text_as_root_smr() {
let a = html! {
"Text Node As Root"
};

let b = html! {
{ "Text Node As Root" }
};

assert_eq!(
HtmlString::try_from(a.clone()).expect("HTML stringify error"),
HtmlString::try_from(b.clone()).expect("HTML stringify error")
);
assert!(
HtmlString::try_from(b)
.expect("HTML stringify error")
.to_string()
== "Text Node As Root"
);
}

#[test]
fn special_chars_smr() {
let a = html! {
"some special-chars\"> here!"
};

let b = html! {
{ "some special-chars\"> here!" }
};

assert_eq!(
HtmlString::try_from(a.clone()).expect("HTML stringify error"),
HtmlString::try_from(b.clone()).expect("HTML stringify error")
);
assert_eq!(
HtmlString::try_from(b.clone())
.expect("HTML stringify error")
.to_string(),
"some special-chars&quot;&gt; here!"
);
}
}

#[cfg(test)]
mod tests_vtag {
use super::*;
use crate::html::NodeRef;
use std::convert::TryFrom;

#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};

#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);

#[test]
fn it_stringifies_simple() {
let p = html! {
<p></p>
};

if let VNode::VTag(p) = p {
let p_html = HtmlString::try_from(*p)
.expect("HTML stringify error")
.to_string();

assert_eq!(p_html, "<p />");
} else {
assert!(false);
}
}

#[test]
fn it_stringifies_complex() {
let other_sym = "bar";
let div = html! {
<div class=("foo", other_sym)>
{ "quux" }
</div>
};
let p = html! {
<p aria-controls="it-works">
{ "test" }
{div}
</p>
};

if let VNode::VTag(p) = p {
let p_html = HtmlString::try_from(*p)
.expect("HTML stringify error")
.to_string();

assert_eq!(
p_html,
"<p aria-controls=\"it&#x2D;works\">test<div class=\"foo&#x20;bar\">quux</div></p>"
);
} else {
assert!(false);
}
}

#[test]
fn it_stringifies_attrs() {
let div = html! {
<div a="b" b="a" />
};

if let VNode::VTag(div) = div {
let div_html = HtmlString::try_from(*div)
.expect("HTML stringify error")
.to_string();
let order_1 = "<div a=\"b\" b=\"a\" />";
let order_2 = "<div b=\"a\" a=\"b\" />";
assert!(div_html == order_1 || div_html == order_2);
} else {
assert!(false);
}
}

#[test]
fn it_does_not_stringify_special_attrs() {
let node_ref = NodeRef::default();

let div = html! {
<div ref=node_ref />
};

if let VNode::VTag(div) = div {
let div_html = HtmlString::try_from(*div)
.expect("HTML stringify error")
.to_string();
assert_eq!(div_html, "<div />");
} else {
assert!(false);
}
}
}
2 changes: 1 addition & 1 deletion yew/src/virtual_dom/vcomp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cfg_if! {
/// A virtual component.
pub struct VComp {
type_id: TypeId,
scope: Option<Box<dyn Scoped>>,
pub(crate) scope: Option<Box<dyn Scoped>>,
props: Option<Box<dyn Mountable>>,
pub(crate) node_ref: NodeRef,
pub(crate) key: Option<String>,
Expand Down
2 changes: 1 addition & 1 deletion yew/src/virtual_dom/vtag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl ElementType {
#[derive(Debug)]
pub struct VTag {
/// A tag of the element.
tag: Cow<'static, str>,
pub(crate) tag: Cow<'static, str>,
/// Type of element.
element_type: ElementType,
/// A reference to the `Element`.
Expand Down