1use std::fmt;
28
29use crate::engine::encode_loop;
30
31#[derive(Clone, Copy)]
33struct JsConfig {
34 hex_quotes: bool,
37 encode_ampersand: bool,
39 encode_slash: bool,
41}
42
43const JS_UNIVERSAL: JsConfig = JsConfig {
44 hex_quotes: true,
45 encode_ampersand: true,
46 encode_slash: true,
47};
48
49const JS_ATTRIBUTE: JsConfig = JsConfig {
50 hex_quotes: true,
51 encode_ampersand: true,
52 encode_slash: false,
53};
54
55const JS_BLOCK: JsConfig = JsConfig {
56 hex_quotes: false,
57 encode_ampersand: true,
58 encode_slash: true,
59};
60
61const JS_SOURCE: JsConfig = JsConfig {
62 hex_quotes: false,
63 encode_ampersand: false,
64 encode_slash: false,
65};
66
67pub fn for_javascript(input: &str) -> String {
107 encode_js(input, &JS_UNIVERSAL)
108}
109
110pub fn write_javascript<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
114 write_js(out, input, &JS_UNIVERSAL)
115}
116
117pub fn for_javascript_attribute(input: &str) -> String {
135 encode_js(input, &JS_ATTRIBUTE)
136}
137
138pub fn write_javascript_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
142 write_js(out, input, &JS_ATTRIBUTE)
143}
144
145pub fn for_javascript_block(input: &str) -> String {
161 encode_js(input, &JS_BLOCK)
162}
163
164pub fn write_javascript_block<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
168 write_js(out, input, &JS_BLOCK)
169}
170
171pub fn for_javascript_source(input: &str) -> String {
186 encode_js(input, &JS_SOURCE)
187}
188
189pub fn write_javascript_source<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
193 write_js(out, input, &JS_SOURCE)
194}
195
196pub fn for_js_template(input: &str) -> String {
227 let mut out = String::with_capacity(input.len());
228 write_js_template(&mut out, input).expect("writing to string cannot fail");
229 out
230}
231
232pub fn write_js_template<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
236 encode_loop(
237 out,
238 input,
239 needs_js_template_encoding,
240 write_js_template_encoded,
241 )
242}
243
244fn needs_js_template_encoding(c: char) -> bool {
245 matches!(
246 c,
247 '\x00'..='\x1F' | '\\' | '`' | '$' | '/' | '\u{2028}' | '\u{2029}'
248 )
249}
250
251fn write_js_template_encoded<W: fmt::Write>(
252 out: &mut W,
253 c: char,
254 next: Option<char>,
255) -> fmt::Result {
256 match c {
257 '\x08' => out.write_str("\\b"),
258 '\t' => out.write_str("\\t"),
259 '\n' => out.write_str("\\n"),
260 '\x0B' => out.write_str("\\x0b"),
261 '\x0C' => out.write_str("\\f"),
262 '\r' => out.write_str("\\r"),
263 '`' => out.write_str("\\`"),
264 '$' if next == Some('{') => out.write_str("\\$"),
265 '$' => out.write_char('$'),
266 '/' => out.write_str("\\/"),
267 '\\' => out.write_str("\\\\"),
268 '\u{2028}' => out.write_str("\\u2028"),
269 '\u{2029}' => out.write_str("\\u2029"),
270 c => write!(out, "\\x{:02x}", c as u32),
272 }
273}
274
275fn encode_js(input: &str, config: &JsConfig) -> String {
276 let mut out = String::with_capacity(input.len());
277 write_js(&mut out, input, config).expect("writing to string cannot fail");
278 out
279}
280
281fn write_js<W: fmt::Write>(out: &mut W, input: &str, config: &JsConfig) -> fmt::Result {
282 encode_loop(
283 out,
284 input,
285 |c| needs_js_encoding(c, config),
286 |out, c, _next| write_js_encoded(out, c, config),
287 )
288}
289
290fn needs_js_encoding(c: char, config: &JsConfig) -> bool {
291 match c {
292 '\x00'..='\x1F' | '\\' | '"' | '\'' | '\u{2028}' | '\u{2029}' => true,
293 '&' => config.encode_ampersand,
294 '/' => config.encode_slash,
295 _ => false,
296 }
297}
298
299fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
300 match c {
301 '\x08' => out.write_str("\\b"),
302 '\t' => out.write_str("\\t"),
303 '\n' => out.write_str("\\n"),
304 '\x0B' => out.write_str("\\x0b"),
305 '\x0C' => out.write_str("\\f"),
306 '\r' => out.write_str("\\r"),
307 '"' if config.hex_quotes => out.write_str("\\x22"),
308 '"' => out.write_str("\\\""),
309 '\'' if config.hex_quotes => out.write_str("\\x27"),
310 '\'' => out.write_str("\\'"),
311 '&' => out.write_str("\\x26"),
312 '/' => out.write_str("\\/"),
313 '\\' => out.write_str("\\\\"),
314 '\u{2028}' => out.write_str("\\u2028"),
315 '\u{2029}' => out.write_str("\\u2029"),
316 c => write!(out, "\\x{:02x}", c as u32),
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
328 fn js_no_encoding_needed() {
329 assert_eq!(for_javascript("hello world"), "hello world");
330 assert_eq!(for_javascript(""), "");
331 }
332
333 #[test]
334 fn js_encodes_quotes_as_hex() {
335 assert_eq!(for_javascript(r#"a"b"#), r"a\x22b");
336 assert_eq!(for_javascript("a'b"), r"a\x27b");
337 }
338
339 #[test]
340 fn js_encodes_backslash() {
341 assert_eq!(for_javascript(r"a\b"), r"a\\b");
342 }
343
344 #[test]
345 fn js_encodes_ampersand() {
346 assert_eq!(for_javascript("a&b"), r"a\x26b");
347 }
348
349 #[test]
350 fn js_encodes_slash() {
351 assert_eq!(for_javascript("</script>"), r"<\/script>");
352 }
353
354 #[test]
355 fn js_encodes_control_chars() {
356 assert_eq!(for_javascript("\x00"), r"\x00");
357 assert_eq!(for_javascript("\x08"), r"\b");
358 assert_eq!(for_javascript("\t"), r"\t");
359 assert_eq!(for_javascript("\n"), r"\n");
360 assert_eq!(for_javascript("\x0B"), r"\x0b");
361 assert_eq!(for_javascript("\x0C"), r"\f");
362 assert_eq!(for_javascript("\r"), r"\r");
363 assert_eq!(for_javascript("\x1F"), r"\x1f");
364 }
365
366 #[test]
367 fn js_encodes_line_separators() {
368 assert_eq!(for_javascript("\u{2028}"), r"\u2028");
369 assert_eq!(for_javascript("\u{2029}"), r"\u2029");
370 }
371
372 #[test]
373 fn js_preserves_non_ascii() {
374 assert_eq!(for_javascript("café"), "café");
375 assert_eq!(for_javascript("日本語"), "日本語");
376 }
377
378 #[test]
379 fn js_writer_variant() {
380 let mut out = String::new();
381 write_javascript(&mut out, "a'b").unwrap();
382 assert_eq!(out, r"a\x27b");
383 }
384
385 #[test]
388 fn js_attr_does_not_encode_slash() {
389 assert_eq!(for_javascript_attribute("a/b"), "a/b");
390 }
391
392 #[test]
393 fn js_attr_encodes_quotes_as_hex() {
394 assert_eq!(for_javascript_attribute("a'b"), r"a\x27b");
395 }
396
397 #[test]
398 fn js_attr_encodes_ampersand() {
399 assert_eq!(for_javascript_attribute("a&b"), r"a\x26b");
400 }
401
402 #[test]
405 fn js_block_uses_backslash_quotes() {
406 assert_eq!(for_javascript_block(r#"a"b"#), r#"a\"b"#);
407 assert_eq!(for_javascript_block("a'b"), r"a\'b");
408 }
409
410 #[test]
411 fn js_block_encodes_slash() {
412 assert_eq!(for_javascript_block("a/b"), r"a\/b");
413 }
414
415 #[test]
416 fn js_block_encodes_ampersand() {
417 assert_eq!(for_javascript_block("a&b"), r"a\x26b");
418 }
419
420 #[test]
423 fn js_source_uses_backslash_quotes() {
424 assert_eq!(for_javascript_source(r#"a"b"#), r#"a\"b"#);
425 assert_eq!(for_javascript_source("a'b"), r"a\'b");
426 }
427
428 #[test]
429 fn js_source_does_not_encode_slash_or_ampersand() {
430 assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
431 }
432
433 #[test]
434 fn js_source_encodes_line_separators() {
435 assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
436 }
437
438 #[test]
441 fn js_template_no_encoding_needed() {
442 assert_eq!(for_js_template("hello world"), "hello world");
443 assert_eq!(for_js_template(""), "");
444 }
445
446 #[test]
447 fn js_template_encodes_backtick() {
448 assert_eq!(for_js_template("hello `world`"), r"hello \`world\`");
449 assert_eq!(for_js_template("`"), r"\`");
450 }
451
452 #[test]
453 fn js_template_encodes_interpolation() {
454 assert_eq!(for_js_template("${alert(1)}"), r"\${alert(1)}");
455 assert_eq!(for_js_template("a${b}c"), r"a\${b}c");
456 assert_eq!(for_js_template("${a}${b}"), r"\${a}\${b}");
457 }
458
459 #[test]
460 fn js_template_dollar_without_brace_passes_through() {
461 assert_eq!(for_js_template("a $ b"), "a $ b");
462 assert_eq!(for_js_template("$100"), "$100");
463 assert_eq!(for_js_template("a$"), "a$");
464 }
465
466 #[test]
467 fn js_template_encodes_backslash() {
468 assert_eq!(for_js_template(r"a\b"), r"a\\b");
469 }
470
471 #[test]
472 fn js_template_encodes_slash() {
473 assert_eq!(for_js_template("</script>"), r"<\/script>");
474 }
475
476 #[test]
477 fn js_template_does_not_encode_quotes() {
478 assert_eq!(for_js_template(r#"a"b"#), r#"a"b"#);
479 assert_eq!(for_js_template("a'b"), "a'b");
480 }
481
482 #[test]
483 fn js_template_encodes_control_chars() {
484 assert_eq!(for_js_template("\x00"), r"\x00");
485 assert_eq!(for_js_template("\x08"), r"\b");
486 assert_eq!(for_js_template("\t"), r"\t");
487 assert_eq!(for_js_template("\n"), r"\n");
488 assert_eq!(for_js_template("\x0B"), r"\x0b");
489 assert_eq!(for_js_template("\x0C"), r"\f");
490 assert_eq!(for_js_template("\r"), r"\r");
491 assert_eq!(for_js_template("\x1F"), r"\x1f");
492 }
493
494 #[test]
495 fn js_template_encodes_line_separators() {
496 assert_eq!(for_js_template("\u{2028}"), r"\u2028");
497 assert_eq!(for_js_template("\u{2029}"), r"\u2029");
498 }
499
500 #[test]
501 fn js_template_preserves_non_ascii() {
502 assert_eq!(for_js_template("café"), "café");
503 assert_eq!(for_js_template("日本語"), "日本語");
504 assert_eq!(for_js_template("😀"), "😀");
505 }
506
507 #[test]
508 fn js_template_mixed_input() {
509 assert_eq!(
510 for_js_template("`Hello ${name}`, welcome\\n"),
511 r"\`Hello \${name}\`, welcome\\n"
512 );
513 }
514
515 #[test]
516 fn js_template_writer_variant() {
517 let input = "`test` ${x} café";
518 let string_result = for_js_template(input);
519 let mut writer_result = String::new();
520 write_js_template(&mut writer_result, input).unwrap();
521 assert_eq!(string_result, writer_result);
522 }
523}