1use std::fmt;
34
35use crate::engine::encode_loop;
36
37#[derive(Clone, Copy)]
39struct JsConfig {
40 hex_quotes: bool,
43 encode_ampersand: bool,
45 script_data: bool,
49}
50
51const JS_UNIVERSAL: JsConfig = JsConfig {
52 hex_quotes: true,
53 encode_ampersand: true,
54 script_data: true,
55};
56
57const JS_ATTRIBUTE: JsConfig = JsConfig {
58 hex_quotes: true,
59 encode_ampersand: true,
60 script_data: false,
61};
62
63const JS_BLOCK: JsConfig = JsConfig {
64 hex_quotes: false,
65 encode_ampersand: true,
66 script_data: true,
67};
68
69const JS_SOURCE: JsConfig = JsConfig {
70 hex_quotes: false,
71 encode_ampersand: false,
72 script_data: false,
73};
74
75pub fn for_javascript(input: &str) -> String {
116 encode_js(input, &JS_UNIVERSAL)
117}
118
119pub fn write_javascript<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
123 write_js(out, input, &JS_UNIVERSAL)
124}
125
126pub fn for_javascript_attribute(input: &str) -> String {
146 encode_js(input, &JS_ATTRIBUTE)
147}
148
149pub fn write_javascript_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
153 write_js(out, input, &JS_ATTRIBUTE)
154}
155
156pub fn for_javascript_block(input: &str) -> String {
174 encode_js(input, &JS_BLOCK)
175}
176
177pub fn write_javascript_block<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
181 write_js(out, input, &JS_BLOCK)
182}
183
184pub fn for_javascript_source(input: &str) -> String {
207 encode_js(input, &JS_SOURCE)
208}
209
210pub fn write_javascript_source<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
214 write_js(out, input, &JS_SOURCE)
215}
216
217pub fn for_js_template(input: &str) -> String {
257 let mut out = String::with_capacity(input.len());
258 write_js_template(&mut out, input).expect("writing to string cannot fail");
259 out
260}
261
262pub fn write_js_template<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
266 encode_loop(
267 out,
268 input,
269 needs_js_template_encoding,
270 write_js_template_encoded,
271 )
272}
273
274fn needs_js_template_encoding(c: char) -> bool {
275 matches!(
276 c,
277 '\x00'..='\x1F' | '\\' | '`' | '$' | '&' | '/' | '<' | '\u{2028}' | '\u{2029}'
278 )
279}
280
281fn write_js_template_encoded<W: fmt::Write>(
282 out: &mut W,
283 c: char,
284 next: Option<char>,
285) -> fmt::Result {
286 match c {
287 '`' => out.write_str("\\`"),
289 '$' if matches!(next, Some('{') | None) => out.write_str("\\$"),
290 '$' => out.write_char('$'),
291 '&' => out.write_str("\\x26"),
292 '/' => out.write_str("\\/"),
293 '<' => out.write_str("\\x3c"),
294 c => write_js_shared_escape(out, c),
296 }
297}
298
299fn encode_js(input: &str, config: &JsConfig) -> String {
300 let mut out = String::with_capacity(input.len());
301 write_js(&mut out, input, config).expect("writing to string cannot fail");
302 out
303}
304
305fn write_js<W: fmt::Write>(out: &mut W, input: &str, config: &JsConfig) -> fmt::Result {
306 encode_loop(
307 out,
308 input,
309 |c| needs_js_encoding(c, config),
310 |out, c, _next| write_js_encoded(out, c, config),
311 )
312}
313
314fn needs_js_encoding(c: char, config: &JsConfig) -> bool {
315 match c {
316 '\x00'..='\x1F' | '\\' | '"' | '\'' | '\u{2028}' | '\u{2029}' => true,
317 '&' => config.encode_ampersand,
318 '/' | '<' => config.script_data,
319 _ => false,
320 }
321}
322
323fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
324 match c {
325 '"' if config.hex_quotes => out.write_str("\\x22"),
327 '"' => out.write_str("\\\""),
328 '\'' if config.hex_quotes => out.write_str("\\x27"),
329 '\'' => out.write_str("\\'"),
330 '&' => out.write_str("\\x26"),
331 '/' => out.write_str("\\/"),
332 '<' => out.write_str("\\x3c"),
333 c => write_js_shared_escape(out, c),
335 }
336}
337
338fn write_js_shared_escape<W: fmt::Write>(out: &mut W, c: char) -> fmt::Result {
342 match c {
343 '\x08' => out.write_str("\\b"),
344 '\t' => out.write_str("\\t"),
345 '\n' => out.write_str("\\n"),
346 '\x0B' => out.write_str("\\x0b"),
347 '\x0C' => out.write_str("\\f"),
348 '\r' => out.write_str("\\r"),
349 '\\' => out.write_str("\\\\"),
350 '\u{2028}' => out.write_str("\\u2028"),
351 '\u{2029}' => out.write_str("\\u2029"),
352 '\x00'..='\x1F' => write!(out, "\\x{:02x}", c as u32),
353 c => write!(out, "\\u{{{:x}}}", c as u32),
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
364 fn js_no_encoding_needed() {
365 assert_eq!(for_javascript("hello world"), "hello world");
366 assert_eq!(for_javascript(""), "");
367 }
368
369 #[test]
370 fn js_encodes_quotes_as_hex() {
371 assert_eq!(for_javascript(r#"a"b"#), r"a\x22b");
372 assert_eq!(for_javascript("a'b"), r"a\x27b");
373 }
374
375 #[test]
376 fn js_encodes_backslash() {
377 assert_eq!(for_javascript(r"a\b"), r"a\\b");
378 }
379
380 #[test]
381 fn js_encodes_ampersand() {
382 assert_eq!(for_javascript("a&b"), r"a\x26b");
383 }
384
385 #[test]
386 fn js_encodes_slash() {
387 assert_eq!(for_javascript("a/b"), r"a\/b");
388 assert_eq!(for_javascript("</script>"), r"\x3c\/script>");
389 }
390
391 #[test]
392 fn js_encodes_lt() {
393 assert_eq!(for_javascript("a<b"), r"a\x3cb");
394 assert_eq!(for_javascript("<!--<script>"), r"\x3c!--\x3cscript>");
395 assert_eq!(for_javascript("<!--"), r"\x3c!--");
396 assert_eq!(for_javascript("<script"), r"\x3cscript");
397 }
398
399 #[test]
400 fn js_encodes_control_chars() {
401 assert_eq!(for_javascript("\x00"), r"\x00");
402 assert_eq!(for_javascript("\x08"), r"\b");
403 assert_eq!(for_javascript("\t"), r"\t");
404 assert_eq!(for_javascript("\n"), r"\n");
405 assert_eq!(for_javascript("\x0B"), r"\x0b");
406 assert_eq!(for_javascript("\x0C"), r"\f");
407 assert_eq!(for_javascript("\r"), r"\r");
408 assert_eq!(for_javascript("\x1F"), r"\x1f");
409 }
410
411 #[test]
412 fn js_encodes_line_separators() {
413 assert_eq!(for_javascript("\u{2028}"), r"\u2028");
414 assert_eq!(for_javascript("\u{2029}"), r"\u2029");
415 }
416
417 #[test]
418 fn js_preserves_non_ascii() {
419 assert_eq!(for_javascript("café"), "café");
420 assert_eq!(for_javascript("日本語"), "日本語");
421 }
422
423 #[test]
424 fn js_writer_variant() {
425 let mut out = String::new();
426 write_javascript(&mut out, "a'b").unwrap();
427 assert_eq!(out, r"a\x27b");
428 }
429
430 #[test]
433 fn js_attr_does_not_encode_slash_or_lt() {
434 assert_eq!(for_javascript_attribute("a/b"), "a/b");
435 assert_eq!(for_javascript_attribute("<!--<script>"), "<!--<script>");
436 }
437
438 #[test]
439 fn js_attr_encodes_quotes_as_hex() {
440 assert_eq!(for_javascript_attribute("a'b"), r"a\x27b");
441 }
442
443 #[test]
444 fn js_attr_encodes_ampersand() {
445 assert_eq!(for_javascript_attribute("a&b"), r"a\x26b");
446 }
447
448 #[test]
451 fn js_block_uses_backslash_quotes() {
452 assert_eq!(for_javascript_block(r#"a"b"#), r#"a\"b"#);
453 assert_eq!(for_javascript_block("a'b"), r"a\'b");
454 }
455
456 #[test]
457 fn js_block_encodes_slash() {
458 assert_eq!(for_javascript_block("a/b"), r"a\/b");
459 }
460
461 #[test]
462 fn js_block_encodes_lt() {
463 assert_eq!(for_javascript_block("a<b"), r"a\x3cb");
464 assert_eq!(for_javascript_block("<!--<script>"), r"\x3c!--\x3cscript>");
465 assert_eq!(for_javascript_block("<!--"), r"\x3c!--");
466 assert_eq!(for_javascript_block("<script"), r"\x3cscript");
467 }
468
469 #[test]
470 fn js_block_encodes_ampersand() {
471 assert_eq!(for_javascript_block("a&b"), r"a\x26b");
472 }
473
474 #[test]
477 fn js_source_uses_backslash_quotes() {
478 assert_eq!(for_javascript_source(r#"a"b"#), r#"a\"b"#);
479 assert_eq!(for_javascript_source("a'b"), r"a\'b");
480 }
481
482 #[test]
483 fn js_source_does_not_encode_slash_ampersand_or_lt() {
484 assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
485 assert_eq!(for_javascript_source("<!--<script>"), "<!--<script>");
486 }
487
488 #[test]
489 fn js_source_encodes_line_separators() {
490 assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
491 }
492
493 #[test]
496 fn js_template_no_encoding_needed() {
497 assert_eq!(for_js_template("hello world"), "hello world");
498 assert_eq!(for_js_template(""), "");
499 }
500
501 #[test]
502 fn js_template_encodes_backtick() {
503 assert_eq!(for_js_template("hello `world`"), r"hello \`world\`");
504 assert_eq!(for_js_template("`"), r"\`");
505 }
506
507 #[test]
508 fn js_template_encodes_interpolation() {
509 assert_eq!(for_js_template("${alert(1)}"), r"\${alert(1)}");
510 assert_eq!(for_js_template("a${b}c"), r"a\${b}c");
511 assert_eq!(for_js_template("${a}${b}"), r"\${a}\${b}");
512 }
513
514 #[test]
515 fn js_template_dollar_without_brace_passes_through() {
516 assert_eq!(for_js_template("a $ b"), "a $ b");
517 assert_eq!(for_js_template("$100"), "$100");
518 }
519
520 #[test]
521 fn js_template_escapes_trailing_dollar() {
522 assert_eq!(for_js_template("a$"), r"a\$");
523 assert_eq!(for_js_template("$"), r"\$");
524 }
525
526 #[test]
527 fn js_template_encodes_backslash() {
528 assert_eq!(for_js_template(r"a\b"), r"a\\b");
529 }
530
531 #[test]
532 fn js_template_encodes_slash() {
533 assert_eq!(for_js_template("a/b"), r"a\/b");
534 assert_eq!(for_js_template("</script>"), r"\x3c\/script>");
535 }
536
537 #[test]
538 fn js_template_encodes_lt() {
539 assert_eq!(for_js_template("a<b"), r"a\x3cb");
540 assert_eq!(for_js_template("<!--<script>"), r"\x3c!--\x3cscript>");
541 assert_eq!(for_js_template("<!--"), r"\x3c!--");
542 assert_eq!(for_js_template("<script"), r"\x3cscript");
543 }
544
545 #[test]
546 fn js_template_does_not_encode_quotes() {
547 assert_eq!(for_js_template(r#"a"b"#), r#"a"b"#);
548 assert_eq!(for_js_template("a'b"), "a'b");
549 }
550
551 #[test]
552 fn js_template_encodes_control_chars() {
553 assert_eq!(for_js_template("\x00"), r"\x00");
554 assert_eq!(for_js_template("\x08"), r"\b");
555 assert_eq!(for_js_template("\t"), r"\t");
556 assert_eq!(for_js_template("\n"), r"\n");
557 assert_eq!(for_js_template("\x0B"), r"\x0b");
558 assert_eq!(for_js_template("\x0C"), r"\f");
559 assert_eq!(for_js_template("\r"), r"\r");
560 assert_eq!(for_js_template("\x1F"), r"\x1f");
561 }
562
563 #[test]
564 fn js_template_encodes_line_separators() {
565 assert_eq!(for_js_template("\u{2028}"), r"\u2028");
566 assert_eq!(for_js_template("\u{2029}"), r"\u2029");
567 }
568
569 #[test]
570 fn js_template_preserves_non_ascii() {
571 assert_eq!(for_js_template("café"), "café");
572 assert_eq!(for_js_template("日本語"), "日本語");
573 assert_eq!(for_js_template("😀"), "😀");
574 }
575
576 #[test]
577 fn js_template_mixed_input() {
578 assert_eq!(
579 for_js_template("`Hello ${name}`, welcome\\n"),
580 r"\`Hello \${name}\`, welcome\\n"
581 );
582 }
583
584 #[test]
585 fn js_template_writer_variant() {
586 let input = "`test` ${x} café";
587 let string_result = for_js_template(input);
588 let mut writer_result = String::new();
589 write_js_template(&mut writer_result, input).unwrap();
590 assert_eq!(string_result, writer_result);
591 }
592
593 #[test]
596 fn shared_escape_handles_shared_chars() {
597 let cases = [
598 ('\x08', r"\b"),
599 ('\t', r"\t"),
600 ('\n', r"\n"),
601 ('\x0B', r"\x0b"),
602 ('\x0C', r"\f"),
603 ('\r', r"\r"),
604 ('\\', r"\\"),
605 ('\x00', r"\x00"),
606 ('\x1F', r"\x1f"),
607 ('\u{2028}', r"\u2028"),
608 ('\u{2029}', r"\u2029"),
609 ];
610 for (c, expected) in cases {
611 let mut out = String::new();
612 assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
613 assert_eq!(out, expected);
614 }
615 }
616
617 #[test]
618 fn shared_escape_never_drops_a_character() {
619 for (c, expected) in [('a', r"\u{61}"), ('"', r"\u{22}"), ('é', r"\u{e9}")] {
620 let mut out = String::new();
621 assert_eq!(write_js_shared_escape(&mut out, c), Ok(()));
622 assert_eq!(out, expected);
623 }
624 }
625}