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