Skip to content

Commit

Permalink
add raw and raw_unsafe escape hatches
Browse files Browse the repository at this point in the history
  • Loading branch information
jcornaz committed Oct 31, 2024
1 parent c6dddaf commit cb3762b
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 12 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Escape HTML strings
* `id` and `class` attributes
* Nodes
* Html document (`html`, `head`, `body`)
* Html document (`html`, `head` and `body`)
* Meta (`title`)
* Text (`h1`, `h2`, `h3`, `h4`, `h5`, `h6` and `text`)
* Container (`div`)
* Escape hatches (`raw and `raw_unsafe`)
* `Node`, `Attribute` and `Document` types

[Unreleased]: https://github.com/jcornaz/fun-html/compare/...HEAD
8 changes: 2 additions & 6 deletions src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ impl<T: Into<Cow<'static, str>>> From<(&'static str, T)> for Attribute {
}
}

pub fn attr(key: &'static str, value: impl Into<Cow<'static, str>>) -> Attribute {
Attribute::new(key, value)
}

pub fn lang(lang: impl Into<Cow<'static, str>>) -> Attribute {
attr("lang", lang)
("lang", lang).into()
}

pub fn id(id: impl Into<Cow<'static, str>>) -> Attribute {
attr("id", id)
("id", id).into()
}

pub fn class<'a>(classes: impl IntoIterator<Item = &'a str>) -> Attribute {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum NodeInner {
children: Vec<Node>,
},
Text(Cow<'static, str>),
Raw(Cow<'static, str>),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -100,6 +101,7 @@ impl Display for Node {
write!(f, "</{tag}>")?;
}
NodeInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?,
NodeInner::Raw(raw) => write!(f, "{raw}")?,
}
Ok(())
}
Expand Down
20 changes: 20 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,30 @@ pub fn h6(
Node::new("h6", attributes, children)
}

/// HTML escaped text
pub fn text(value: impl Into<Cow<'static, str>>) -> Node {
NodeInner::Text(value.into()).into()
}

/// Inline raw HTML without escaping
///
/// This function is considered safe because the HTML being inlined must be known at compile time
///
/// See [`raw_unsafe`] to inline HTML that is generated at runtime
pub fn raw(html: &'static str) -> Node {
NodeInner::Raw(html.into()).into()
}

/// Inline raw HTML without escaping
///
/// This function **IS NOT SAFE** and should be avoided unless really necessary.
/// Miss-use can lead to XSS vulnerability.
///
/// See [`raw`] to safely inline HTML that is known at compile time
pub fn raw_unsafe(html: String) -> Node {
NodeInner::Raw(html.into()).into()
}

impl<T: Into<Cow<'static, str>>> From<T> for Node {
fn from(value: T) -> Self {
text(value)
Expand Down
12 changes: 7 additions & 5 deletions tests/render_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn should_render_html_document() {
#[test]
fn should_render_attributes() {
let node = h1(
[class(["underlined", "blue"]), attr("foo", "bar")],
[class(["underlined", "blue"]), ("foo", "bar").into()],
[text("Hello world!")],
);
assert_eq!(
Expand All @@ -41,10 +41,12 @@ fn should_render_attributes() {
#[case(text("hello".to_string()), "hello")]
#[case("hello".into(), "hello")]
#[case("hello".to_string().into(), "hello")]
#[case(div([attr("foo", "bar")], ["hello".into()]), "<div foo=\"bar\">hello</div>")]
#[case(div([attr("foo", "bar".to_string())], [text("hello".to_string())]), "<div foo=\"bar\">hello</div>")]
#[case(raw("<my-component></my-component>"), "<my-component></my-component>")]
#[case(raw_unsafe("<my-component></my-component>".to_string()), "<my-component></my-component>")]
#[case(div([("foo", "bar").into()], ["hello".into()]), "<div foo=\"bar\">hello</div>")]
#[case(div([("foo", "bar".to_string()).into()], [text("hello".to_string())]), "<div foo=\"bar\">hello</div>")]
#[case(head([id("foo")], [text("hello")]), "<head id=\"foo\">hello</head>")]
#[case(title([attr("foo", "bar")], [text("hello")]), "<title foo=\"bar\">hello</title>")]
#[case(title([("foo", "bar").into()], [text("hello")]), "<title foo=\"bar\">hello</title>")]
#[case(body([id("foo")], [text("hello")]), "<body id=\"foo\">hello</body>")]
#[case(h1([id("foo")], [text("hello")]), "<h1 id=\"foo\">hello</h1>")]
#[case(h2([id("foo")], [text("hello")]), "<h2 id=\"foo\">hello</h2>")]
Expand All @@ -66,7 +68,7 @@ fn text_should_be_escaped() {
#[rstest]
fn attribute_should_be_escaped() {
let string = div(
[attr("foo", "<script>\"\" { open: !close }")],
[("foo", "<script>\"\" { open: !close }").into()],
[text("hello")],
)
.to_string();
Expand Down

0 comments on commit cb3762b

Please sign in to comment.