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 encode_slash: bool,
44}
45
46const JS_UNIVERSAL: JsConfig = JsConfig {
47 hex_quotes: true,
48 encode_ampersand: true,
49 encode_slash: true,
50};
51
52const JS_ATTRIBUTE: JsConfig = JsConfig {
53 hex_quotes: true,
54 encode_ampersand: true,
55 encode_slash: false,
56};
57
58const JS_BLOCK: JsConfig = JsConfig {
59 hex_quotes: false,
60 encode_ampersand: true,
61 encode_slash: true,
62};
63
64const JS_SOURCE: JsConfig = JsConfig {
65 hex_quotes: false,
66 encode_ampersand: false,
67 encode_slash: false,
68};
69
70pub fn for_javascript(input: &str) -> String {
110 encode_js(input, &JS_UNIVERSAL)
111}
112
113pub fn write_javascript<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
117 write_js(out, input, &JS_UNIVERSAL)
118}
119
120pub fn for_javascript_attribute(input: &str) -> String {
138 encode_js(input, &JS_ATTRIBUTE)
139}
140
141pub fn write_javascript_attribute<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
145 write_js(out, input, &JS_ATTRIBUTE)
146}
147
148pub fn for_javascript_block(input: &str) -> String {
164 encode_js(input, &JS_BLOCK)
165}
166
167pub fn write_javascript_block<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
171 write_js(out, input, &JS_BLOCK)
172}
173
174pub fn for_javascript_source(input: &str) -> String {
196 encode_js(input, &JS_SOURCE)
197}
198
199pub fn write_javascript_source<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
203 write_js(out, input, &JS_SOURCE)
204}
205
206pub fn for_js_template(input: &str) -> String {
237 let mut out = String::with_capacity(input.len());
238 write_js_template(&mut out, input).expect("writing to string cannot fail");
239 out
240}
241
242pub fn write_js_template<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
246 encode_loop(
247 out,
248 input,
249 needs_js_template_encoding,
250 write_js_template_encoded,
251 )
252}
253
254fn needs_js_template_encoding(c: char) -> bool {
255 matches!(
256 c,
257 '\x00'..='\x1F' | '\\' | '`' | '$' | '/' | '\u{2028}' | '\u{2029}'
258 )
259}
260
261fn write_js_template_encoded<W: fmt::Write>(
262 out: &mut W,
263 c: char,
264 next: Option<char>,
265) -> fmt::Result {
266 match c {
267 '`' => out.write_str("\\`"),
269 '$' if next == Some('{') => out.write_str("\\$"),
270 '$' => out.write_char('$'),
271 '/' => out.write_str("\\/"),
272 c => {
274 write_js_shared_escape(out, c)?;
275 Ok(())
276 }
277 }
278}
279
280fn encode_js(input: &str, config: &JsConfig) -> String {
281 let mut out = String::with_capacity(input.len());
282 write_js(&mut out, input, config).expect("writing to string cannot fail");
283 out
284}
285
286fn write_js<W: fmt::Write>(out: &mut W, input: &str, config: &JsConfig) -> fmt::Result {
287 encode_loop(
288 out,
289 input,
290 |c| needs_js_encoding(c, config),
291 |out, c, _next| write_js_encoded(out, c, config),
292 )
293}
294
295fn needs_js_encoding(c: char, config: &JsConfig) -> bool {
296 match c {
297 '\x00'..='\x1F' | '\\' | '"' | '\'' | '\u{2028}' | '\u{2029}' => true,
298 '&' => config.encode_ampersand,
299 '/' => config.encode_slash,
300 _ => false,
301 }
302}
303
304fn write_js_encoded<W: fmt::Write>(out: &mut W, c: char, config: &JsConfig) -> fmt::Result {
305 match c {
306 '"' 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 c => {
315 write_js_shared_escape(out, c)?;
316 Ok(())
317 }
318 }
319}
320
321fn write_js_shared_escape<W: fmt::Write>(out: &mut W, c: char) -> Result<bool, fmt::Error> {
323 match c {
324 '\x08' => out.write_str("\\b")?,
325 '\t' => out.write_str("\\t")?,
326 '\n' => out.write_str("\\n")?,
327 '\x0B' => out.write_str("\\x0b")?,
328 '\x0C' => out.write_str("\\f")?,
329 '\r' => out.write_str("\\r")?,
330 '\\' => out.write_str("\\\\")?,
331 '\u{2028}' => out.write_str("\\u2028")?,
332 '\u{2029}' => out.write_str("\\u2029")?,
333 '\x00'..='\x1F' => write!(out, "\\x{:02x}", c as u32)?,
334 _ => return Ok(false),
335 }
336 Ok(true)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
346 fn js_no_encoding_needed() {
347 assert_eq!(for_javascript("hello world"), "hello world");
348 assert_eq!(for_javascript(""), "");
349 }
350
351 #[test]
352 fn js_encodes_quotes_as_hex() {
353 assert_eq!(for_javascript(r#"a"b"#), r"a\x22b");
354 assert_eq!(for_javascript("a'b"), r"a\x27b");
355 }
356
357 #[test]
358 fn js_encodes_backslash() {
359 assert_eq!(for_javascript(r"a\b"), r"a\\b");
360 }
361
362 #[test]
363 fn js_encodes_ampersand() {
364 assert_eq!(for_javascript("a&b"), r"a\x26b");
365 }
366
367 #[test]
368 fn js_encodes_slash() {
369 assert_eq!(for_javascript("</script>"), r"<\/script>");
370 }
371
372 #[test]
373 fn js_encodes_control_chars() {
374 assert_eq!(for_javascript("\x00"), r"\x00");
375 assert_eq!(for_javascript("\x08"), r"\b");
376 assert_eq!(for_javascript("\t"), r"\t");
377 assert_eq!(for_javascript("\n"), r"\n");
378 assert_eq!(for_javascript("\x0B"), r"\x0b");
379 assert_eq!(for_javascript("\x0C"), r"\f");
380 assert_eq!(for_javascript("\r"), r"\r");
381 assert_eq!(for_javascript("\x1F"), r"\x1f");
382 }
383
384 #[test]
385 fn js_encodes_line_separators() {
386 assert_eq!(for_javascript("\u{2028}"), r"\u2028");
387 assert_eq!(for_javascript("\u{2029}"), r"\u2029");
388 }
389
390 #[test]
391 fn js_preserves_non_ascii() {
392 assert_eq!(for_javascript("café"), "café");
393 assert_eq!(for_javascript("日本語"), "日本語");
394 }
395
396 #[test]
397 fn js_writer_variant() {
398 let mut out = String::new();
399 write_javascript(&mut out, "a'b").unwrap();
400 assert_eq!(out, r"a\x27b");
401 }
402
403 #[test]
406 fn js_attr_does_not_encode_slash() {
407 assert_eq!(for_javascript_attribute("a/b"), "a/b");
408 }
409
410 #[test]
411 fn js_attr_encodes_quotes_as_hex() {
412 assert_eq!(for_javascript_attribute("a'b"), r"a\x27b");
413 }
414
415 #[test]
416 fn js_attr_encodes_ampersand() {
417 assert_eq!(for_javascript_attribute("a&b"), r"a\x26b");
418 }
419
420 #[test]
423 fn js_block_uses_backslash_quotes() {
424 assert_eq!(for_javascript_block(r#"a"b"#), r#"a\"b"#);
425 assert_eq!(for_javascript_block("a'b"), r"a\'b");
426 }
427
428 #[test]
429 fn js_block_encodes_slash() {
430 assert_eq!(for_javascript_block("a/b"), r"a\/b");
431 }
432
433 #[test]
434 fn js_block_encodes_ampersand() {
435 assert_eq!(for_javascript_block("a&b"), r"a\x26b");
436 }
437
438 #[test]
441 fn js_source_uses_backslash_quotes() {
442 assert_eq!(for_javascript_source(r#"a"b"#), r#"a\"b"#);
443 assert_eq!(for_javascript_source("a'b"), r"a\'b");
444 }
445
446 #[test]
447 fn js_source_does_not_encode_slash_or_ampersand() {
448 assert_eq!(for_javascript_source("a/b&c"), "a/b&c");
449 }
450
451 #[test]
452 fn js_source_encodes_line_separators() {
453 assert_eq!(for_javascript_source("\u{2028}"), r"\u2028");
454 }
455
456 #[test]
459 fn js_template_no_encoding_needed() {
460 assert_eq!(for_js_template("hello world"), "hello world");
461 assert_eq!(for_js_template(""), "");
462 }
463
464 #[test]
465 fn js_template_encodes_backtick() {
466 assert_eq!(for_js_template("hello `world`"), r"hello \`world\`");
467 assert_eq!(for_js_template("`"), r"\`");
468 }
469
470 #[test]
471 fn js_template_encodes_interpolation() {
472 assert_eq!(for_js_template("${alert(1)}"), r"\${alert(1)}");
473 assert_eq!(for_js_template("a${b}c"), r"a\${b}c");
474 assert_eq!(for_js_template("${a}${b}"), r"\${a}\${b}");
475 }
476
477 #[test]
478 fn js_template_dollar_without_brace_passes_through() {
479 assert_eq!(for_js_template("a $ b"), "a $ b");
480 assert_eq!(for_js_template("$100"), "$100");
481 assert_eq!(for_js_template("a$"), "a$");
482 }
483
484 #[test]
485 fn js_template_encodes_backslash() {
486 assert_eq!(for_js_template(r"a\b"), r"a\\b");
487 }
488
489 #[test]
490 fn js_template_encodes_slash() {
491 assert_eq!(for_js_template("</script>"), r"<\/script>");
492 }
493
494 #[test]
495 fn js_template_does_not_encode_quotes() {
496 assert_eq!(for_js_template(r#"a"b"#), r#"a"b"#);
497 assert_eq!(for_js_template("a'b"), "a'b");
498 }
499
500 #[test]
501 fn js_template_encodes_control_chars() {
502 assert_eq!(for_js_template("\x00"), r"\x00");
503 assert_eq!(for_js_template("\x08"), r"\b");
504 assert_eq!(for_js_template("\t"), r"\t");
505 assert_eq!(for_js_template("\n"), r"\n");
506 assert_eq!(for_js_template("\x0B"), r"\x0b");
507 assert_eq!(for_js_template("\x0C"), r"\f");
508 assert_eq!(for_js_template("\r"), r"\r");
509 assert_eq!(for_js_template("\x1F"), r"\x1f");
510 }
511
512 #[test]
513 fn js_template_encodes_line_separators() {
514 assert_eq!(for_js_template("\u{2028}"), r"\u2028");
515 assert_eq!(for_js_template("\u{2029}"), r"\u2029");
516 }
517
518 #[test]
519 fn js_template_preserves_non_ascii() {
520 assert_eq!(for_js_template("café"), "café");
521 assert_eq!(for_js_template("日本語"), "日本語");
522 assert_eq!(for_js_template("😀"), "😀");
523 }
524
525 #[test]
526 fn js_template_mixed_input() {
527 assert_eq!(
528 for_js_template("`Hello ${name}`, welcome\\n"),
529 r"\`Hello \${name}\`, welcome\\n"
530 );
531 }
532
533 #[test]
534 fn js_template_writer_variant() {
535 let input = "`test` ${x} café";
536 let string_result = for_js_template(input);
537 let mut writer_result = String::new();
538 write_js_template(&mut writer_result, input).unwrap();
539 assert_eq!(string_result, writer_result);
540 }
541
542 #[test]
545 fn shared_escape_handles_shared_chars() {
546 let cases = [
547 ('\x08', r"\b"),
548 ('\t', r"\t"),
549 ('\n', r"\n"),
550 ('\x0B', r"\x0b"),
551 ('\x0C', r"\f"),
552 ('\r', r"\r"),
553 ('\\', r"\\"),
554 ('\x00', r"\x00"),
555 ('\x1F', r"\x1f"),
556 ('\u{2028}', r"\u2028"),
557 ('\u{2029}', r"\u2029"),
558 ];
559 for (c, expected) in cases {
560 let mut out = String::new();
561 assert_eq!(write_js_shared_escape(&mut out, c), Ok(true));
562 assert_eq!(out, expected);
563 }
564 }
565
566 #[test]
567 fn shared_escape_declines_format_specific_chars() {
568 for c in ['a', '"', '\'', '&', '/', '`', '$', 'é'] {
570 let mut out = String::new();
571 assert_eq!(write_js_shared_escape(&mut out, c), Ok(false));
572 assert!(out.is_empty());
573 }
574 }
575}