File size: 2,454 Bytes
cfb152f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//! ECMAScript string operations that the upstream lines rely on, reproduced on Rust `&str`.
//!
//! JavaScript strings are UTF-16: `slice` and `charCodeAt` count UTF-16 code units, `trim` strips the
//! ECMAScript WhiteSpace and LineTerminator code points, and `JSON.stringify` of a string returns its
//! quoted literal. Node's `createHash().update(string)` and `appendFileSync` write a string as UTF-8,
//! encoding an unpaired surrogate as U+FFFD.

use std::fmt::Write;

/// `JSON.stringify(s)` for a string value (ECMA-262 QuoteJSONString). It quotes one string and parses
/// nothing; a Rust `&str` holds no unpaired surrogate, so the `\uDXXX` branch of the algorithm never applies.
pub fn json_quote(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\u{8}' => out.push_str("\\b"),
            '\u{c}' => out.push_str("\\f"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if u32::from(c) < 0x20 => {
                let _ = write!(out, "\\u{:04x}", u32::from(c));
            }
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// `String(s).slice(0, n)` as Node writes it to UTF-8: the first `n` UTF-16 code units. When the cut
/// splits a surrogate pair, the kept high surrogate is written as U+FFFD.
pub fn utf16_prefix(s: &str, n: usize) -> String {
    let mut out = String::new();
    let mut units = 0usize;
    for c in s.chars() {
        let width = c.len_utf16();
        if units + width > n {
            if units < n {
                out.push('\u{FFFD}');
            }
            break;
        }
        out.push(c);
        units += width;
    }
    out
}

/// ECMAScript WhiteSpace (TAB, VT, FF, ZWNBSP, category Zs) and LineTerminator (LF, CR, LS, PS) code
/// points, the set `String.prototype.trim` strips.
pub fn is_js_whitespace(c: char) -> bool {
    const SINGLES: [char; 14] = [
        '\u{9}', '\u{A}', '\u{B}', '\u{C}', '\u{D}', '\u{20}', '\u{A0}', '\u{1680}', '\u{2028}',
        '\u{2029}', '\u{202F}', '\u{205F}', '\u{3000}', '\u{FEFF}',
    ];
    SINGLES.contains(&c) || ('\u{2000}'..='\u{200A}').contains(&c)
}

/// `String(s).trim()`.
pub fn js_trim(s: &str) -> &str {
    s.trim_matches(is_js_whitespace)
}