Skip to main content

quickjs_oxide/source/
unicode.rs

1//! The checksum-pinned Unicode 17 tables of the `quickjs-oxide-regexp` crate at their
2//! historical path. Case conversion and normalization keep `JsString`
3//! wrappers here: they hand the crate the string's flat code units and wrap
4//! its result in the storage the crate chose.
5
6pub use oxide_regexp::unicode::{is_id_continue, is_id_start, property};
7
8use crate::engine::value::{JsString, JsStringError};
9use oxide_regexp::unicode::{CodeUnitBuf, StringError};
10
11const _: () = assert!(JsString::MAX_LEN == oxide_regexp::unicode::MAX_STRING_LEN);
12
13/// Run `latin1` or `utf16` over the flat code units of `input`. A rope is
14/// read into a temporary buffer rather than linearized, so its representation
15/// does not change.
16fn with_code_units<R>(
17    input: &JsString,
18    latin1: impl FnOnce(&[u8]) -> R,
19    utf16: impl FnOnce(&[u16]) -> R,
20) -> R {
21    if let Some(units) = input.flat_latin1() {
22        latin1(units)
23    } else if let Some(units) = input.flat_utf16() {
24        utf16(units)
25    } else {
26        utf16(&input.utf16_units().collect::<Vec<_>>())
27    }
28}
29
30fn into_js_string(output: Result<CodeUnitBuf, StringError>) -> Result<JsString, JsStringError> {
31    match output {
32        Ok(CodeUnitBuf::Latin1(units)) => Ok(JsString::from_owned_latin1(units)),
33        Ok(CodeUnitBuf::Utf16(units)) => Ok(JsString::from_owned_utf16(units)),
34        Err(error) => Err(into_js_string_error(error)),
35    }
36}
37
38const fn into_js_string_error(error: StringError) -> JsStringError {
39    match error {
40        StringError::TooLong => JsStringError::TooLong,
41        StringError::OutOfMemory => JsStringError::OutOfMemory,
42    }
43}
44
45pub mod case {
46    //! Case conversion over engine strings; see `oxide_regexp::unicode::case`.
47
48    pub use oxide_regexp::unicode::case::*;
49
50    use crate::engine::value::{JsString, JsStringError};
51
52    /// Convert one raw UTF-16 string with QuickJS 2026-06-04's Unicode 17
53    /// case tables and contextual lowercase rule. An empty input is returned
54    /// as is, without reserving a buffer.
55    pub fn convert_case_with_limit(
56        input: &JsString,
57        upper: bool,
58        max_len: usize,
59    ) -> Result<JsString, JsStringError> {
60        if input.is_empty() {
61            return Ok(input.clone());
62        }
63        super::into_js_string(super::with_code_units(
64            input,
65            |units| oxide_regexp::unicode::case::convert_case_with_limit(units, upper, max_len),
66            |units| oxide_regexp::unicode::case::convert_case_with_limit(units, upper, max_len),
67        ))
68    }
69}
70
71pub mod normalize {
72    //! Normalization over engine strings; see
73    //! `oxide_regexp::unicode::normalize`.
74
75    pub use oxide_regexp::unicode::normalize::*;
76
77    use crate::engine::value::{JsString, JsStringError};
78
79    /// Normalize to code points; lone surrogates are kept.
80    pub fn normalize_code_points(
81        input: &JsString,
82        form: NormalizationForm,
83    ) -> Result<Vec<u32>, JsStringError> {
84        super::with_code_units(
85            input,
86            |units| oxide_regexp::unicode::normalize::normalize_code_points(units, form),
87            |units| oxide_regexp::unicode::normalize::normalize_code_points(units, form),
88        )
89        .map_err(super::into_js_string_error)
90    }
91
92    /// [`normalize`] with a UTF-16 output length limit.
93    pub fn normalize_with_limit(
94        input: &JsString,
95        form: NormalizationForm,
96        max_len: usize,
97    ) -> Result<JsString, JsStringError> {
98        super::into_js_string(super::with_code_units(
99            input,
100            |units| oxide_regexp::unicode::normalize::normalize_with_limit(units, form, max_len),
101            |units| oxide_regexp::unicode::normalize::normalize_with_limit(units, form, max_len),
102        ))
103    }
104
105    /// Normalize an ECMAScript UTF-16 string with QuickJS 2026-06-04's
106    /// Unicode 17 data. Valid surrogate pairs are decoded and re-encoded;
107    /// lone surrogates are preserved as lone code units.
108    pub fn normalize(input: &JsString, form: NormalizationForm) -> Result<JsString, JsStringError> {
109        normalize_with_limit(input, form, JsString::MAX_LEN)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::case::{convert_case_with_limit, fail_next_case_reservation_for_test};
116    use super::normalize::{NormalizationForm, normalize, normalize_code_points};
117    use crate::engine::value::{JsString, JsStringError};
118
119    #[test]
120    fn empty_case_conversion_returns_the_input_without_reserving() {
121        let empty = JsString::from_static("");
122        fail_next_case_reservation_for_test();
123        let converted_empty = convert_case_with_limit(&empty, true, 0).unwrap();
124        assert!(converted_empty.same_representation(&empty));
125
126        // The empty fast path did not consume the injected allocation failure.
127        let ascii = JsString::from_static("a");
128        assert_eq!(
129            convert_case_with_limit(&ascii, true, 1),
130            Err(JsStringError::OutOfMemory)
131        );
132    }
133
134    #[test]
135    fn wrappers_read_latin1_utf16_and_rope_strings() {
136        let latin1 = JsString::try_from_utf8("ÿ").unwrap();
137        assert!(!latin1.is_wide());
138        let upper = convert_case_with_limit(&latin1, true, JsString::MAX_LEN).unwrap();
139        assert_eq!(upper.to_utf8_lossy(), "Ÿ");
140        assert!(upper.is_wide());
141
142        let wide = JsString::try_from_utf8("Ÿ").unwrap();
143        let lower = convert_case_with_limit(&wide, false, JsString::MAX_LEN).unwrap();
144        assert_eq!(lower.to_utf8_lossy(), "ÿ");
145        assert!(!lower.is_wide());
146
147        let rope = JsString::try_from_utf8(&"e".repeat(600))
148            .unwrap()
149            .try_concat(&JsString::try_from_utf8("\u{301}").unwrap())
150            .unwrap();
151        let normalized = normalize(&rope, NormalizationForm::Nfc).unwrap();
152        assert_eq!(normalized.len(), 600);
153        assert_eq!(normalized.code_unit_at(599), Some(0xe9));
154        assert_eq!(
155            normalize_code_points(&rope, NormalizationForm::Nfd)
156                .unwrap()
157                .len(),
158            601
159        );
160    }
161}