use std::fmt;
use crate::engine::{encode_loop, is_text_direction_control};
#[derive(Clone, Copy)]
struct JsConfig {
hex_quotes: bool,
encode_ampersand: bool,
script_data: bool,
}
const JS_UNIVERSAL: JsConfig = JsConfig {
hex_quotes: true,
encode_ampersand: true,
script_data: true,
};
const JS_ATTRIBUTE: JsConfig = JsConfig {
hex_quotes: true,
encode_ampersand: true,
script_data: false,
};
const JS_BLOCK: JsConfig = JsConfig {
hex_quotes: false,
encode_ampersand: true,
script_data: true,
};
const JS_SOURCE: JsConfig = JsConfig {
hex_quotes: false,
encode_ampersand: false,
script_data: 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}'
) || is_text_direction_control(c)
}
fn write_js_template_encoded<W: fmt::Write>(
out: &mut W,
c: char,
next: Option<char>,
) -> fmt::Result {
match c {
'`' => out.write_str("\\`"),
'$' if matches!(next, Some('{') | None) => out.write_str("\\$"),
'$' => out.write_char('$'),
'&' => out.write_str("\\x26"),
'/' => out.write_str("\\/"),
'<' => out.write_str("\\x3c"),
c => write_js_shared_escape(out, c),
}
}
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.script_data,
c => is_text_direction_control(c),
}
}
fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
match c {
'"' 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("\\x3c"),
c => write_js_shared_escape(out, c),
}
}
fn write_js_shared_escape<W: fmt::Write>(out: &mut W, c: 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("\\\\"),
'\u{2028}' => out.write_str("\\u2028"),
'\u{2029}' => out.write_str("\\u2029"),
'\x00'..='\x1F' => write!(out, "\\x{:02x}", c as u32),
c if is_text_direction_control(c) => write!(out, "\\u{:04x}", c as u32),
c => write!(out, "\\u{{{:x}}}", c as u32),
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEXT_DIRECTION: [(&str, &str); 9] = [
("\u{202A}", r"\u202a"),
("\u{202B}", r"\u202b"),
("\u{202C}", r"\u202c"),
("\u{202D}", r"\u202d"),
("\u{202E}", r"\u202e"),
("\u{2066}", r"\u2066"),
("\u{2067}", r"\u2067"),
("\u{2068}", r"\u2068"),
("\u{2069}", r"\u2069"),
];
#[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("a/b"), r"a\/b");
assert_eq!(for_javascript("</script>"), r"\x3c\/script>");
}
#[test]
fn js_encodes_lt() {
assert_eq!(for_javascript("a<b"), r"a\x3cb");
assert_eq!(for_javascript("<!--<script>"), r"\x3c!--\x3cscript>");
assert_eq!(for_javascript("<!--"), r"\x3c!--");
assert_eq!(for_javascript("<script"), r"\x3cscript");
}
#[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_escapes_text_direction_controls() {
for (raw, escaped) in TEXT_DIRECTION {
assert_eq!(for_javascript(raw), escaped);
assert_eq!(for_javascript(&format!("a{raw}b")), format!("a{escaped}b"));
}
}
#[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_or_lt() {
assert_eq!(for_javascript_attribute("a/b"), "a/b");
assert_eq!(for_javascript_attribute("<!--<script>"), "<!--<script>");
}
#[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_attr_escapes_text_direction_controls() {
for (raw, escaped) in TEXT_DIRECTION {
assert_eq!(for_javascript_attribute(raw), escaped);
}
}
#[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_lt() {
assert_eq!(for_javascript_block("a<b"), r"a\x3cb");
assert_eq!(for_javascript_block("<!--<script>"), r"\x3c!--\x3cscript>");
assert_eq!(for_javascript_block("<!--"), r"\x3c!--");
assert_eq!(for_javascript_block("<script"), r"\x3cscript");
}
#[test]
fn js_block_encodes_ampersand() {
assert_eq!(for_javascript_block("a&b"), r"a\x26b");
}
#[test]
fn js_block_escapes_text_direction_controls() {
for (raw, escaped) in TEXT_DIRECTION {
assert_eq!(for_javascript_block(raw), escaped);
}
}
#[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_ampersand_or_lt() {
assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
assert_eq!(for_javascript_source("<!--<script>"), "<!--<script>");
}
#[test]
fn js_source_encodes_line_separators() {
assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
}
#[test]
fn js_source_escapes_text_direction_controls() {
for (raw, escaped) in TEXT_DIRECTION {
assert_eq!(for_javascript_source(raw), escaped);
assert_eq!(
for_javascript_source(&format!("var x = '{raw}';")),
format!(r"var x = \'{escaped}\';")
);
}
}
#[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");
}
#[test]
fn js_template_escapes_trailing_dollar() {
assert_eq!(for_js_template("a$"), r"a\$");
assert_eq!(for_js_template("$"), r"\$");
}
#[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("a/b"), r"a\/b");
assert_eq!(for_js_template("</script>"), r"\x3c\/script>");
}
#[test]
fn js_template_encodes_lt() {
assert_eq!(for_js_template("a<b"), r"a\x3cb");
assert_eq!(for_js_template("<!--<script>"), r"\x3c!--\x3cscript>");
assert_eq!(for_js_template("<!--"), r"\x3c!--");
assert_eq!(for_js_template("<script"), r"\x3cscript");
}
#[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_escapes_text_direction_controls() {
for (raw, escaped) in TEXT_DIRECTION {
assert_eq!(for_js_template(raw), escaped);
assert_eq!(for_js_template(&format!("a{raw}b")), format!("a{escaped}b"));
}
}
#[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);
}
#[test]
fn shared_escape_handles_shared_chars() {
let cases = [
('\x08', r"\b"),
('\t', r"\t"),
('\n', r"\n"),
('\x0B', r"\x0b"),
('\x0C', r"\f"),
('\r', r"\r"),
('\\', r"\\"),
('\x00', r"\x00"),
('\x1F', r"\x1f"),
('\u{2028}', r"\u2028"),
('\u{2029}', r"\u2029"),
];
for (c, expected) in cases {
let mut out = String::new();
assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
assert_eq!(out, expected);
}
}
#[test]
fn shared_escape_never_drops_a_character() {
for (c, expected) in [('a', r"\u{61}"), ('"', r"\u{22}"), ('é', r"\u{e9}")] {
let mut out = String::new();
assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
assert_eq!(out, expected);
}
}
}