1use std::fmt;
37
38use crate::engine::{encode_loop, is_text_direction_control};
39
40#[derive(Clone, Copy)]
42struct JsConfig {
43 hex_quotes: bool,
46 encode_ampersand: bool,
48 script_data: bool,
52}
53
54const JS_UNIVERSAL: JsConfig = JsConfig {
55 hex_quotes: true,
56 encode_ampersand: true,
57 script_data: true,
58};
59
60const JS_ATTRIBUTE: JsConfig = JsConfig {
61 hex_quotes: true,
62 encode_ampersand: true,
63 script_data: false,
64};
65
66const JS_BLOCK: JsConfig = JsConfig {
67 hex_quotes: false,
68 encode_ampersand: true,
69 script_data: true,
70};
71
72const JS_SOURCE: JsConfig = JsConfig {
73 hex_quotes: false,
74 encode_ampersand: false,
75 script_data: false,
76};
77
78pub fn for_javascript(input: &str) -> String {
120 encode_js(input, &JS_UNIVERSAL)
121}
122
123pub fn write_javascript<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
127 write_js(out, input, &JS_UNIVERSAL)
128}
129
130pub fn for_javascript_attribute(input: &str) -> String {
150 encode_js(input, &JS_ATTRIBUTE)
151}
152
153pub fn write_javascript_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
157 write_js(out, input, &JS_ATTRIBUTE)
158}
159
160pub fn for_javascript_block(input: &str) -> String {
178 encode_js(input, &JS_BLOCK)
179}
180
181pub fn write_javascript_block<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
185 write_js(out, input, &JS_BLOCK)
186}
187
188pub fn for_javascript_source(input: &str) -> String {
211 encode_js(input, &JS_SOURCE)
212}
213
214pub fn write_javascript_source<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
218 write_js(out, input, &JS_SOURCE)
219}
220
221pub fn for_js_template(input: &str) -> String {
262 let mut out = String::with_capacity(input.len());
263 write_js_template(&mut out, input).expect("writing to string cannot fail");
264 out
265}
266
267pub fn write_js_template<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
271 encode_loop(
272 out,
273 input,
274 needs_js_template_encoding,
275 write_js_template_encoded,
276 )
277}
278
279fn needs_js_template_encoding(c: char) -> bool {
280 matches!(
281 c,
282 '\x00'..='\x1F' | '\\' | '`' | '$' | '&' | '/' | '<' | '\u{2028}' | '\u{2029}'
283 ) || is_text_direction_control(c)
284}
285
286fn write_js_template_encoded<W: fmt::Write>(
287 out: &mut W,
288 c: char,
289 next: Option<char>,
290) -> fmt::Result {
291 match c {
292 '`' => out.write_str("\\`"),
294 '$' if matches!(next, Some('{') | None) => out.write_str("\\$"),
295 '$' => out.write_char('$'),
296 '&' => out.write_str("\\x26"),
297 '/' => out.write_str("\\/"),
298 '<' => out.write_str("\\x3c"),
299 c => write_js_shared_escape(out, c),
300 }
301}
302
303fn encode_js(input: &str, config: &JsConfig) -> String {
304 let mut out = String::with_capacity(input.len());
305 write_js(&mut out, input, config).expect("writing to string cannot fail");
306 out
307}
308
309fn write_js<W: fmt::Write>(out: &mut W, input: &str, config: &JsConfig) -> fmt::Result {
310 encode_loop(
311 out,
312 input,
313 |c| needs_js_encoding(c, config),
314 |out, c, _next| write_js_encoded(out, c, config),
315 )
316}
317
318fn needs_js_encoding(c: char, config: &JsConfig) -> bool {
319 match c {
320 '\x00'..='\x1F' | '\\' | '"' | '\'' | '\u{2028}' | '\u{2029}' => true,
321 '&' => config.encode_ampersand,
322 '/' | '<' => config.script_data,
323 c => is_text_direction_control(c),
324 }
325}
326
327fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
328 match c {
329 '"' if config.hex_quotes => out.write_str("\\x22"),
331 '"' => out.write_str("\\\""),
332 '\'' if config.hex_quotes => out.write_str("\\x27"),
333 '\'' => out.write_str("\\'"),
334 '&' => out.write_str("\\x26"),
335 '/' => out.write_str("\\/"),
336 '<' => out.write_str("\\x3c"),
337 c => write_js_shared_escape(out, c),
338 }
339}
340
341fn write_js_shared_escape<W: fmt::Write>(out: &mut W, c: char) -> fmt::Result {
345 match c {
346 '\x08' => out.write_str("\\b"),
347 '\t' => out.write_str("\\t"),
348 '\n' => out.write_str("\\n"),
349 '\x0B' => out.write_str("\\x0b"),
350 '\x0C' => out.write_str("\\f"),
351 '\r' => out.write_str("\\r"),
352 '\\' => out.write_str("\\\\"),
353 '\u{2028}' => out.write_str("\\u2028"),
354 '\u{2029}' => out.write_str("\\u2029"),
355 '\x00'..='\x1F' => write!(out, "\\x{:02x}", c as u32),
356 c if is_text_direction_control(c) => write!(out, "\\u{:04x}", c as u32),
357 c => write!(out, "\\u{{{:x}}}", c as u32),
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 const TEXT_DIRECTION: [(&str, &str); 9] = [
367 ("\u{202A}", r"\u202a"),
368 ("\u{202B}", r"\u202b"),
369 ("\u{202C}", r"\u202c"),
370 ("\u{202D}", r"\u202d"),
371 ("\u{202E}", r"\u202e"),
372 ("\u{2066}", r"\u2066"),
373 ("\u{2067}", r"\u2067"),
374 ("\u{2068}", r"\u2068"),
375 ("\u{2069}", r"\u2069"),
376 ];
377
378 #[test]
381 fn js_no_encoding_needed() {
382 assert_eq!(for_javascript("hello world"), "hello world");
383 assert_eq!(for_javascript(""), "");
384 }
385
386 #[test]
387 fn js_encodes_quotes_as_hex() {
388 assert_eq!(for_javascript(r#"a"b"#), r"a\x22b");
389 assert_eq!(for_javascript("a'b"), r"a\x27b");
390 }
391
392 #[test]
393 fn js_encodes_backslash() {
394 assert_eq!(for_javascript(r"a\b"), r"a\\b");
395 }
396
397 #[test]
398 fn js_encodes_ampersand() {
399 assert_eq!(for_javascript("a&b"), r"a\x26b");
400 }
401
402 #[test]
403 fn js_encodes_slash() {
404 assert_eq!(for_javascript("a/b"), r"a\/b");
405 assert_eq!(for_javascript("</script>"), r"\x3c\/script>");
406 }
407
408 #[test]
409 fn js_encodes_lt() {
410 assert_eq!(for_javascript("a<b"), r"a\x3cb");
411 assert_eq!(for_javascript("<!--<script>"), r"\x3c!--\x3cscript>");
412 assert_eq!(for_javascript("<!--"), r"\x3c!--");
413 assert_eq!(for_javascript("<script"), r"\x3cscript");
414 }
415
416 #[test]
417 fn js_encodes_control_chars() {
418 assert_eq!(for_javascript("\x00"), r"\x00");
419 assert_eq!(for_javascript("\x08"), r"\b");
420 assert_eq!(for_javascript("\t"), r"\t");
421 assert_eq!(for_javascript("\n"), r"\n");
422 assert_eq!(for_javascript("\x0B"), r"\x0b");
423 assert_eq!(for_javascript("\x0C"), r"\f");
424 assert_eq!(for_javascript("\r"), r"\r");
425 assert_eq!(for_javascript("\x1F"), r"\x1f");
426 }
427
428 #[test]
429 fn js_encodes_line_separators() {
430 assert_eq!(for_javascript("\u{2028}"), r"\u2028");
431 assert_eq!(for_javascript("\u{2029}"), r"\u2029");
432 }
433
434 #[test]
435 fn js_escapes_text_direction_controls() {
436 for (raw, escaped) in TEXT_DIRECTION {
437 assert_eq!(for_javascript(raw), escaped);
438 assert_eq!(for_javascript(&format!("a{raw}b")), format!("a{escaped}b"));
439 }
440 }
441
442 #[test]
443 fn js_preserves_non_ascii() {
444 assert_eq!(for_javascript("café"), "café");
445 assert_eq!(for_javascript("日本語"), "日本語");
446 }
447
448 #[test]
449 fn js_writer_variant() {
450 let mut out = String::new();
451 write_javascript(&mut out, "a'b").unwrap();
452 assert_eq!(out, r"a\x27b");
453 }
454
455 #[test]
458 fn js_attr_does_not_encode_slash_or_lt() {
459 assert_eq!(for_javascript_attribute("a/b"), "a/b");
460 assert_eq!(for_javascript_attribute("<!--<script>"), "<!--<script>");
461 }
462
463 #[test]
464 fn js_attr_encodes_quotes_as_hex() {
465 assert_eq!(for_javascript_attribute("a'b"), r"a\x27b");
466 }
467
468 #[test]
469 fn js_attr_encodes_ampersand() {
470 assert_eq!(for_javascript_attribute("a&b"), r"a\x26b");
471 }
472
473 #[test]
474 fn js_attr_escapes_text_direction_controls() {
475 for (raw, escaped) in TEXT_DIRECTION {
476 assert_eq!(for_javascript_attribute(raw), escaped);
477 }
478 }
479
480 #[test]
483 fn js_block_uses_backslash_quotes() {
484 assert_eq!(for_javascript_block(r#"a"b"#), r#"a\"b"#);
485 assert_eq!(for_javascript_block("a'b"), r"a\'b");
486 }
487
488 #[test]
489 fn js_block_encodes_slash() {
490 assert_eq!(for_javascript_block("a/b"), r"a\/b");
491 }
492
493 #[test]
494 fn js_block_encodes_lt() {
495 assert_eq!(for_javascript_block("a<b"), r"a\x3cb");
496 assert_eq!(for_javascript_block("<!--<script>"), r"\x3c!--\x3cscript>");
497 assert_eq!(for_javascript_block("<!--"), r"\x3c!--");
498 assert_eq!(for_javascript_block("<script"), r"\x3cscript");
499 }
500
501 #[test]
502 fn js_block_encodes_ampersand() {
503 assert_eq!(for_javascript_block("a&b"), r"a\x26b");
504 }
505
506 #[test]
507 fn js_block_escapes_text_direction_controls() {
508 for (raw, escaped) in TEXT_DIRECTION {
509 assert_eq!(for_javascript_block(raw), escaped);
510 }
511 }
512
513 #[test]
516 fn js_source_uses_backslash_quotes() {
517 assert_eq!(for_javascript_source(r#"a"b"#), r#"a\"b"#);
518 assert_eq!(for_javascript_source("a'b"), r"a\'b");
519 }
520
521 #[test]
522 fn js_source_does_not_encode_slash_ampersand_or_lt() {
523 assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
524 assert_eq!(for_javascript_source("<!--<script>"), "<!--<script>");
525 }
526
527 #[test]
528 fn js_source_encodes_line_separators() {
529 assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
530 }
531
532 #[test]
533 fn js_source_escapes_text_direction_controls() {
534 for (raw, escaped) in TEXT_DIRECTION {
535 assert_eq!(for_javascript_source(raw), escaped);
536 assert_eq!(
537 for_javascript_source(&format!("var x = '{raw}';")),
538 format!(r"var x = \'{escaped}\';")
539 );
540 }
541 }
542
543 #[test]
546 fn js_template_no_encoding_needed() {
547 assert_eq!(for_js_template("hello world"), "hello world");
548 assert_eq!(for_js_template(""), "");
549 }
550
551 #[test]
552 fn js_template_encodes_backtick() {
553 assert_eq!(for_js_template("hello `world`"), r"hello \`world\`");
554 assert_eq!(for_js_template("`"), r"\`");
555 }
556
557 #[test]
558 fn js_template_encodes_interpolation() {
559 assert_eq!(for_js_template("${alert(1)}"), r"\${alert(1)}");
560 assert_eq!(for_js_template("a${b}c"), r"a\${b}c");
561 assert_eq!(for_js_template("${a}${b}"), r"\${a}\${b}");
562 }
563
564 #[test]
565 fn js_template_dollar_without_brace_passes_through() {
566 assert_eq!(for_js_template("a $ b"), "a $ b");
567 assert_eq!(for_js_template("$100"), "$100");
568 }
569
570 #[test]
571 fn js_template_escapes_trailing_dollar() {
572 assert_eq!(for_js_template("a$"), r"a\$");
573 assert_eq!(for_js_template("$"), r"\$");
574 }
575
576 #[test]
577 fn js_template_encodes_backslash() {
578 assert_eq!(for_js_template(r"a\b"), r"a\\b");
579 }
580
581 #[test]
582 fn js_template_encodes_slash() {
583 assert_eq!(for_js_template("a/b"), r"a\/b");
584 assert_eq!(for_js_template("</script>"), r"\x3c\/script>");
585 }
586
587 #[test]
588 fn js_template_encodes_lt() {
589 assert_eq!(for_js_template("a<b"), r"a\x3cb");
590 assert_eq!(for_js_template("<!--<script>"), r"\x3c!--\x3cscript>");
591 assert_eq!(for_js_template("<!--"), r"\x3c!--");
592 assert_eq!(for_js_template("<script"), r"\x3cscript");
593 }
594
595 #[test]
596 fn js_template_does_not_encode_quotes() {
597 assert_eq!(for_js_template(r#"a"b"#), r#"a"b"#);
598 assert_eq!(for_js_template("a'b"), "a'b");
599 }
600
601 #[test]
602 fn js_template_encodes_control_chars() {
603 assert_eq!(for_js_template("\x00"), r"\x00");
604 assert_eq!(for_js_template("\x08"), r"\b");
605 assert_eq!(for_js_template("\t"), r"\t");
606 assert_eq!(for_js_template("\n"), r"\n");
607 assert_eq!(for_js_template("\x0B"), r"\x0b");
608 assert_eq!(for_js_template("\x0C"), r"\f");
609 assert_eq!(for_js_template("\r"), r"\r");
610 assert_eq!(for_js_template("\x1F"), r"\x1f");
611 }
612
613 #[test]
614 fn js_template_escapes_text_direction_controls() {
615 for (raw, escaped) in TEXT_DIRECTION {
616 assert_eq!(for_js_template(raw), escaped);
617 assert_eq!(for_js_template(&format!("a{raw}b")), format!("a{escaped}b"));
618 }
619 }
620
621 #[test]
622 fn js_template_encodes_line_separators() {
623 assert_eq!(for_js_template("\u{2028}"), r"\u2028");
624 assert_eq!(for_js_template("\u{2029}"), r"\u2029");
625 }
626
627 #[test]
628 fn js_template_preserves_non_ascii() {
629 assert_eq!(for_js_template("café"), "café");
630 assert_eq!(for_js_template("日本語"), "日本語");
631 assert_eq!(for_js_template("😀"), "😀");
632 }
633
634 #[test]
635 fn js_template_mixed_input() {
636 assert_eq!(
637 for_js_template("`Hello ${name}`, welcome\\n"),
638 r"\`Hello \${name}\`, welcome\\n"
639 );
640 }
641
642 #[test]
643 fn js_template_writer_variant() {
644 let input = "`test` ${x} café";
645 let string_result = for_js_template(input);
646 let mut writer_result = String::new();
647 write_js_template(&mut writer_result, input).unwrap();
648 assert_eq!(string_result, writer_result);
649 }
650
651 #[test]
654 fn shared_escape_handles_shared_chars() {
655 let cases = [
656 ('\x08', r"\b"),
657 ('\t', r"\t"),
658 ('\n', r"\n"),
659 ('\x0B', r"\x0b"),
660 ('\x0C', r"\f"),
661 ('\r', r"\r"),
662 ('\\', r"\\"),
663 ('\x00', r"\x00"),
664 ('\x1F', r"\x1f"),
665 ('\u{2028}', r"\u2028"),
666 ('\u{2029}', r"\u2029"),
667 ];
668 for (c, expected) in cases {
669 let mut out = String::new();
670 assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
671 assert_eq!(out, expected);
672 }
673 }
674
675 #[test]
676 fn shared_escape_never_drops_a_character() {
677 for (c, expected) in [('a', r"\u{61}"), ('"', r"\u{22}"), ('é', r"\u{e9}")] {
678 let mut out = String::new();
679 assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
680 assert_eq!(out, expected);
681 }
682 }
683}