mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Fix incorrect text escaping during SSR (#3381)
Fixes: #3129 --- * (#3129) fixed incorrect text escaping during SSR * fixed formatting issues * moved SpecialVTagKind to feat_ssr, improved its description * added a test case for multiple text nodes in a style tag * fixed formatting * SpecialVTagKind -> VTagKind
This commit is contained in:
parent
b28e69a574
commit
afde963230
@ -345,15 +345,7 @@ impl TryFrom<Field> for PropField {
|
||||
|
||||
impl PartialOrd for PropField {
|
||||
fn partial_cmp(&self, other: &PropField) -> Option<Ordering> {
|
||||
if self.name == other.name {
|
||||
Some(Ordering::Equal)
|
||||
} else if self.name == "children" {
|
||||
Some(Ordering::Greater)
|
||||
} else if other.name == "children" {
|
||||
Some(Ordering::Less)
|
||||
} else {
|
||||
self.name.partial_cmp(&other.name)
|
||||
}
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -294,6 +294,7 @@ mod feat_ssr {
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::component::lifecycle::{
|
||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
||||
};
|
||||
@ -308,6 +309,7 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
props: Rc<COMP::Properties>,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
// Rust's Future implementation is stack-allocated and incurs zero runtime-cost.
|
||||
//
|
||||
@ -340,7 +342,7 @@ mod feat_ssr {
|
||||
let html = rx.await.unwrap();
|
||||
|
||||
let self_any_scope = AnyScope::from(self.clone());
|
||||
html.render_into_stream(w, &self_any_scope, hydratable)
|
||||
html.render_into_stream(w, &self_any_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
|
||||
if let Some(prepared_state) = self.get_component().unwrap().prepare_state() {
|
||||
|
||||
@ -9,6 +9,37 @@ use crate::html::{BaseComponent, Scope};
|
||||
use crate::platform::fmt::BufStream;
|
||||
use crate::platform::{LocalHandle, Runtime};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) mod feat_ssr {
|
||||
/// Passed top-down as context for `render_into_stream` functions to know the current innermost
|
||||
/// `VTag` kind to apply appropriate text escaping.
|
||||
/// Right now this is used to make `VText` nodes aware of their environment and correctly
|
||||
/// escape their contents when rendering them during SSR.
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub(crate) enum VTagKind {
|
||||
/// <style> tag
|
||||
Style,
|
||||
/// <script> tag
|
||||
Script,
|
||||
#[default]
|
||||
/// any other tag
|
||||
Other,
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for VTagKind {
|
||||
fn from(value: T) -> Self {
|
||||
let value = value.as_ref();
|
||||
if value.eq_ignore_ascii_case("style") {
|
||||
Self::Style
|
||||
} else if value.eq_ignore_ascii_case("script") {
|
||||
Self::Script
|
||||
} else {
|
||||
Self::Other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Yew Server-side Renderer that renders on the current thread.
|
||||
///
|
||||
/// # Note
|
||||
@ -99,7 +130,12 @@ where
|
||||
let render_span = tracing::debug_span!("render_stream_item");
|
||||
render_span.follows_from(outer_span);
|
||||
scope
|
||||
.render_into_stream(&mut w, self.props.into(), self.hydratable)
|
||||
.render_into_stream(
|
||||
&mut w,
|
||||
self.props.into(),
|
||||
self.hydratable,
|
||||
Default::default(),
|
||||
)
|
||||
.instrument(render_span)
|
||||
.await;
|
||||
})
|
||||
|
||||
@ -20,7 +20,7 @@ use crate::html::Scoped;
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
use crate::html::{AnyScope, Scope};
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::platform::fmt::BufWriter;
|
||||
use crate::{feat_ssr::VTagKind, platform::fmt::BufWriter};
|
||||
|
||||
/// A virtual component.
|
||||
pub struct VComp {
|
||||
@ -77,6 +77,7 @@ pub(crate) trait Mountable {
|
||||
w: &'a mut BufWriter,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
@ -146,12 +147,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
w: &'a mut BufWriter,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
|
||||
async move {
|
||||
scope
|
||||
.render_into_stream(w, self.props.clone(), hydratable)
|
||||
.render_into_stream(w, self.props.clone(), hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
}
|
||||
.boxed_local()
|
||||
@ -262,10 +264,11 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
self.mountable
|
||||
.as_ref()
|
||||
.render_into_stream(w, parent_scope, hydratable)
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +182,7 @@ mod feat_ssr {
|
||||
use futures::{join, pin_mut, poll, FutureExt};
|
||||
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::AnyScope;
|
||||
use crate::platform::fmt::{self, BufWriter};
|
||||
|
||||
@ -191,11 +192,14 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
match &self[..] {
|
||||
[] => {}
|
||||
[child] => {
|
||||
child.render_into_stream(w, parent_scope, hydratable).await;
|
||||
child
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
}
|
||||
_ => {
|
||||
async fn render_child_iter<'a, I>(
|
||||
@ -203,6 +207,7 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) where
|
||||
I: Iterator<Item = &'a VNode>,
|
||||
{
|
||||
@ -215,7 +220,8 @@ mod feat_ssr {
|
||||
//
|
||||
// We capture and return the mutable reference to avoid this.
|
||||
|
||||
m.render_into_stream(w, parent_scope, hydratable).await;
|
||||
m.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
w
|
||||
};
|
||||
pin_mut!(child_fur);
|
||||
@ -231,6 +237,7 @@ mod feat_ssr {
|
||||
&mut next_w,
|
||||
parent_scope,
|
||||
hydratable,
|
||||
parent_vtag_kind,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -257,7 +264,8 @@ mod feat_ssr {
|
||||
}
|
||||
|
||||
let children = self.iter();
|
||||
render_child_iter(children, w, parent_scope, hydratable).await;
|
||||
render_child_iter(children, w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,6 +194,7 @@ mod feat_ssr {
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::AnyScope;
|
||||
use crate::platform::fmt::BufWriter;
|
||||
|
||||
@ -203,23 +204,31 @@ mod feat_ssr {
|
||||
w: &'a mut BufWriter,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async fn render_into_stream_(
|
||||
this: &VNode,
|
||||
w: &mut BufWriter,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
match this {
|
||||
VNode::VTag(vtag) => vtag.render_into_stream(w, parent_scope, hydratable).await,
|
||||
VNode::VText(vtext) => {
|
||||
vtext.render_into_stream(w, parent_scope, hydratable).await
|
||||
vtext
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await
|
||||
}
|
||||
VNode::VComp(vcomp) => {
|
||||
vcomp.render_into_stream(w, parent_scope, hydratable).await
|
||||
vcomp
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await
|
||||
}
|
||||
VNode::VList(vlist) => {
|
||||
vlist.render_into_stream(w, parent_scope, hydratable).await
|
||||
vlist
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await
|
||||
}
|
||||
// We are pretty safe here as it's not possible to get a web_sys::Node without
|
||||
// DOM support in the first place.
|
||||
@ -233,7 +242,7 @@ mod feat_ssr {
|
||||
VNode::VPortal(_) => {}
|
||||
VNode::VSuspense(vsuspense) => {
|
||||
vsuspense
|
||||
.render_into_stream(w, parent_scope, hydratable)
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -241,8 +250,9 @@ mod feat_ssr {
|
||||
}
|
||||
}
|
||||
|
||||
async move { render_into_stream_(self, w, parent_scope, hydratable).await }
|
||||
.boxed_local()
|
||||
async move {
|
||||
render_into_stream_(self, w, parent_scope, hydratable, parent_vtag_kind).await
|
||||
}.boxed_local()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ impl VSuspense {
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::AnyScope;
|
||||
use crate::platform::fmt::BufWriter;
|
||||
use crate::virtual_dom::Collectable;
|
||||
@ -37,6 +38,7 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
let collectable = Collectable::Suspense;
|
||||
|
||||
@ -46,7 +48,7 @@ mod feat_ssr {
|
||||
|
||||
// always render children on the server side.
|
||||
self.children
|
||||
.render_into_stream(w, parent_scope, hydratable)
|
||||
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
|
||||
.await;
|
||||
|
||||
if hydratable {
|
||||
|
||||
@ -451,6 +451,7 @@ mod feat_ssr {
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::AnyScope;
|
||||
use crate::platform::fmt::BufWriter;
|
||||
use crate::virtual_dom::VText;
|
||||
@ -505,7 +506,7 @@ mod feat_ssr {
|
||||
VTagInner::Textarea { .. } => {
|
||||
if let Some(m) = self.value() {
|
||||
VText::new(m.to_owned())
|
||||
.render_into_stream(w, parent_scope, hydratable)
|
||||
.render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
|
||||
.await;
|
||||
}
|
||||
|
||||
@ -518,7 +519,7 @@ mod feat_ssr {
|
||||
} => {
|
||||
if !VOID_ELEMENTS.contains(&tag.as_ref()) {
|
||||
children
|
||||
.render_into_stream(w, parent_scope, hydratable)
|
||||
.render_into_stream(w, parent_scope, hydratable, tag.into())
|
||||
.await;
|
||||
|
||||
let _ = w.write_str("</");
|
||||
@ -623,4 +624,59 @@ mod ssr_tests {
|
||||
|
||||
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_escaping_in_style_tag() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <style>{"body > a {color: #cc0;}"}</style> }
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_escaping_in_script_tag() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { <script>{"foo.bar = x < y;"}</script> }
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn test_multiple_vtext_in_style_tag() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
let one = "html { background: black } ";
|
||||
let two = "body > a { color: white } ";
|
||||
html! {
|
||||
<style>
|
||||
{one}
|
||||
{two}
|
||||
</style>
|
||||
}
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
r#"<style>html { background: black } body > a { color: white } </style>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ mod feat_ssr {
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::feat_ssr::VTagKind;
|
||||
use crate::html::AnyScope;
|
||||
use crate::platform::fmt::BufWriter;
|
||||
|
||||
@ -53,9 +54,13 @@ mod feat_ssr {
|
||||
w: &mut BufWriter,
|
||||
_parent_scope: &AnyScope,
|
||||
_hydratable: bool,
|
||||
parent_vtag_kind: VTagKind,
|
||||
) {
|
||||
let s = html_escape::encode_text(&self.text);
|
||||
let _ = w.write_str(&s);
|
||||
_ = w.write_str(&match parent_vtag_kind {
|
||||
VTagKind::Style => html_escape::encode_style(&self.text),
|
||||
VTagKind::Script => html_escape::encode_script(&self.text),
|
||||
VTagKind::Other => html_escape::encode_text(&self.text),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user