Skip to main content

sim_lib_lang_javascript/
text.rs

1//! UTF-16 code-unit face for ECMAScript strings.
2
3/// Error crossing from the code-unit face to canonical scalar SIM text.
4#[derive(Clone, Debug, Eq, PartialEq)]
5pub enum JavascriptTextError {
6    /// The unit sequence contains an unpaired surrogate.
7    LoneSurrogate {
8        /// Code-unit index of the first invalid surrogate.
9        index: usize,
10        /// Invalid code unit.
11        unit: u16,
12    },
13}
14
15/// An ECMAScript String as exact UTF-16 code units.
16///
17/// This face admits lone surrogates. Canonical SIM text remains scalar Unicode;
18/// conversion to it is explicit and fails rather than replacing data.
19#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub struct JavascriptCodeUnitString {
21    units: Vec<u16>,
22}
23impl JavascriptCodeUnitString {
24    /// Encode canonical scalar text into the JavaScript face.
25    pub fn from_scalar(text: &str) -> Self {
26        Self {
27            units: text.encode_utf16().collect(),
28        }
29    }
30    /// Preserve an exact sequence including lone surrogates.
31    pub fn from_code_units(units: Vec<u16>) -> Self {
32        Self { units }
33    }
34    /// ECMAScript `length` in code units.
35    pub fn len(&self) -> usize {
36        self.units.len()
37    }
38    /// Whether the string has no code units.
39    pub fn is_empty(&self) -> bool {
40        self.units.is_empty()
41    }
42    /// Index one code unit.
43    pub fn code_unit_at(&self, index: usize) -> Option<u16> {
44        self.units.get(index).copied()
45    }
46    /// Slice by code-unit indices, clamped as `String.prototype.slice` does for nonnegative indices.
47    pub fn slice(&self, start: usize, end: usize) -> Self {
48        let start = start.min(self.len());
49        let end = end.max(start).min(self.len());
50        Self::from_code_units(self.units[start..end].to_vec())
51    }
52    /// Iterate exact code units (the indexing face).
53    pub fn code_units(&self) -> impl Iterator<Item = u16> + '_ {
54        self.units.iter().copied()
55    }
56    /// Iterate ECMAScript string iterator chunks: paired surrogates together, lone units alone.
57    pub fn iter_strings(&self) -> JavascriptStringIterator<'_> {
58        JavascriptStringIterator {
59            units: &self.units,
60            at: 0,
61        }
62    }
63    /// Convert only well-formed UTF-16 to canonical scalar SIM text.
64    pub fn to_scalar(&self) -> Result<String, JavascriptTextError> {
65        String::from_utf16(&self.units).map_err(|_| {
66            let (index, unit) =
67                first_lone(&self.units).expect("invalid UTF-16 has a lone surrogate");
68            JavascriptTextError::LoneSurrogate { index, unit }
69        })
70    }
71}
72/// Iterator over ECMAScript code-point chunks represented as exact code-unit strings.
73pub struct JavascriptStringIterator<'a> {
74    units: &'a [u16],
75    at: usize,
76}
77impl Iterator for JavascriptStringIterator<'_> {
78    type Item = JavascriptCodeUnitString;
79    fn next(&mut self) -> Option<Self::Item> {
80        let first = *self.units.get(self.at)?;
81        let width = if (0xd800..=0xdbff).contains(&first)
82            && self
83                .units
84                .get(self.at + 1)
85                .is_some_and(|u| (0xdc00..=0xdfff).contains(u))
86        {
87            2
88        } else {
89            1
90        };
91        let out = JavascriptCodeUnitString::from_code_units(
92            self.units[self.at..self.at + width].to_vec(),
93        );
94        self.at += width;
95        Some(out)
96    }
97}
98fn first_lone(units: &[u16]) -> Option<(usize, u16)> {
99    let mut i = 0;
100    while i < units.len() {
101        let u = units[i];
102        if (0xd800..=0xdbff).contains(&u) {
103            if units
104                .get(i + 1)
105                .is_some_and(|v| (0xdc00..=0xdfff).contains(v))
106            {
107                i += 2;
108                continue;
109            }
110            return Some((i, u));
111        }
112        if (0xdc00..=0xdfff).contains(&u) {
113            return Some((i, u));
114        }
115        i += 1;
116    }
117    None
118}
119
120#[cfg(test)]
121mod law_fixtures {
122    use super::*;
123    #[test]
124    fn length_index_slice_and_code_unit_iteration_are_utf16() {
125        let s = JavascriptCodeUnitString::from_scalar("A😀B");
126        assert_eq!(s.len(), 4);
127        assert_eq!(s.code_unit_at(1), Some(0xd83d));
128        assert_eq!(
129            s.slice(1, 3).code_units().collect::<Vec<_>>(),
130            vec![0xd83d, 0xde00]
131        );
132        assert_eq!(s.code_units().count(), 4);
133    }
134    #[test]
135    fn scalar_conversion_and_paired_iteration_are_exact() {
136        let s = JavascriptCodeUnitString::from_code_units(vec![0xd83d, 0xde00]);
137        assert_eq!(s.to_scalar().unwrap(), "😀");
138        assert_eq!(s.iter_strings().next().unwrap().len(), 2);
139    }
140    #[test]
141    fn lone_high_surrogate_is_preserved_and_rejected_by_scalar_face() {
142        let s = JavascriptCodeUnitString::from_code_units(vec![0xd800]);
143        assert_eq!(s.code_unit_at(0), Some(0xd800));
144        assert_eq!(
145            s.iter_strings()
146                .next()
147                .unwrap()
148                .code_units()
149                .collect::<Vec<_>>(),
150            vec![0xd800]
151        );
152        assert_eq!(
153            s.to_scalar(),
154            Err(JavascriptTextError::LoneSurrogate {
155                index: 0,
156                unit: 0xd800
157            })
158        );
159    }
160    #[test]
161    fn lone_low_surrogate_is_preserved_and_rejected_by_scalar_face() {
162        let s = JavascriptCodeUnitString::from_code_units(vec![0xdc00]);
163        assert_eq!(s.slice(0, 1), s);
164        assert_eq!(
165            s.to_scalar(),
166            Err(JavascriptTextError::LoneSurrogate {
167                index: 0,
168                unit: 0xdc00
169            })
170        );
171    }
172}