use std::fmt;
use crate::engine::encode_loop;
#[derive(Clone, Copy)]
struct JsConfig {
hex_quotes: bool,
encode_ampersand: bool,
encode_slash: bool,
}
const JS_UNIVERSAL: JsConfig = JsConfig {
hex_quotes: true,
encode_ampersand: true,
encode_slash: true,
};
const JS_ATTRIBUTE: JsConfig = JsConfig {
hex_quotes: true,
encode_ampersand: true,
encode_slash: false,
};
const JS_BLOCK: JsConfig = JsConfig {
hex_quotes: false,
encode_ampersand: true,
encode_slash: true,
};
const JS_SOURCE: JsConfig = JsConfig {
hex_quotes: false,
encode_ampersand: false,
encode_slash: false,
};
pub fn for_javascript(input: &str) -> String {
encode_js(input, &JS_UNIVERSAL)
}
pub fn write_javascript<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
write_js(out, input, &JS_UNIVERSAL)
}
pub fn for_javascript_attribute(input: &str) -> String {
encode_js(input, &JS_ATTRIBUTE)
}
pub fn write_javascript_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
write_js(out, input, &JS_ATTRIBUTE)
}
pub fn for_javascript_block(input: &str) -> String {
encode_js(input, &JS_BLOCK)
}
pub fn write_javascript_block<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
write_js(out, input, &JS_BLOCK)
}
pub fn for_javascript_source(input: &str) -> String {
encode_js(input, &JS_SOURCE)
}
pub fn write_javascript_source<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
write_js(out, input, &JS_SOURCE)
}
pub fn for_js_template(input: &str) -> String {
let mut out = String::with_capacity(input.len());
write_js_template(&mut out, input).expect("writing to string cannot fail");
out
}
pub fn write_js_template<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
encode_loop(
out,
input,
needs_js_template_encoding,
write_js_template_encoded,
)
}
fn needs_js_template_encoding(c: char) -> bool {
matches!(
c,
'\x00'..='\x1F' | '\\' | '`' | '$' | '/' | '\u{2028}' | '\u{2029}'
)
}
fn write_js_template_encoded<W: fmt::Write>(
out: &mut W,
c: char,
next: Option<char>,
) -> fmt::Result {
match c {
'\x08' => out.write_str("\\b"),
'\t' => out.write_str("\\t"),
'\n' => out.write_str("\\n"),
'\x0B' => out.write_str("\\x0b"),
'\x0C' => out.write_str("\\f"),
'\r' => out.write_str("\\r"),
'`' => out.write_str("\\`"),
'$' if next == Some('{') => out.write_str("\\$"),
'$' => out.write_char('$'),
'/' => out.write_str("\\/"),
'\\' => out.write_str("\\\\"),
'\u{2028}' => out.write_str("\\u2028"),
'\u{2029}' => out.write_str("\\u2029"),
c => write!(out, "\\x{:02x}", c as u32),
}
}
fn encode_js(input: &str, config: &JsConfig) -> String {
let mut out = String::with_capacity(input.len());
write_js(&mut out, input, config).expect("writing to string cannot fail");
out
}
fn write_js<W: fmt::Write>(out: &mut W, input: &str, config: &JsConfig) -> fmt::Result {
encode_loop(
out,
input,
|c| needs_js_encoding(c, config),
|out, c, _next| write_js_encoded(out, c, config),
)
}
fn needs_js_encoding(c: char, config: &JsConfig) -> bool {
match c {
'\x00'..='\x1F' | '\\' | '"' | '\'' | '\u{2028}' | '\u{2029}' => true,
'&' => config.encode_ampersand,
'/' => config.encode_slash,
_ => false,
}
}
fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
match c {
'\x08' => out.write_str("\\b"),
'\t' => out.write_str("\\t"),
'\n' => out.write_str("\\n"),
'\x0B' => out.write_str("\\x0b"),
'\x0C' => out.write_str("\\f"),
'\r' => out.write_str("\\r"),
'"' if config.hex_quotes => out.write_str("\\x22"),
'"' => out.write_str("\\\""),
'\'' if config.hex_quotes => out.write_str("\\x27"),
'\'' => out.write_str("\\'"),
'&' => out.write_str("\\x26"),
'/' => out.write_str("\\/"),
'\\' => out.write_str("\\\\"),
'\u{2028}' => out.write_str("\\u2028"),
'\u{2029}' => out.write_str("\\u2029"),
c => write!(out, "\\x{:02x}", c as u32),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn js_no_encoding_needed() {
assert_eq!(for_javascript("hello world"), "hello world");
assert_eq!(for_javascript(""), "");
}
#[test]
fn js_encodes_quotes_as_hex() {
assert_eq!(for_javascript(r#"a"b"#), r"a\x22b");
assert_eq!(for_javascript("a'b"), r"a\x27b");
}
#[test]
fn js_encodes_backslash() {
assert_eq!(for_javascript(r"a\b"), r"a\\b");
}
#[test]
fn js_encodes_ampersand() {
assert_eq!(for_javascript("a&b"), r"a\x26b");
}
#[test]
fn js_encodes_slash() {
assert_eq!(for_javascript("</script>"), r"<\/script>");
}
#[test]
fn js_encodes_control_chars() {
assert_eq!(for_javascript("\x00"), r"\x00");
assert_eq!(for_javascript("\x08"), r"\b");
assert_eq!(for_javascript("\t"), r"\t");
assert_eq!(for_javascript("\n"), r"\n");
assert_eq!(for_javascript("\x0B"), r"\x0b");
assert_eq!(for_javascript("\x0C"), r"\f");
assert_eq!(for_javascript("\r"), r"\r");
assert_eq!(for_javascript("\x1F"), r"\x1f");
}
#[test]
fn js_encodes_line_separators() {
assert_eq!(for_javascript("\u{2028}"), r"\u2028");
assert_eq!(for_javascript("\u{2029}"), r"\u2029");
}
#[test]
fn js_preserves_non_ascii() {
assert_eq!(for_javascript("café"), "café");
assert_eq!(for_javascript("日本語"), "日本語");
}
#[test]
fn js_writer_variant() {
let mut out = String::new();
write_javascript(&mut out, "a'b").unwrap();
assert_eq!(out, r"a\x27b");
}
#[test]
fn js_attr_does_not_encode_slash() {
assert_eq!(for_javascript_attribute("a/b"), "a/b");
}
#[test]
fn js_attr_encodes_quotes_as_hex() {
assert_eq!(for_javascript_attribute("a'b"), r"a\x27b");
}
#[test]
fn js_attr_encodes_ampersand() {
assert_eq!(for_javascript_attribute("a&b"), r"a\x26b");
}
#[test]
fn js_block_uses_backslash_quotes() {
assert_eq!(for_javascript_block(r#"a"b"#), r#"a\"b"#);
assert_eq!(for_javascript_block("a'b"), r"a\'b");
}
#[test]
fn js_block_encodes_slash() {
assert_eq!(for_javascript_block("a/b"), r"a\/b");
}
#[test]
fn js_block_encodes_ampersand() {
assert_eq!(for_javascript_block("a&b"), r"a\x26b");
}
#[test]
fn js_source_uses_backslash_quotes() {
assert_eq!(for_javascript_source(r#"a"b"#), r#"a\"b"#);
assert_eq!(for_javascript_source("a'b"), r"a\'b");
}
#[test]
fn js_source_does_not_encode_slash_or_ampersand() {
assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
}
#[test]
fn js_source_encodes_line_separators() {
assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
}
#[test]
fn js_template_no_encoding_needed() {
assert_eq!(for_js_template("hello world"), "hello world");
assert_eq!(for_js_template(""), "");
}
#[test]
fn js_template_encodes_backtick() {
assert_eq!(for_js_template("hello `world`"), r"hello \`world\`");
assert_eq!(for_js_template("`"), r"\`");
}
#[test]
fn js_template_encodes_interpolation() {
assert_eq!(for_js_template("${alert(1)}"), r"\${alert(1)}");
assert_eq!(for_js_template("a${b}c"), r"a\${b}c");
assert_eq!(for_js_template("${a}${b}"), r"\${a}\${b}");
}
#[test]
fn js_template_dollar_without_brace_passes_through() {
assert_eq!(for_js_template("a $ b"), "a $ b");
assert_eq!(for_js_template("$100"), "$100");
assert_eq!(for_js_template("a$"), "a$");
}
#[test]
fn js_template_encodes_backslash() {
assert_eq!(for_js_template(r"a\b"), r"a\\b");
}
#[test]
fn js_template_encodes_slash() {
assert_eq!(for_js_template("</script>"), r"<\/script>");
}
#[test]
fn js_template_does_not_encode_quotes() {
assert_eq!(for_js_template(r#"a"b"#), r#"a"b"#);
assert_eq!(for_js_template("a'b"), "a'b");
}
#[test]
fn js_template_encodes_control_chars() {
assert_eq!(for_js_template("\x00"), r"\x00");
assert_eq!(for_js_template("\x08"), r"\b");
assert_eq!(for_js_template("\t"), r"\t");
assert_eq!(for_js_template("\n"), r"\n");
assert_eq!(for_js_template("\x0B"), r"\x0b");
assert_eq!(for_js_template("\x0C"), r"\f");
assert_eq!(for_js_template("\r"), r"\r");
assert_eq!(for_js_template("\x1F"), r"\x1f");
}
#[test]
fn js_template_encodes_line_separators() {
assert_eq!(for_js_template("\u{2028}"), r"\u2028");
assert_eq!(for_js_template("\u{2029}"), r"\u2029");
}
#[test]
fn js_template_preserves_non_ascii() {
assert_eq!(for_js_template("café"), "café");
assert_eq!(for_js_template("日本語"), "日本語");
assert_eq!(for_js_template("😀"), "😀");
}
#[test]
fn js_template_mixed_input() {
assert_eq!(
for_js_template("`Hello ${name}`, welcome\\n"),
r"\`Hello \${name}\`, welcome\\n"
);
}
#[test]
fn js_template_writer_variant() {
let input = "`test` ${x} café";
let string_result = for_js_template(input);
let mut writer_result = String::new();
write_js_template(&mut writer_result, input).unwrap();
assert_eq!(string_result, writer_result);
}
}