1use sim_text::{CodeUnitOffset, CodeUnitRange, CodeUnitString, CodeUnitStringError};
4
5#[derive(Clone, Debug, Eq, PartialEq)]
7pub enum JavascriptTextError {
8 LoneSurrogate {
10 index: usize,
12 unit: u16,
14 },
15}
16
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
22pub struct JavascriptCodeUnitString(CodeUnitString);
23impl JavascriptCodeUnitString {
24 pub fn from_scalar(text: &str) -> Self {
26 Self(CodeUnitString::from_scalar(text))
27 }
28 pub fn from_code_units(units: Vec<u16>) -> Self {
30 Self(CodeUnitString::from_code_units(units))
31 }
32 pub fn len(&self) -> usize {
34 self.0.len()
35 }
36 pub fn is_empty(&self) -> bool {
38 self.0.is_empty()
39 }
40 pub fn code_unit_at(&self, index: usize) -> Option<u16> {
42 self.0.code_unit_at(CodeUnitOffset::new(index))
43 }
44 pub fn slice(&self, start: usize, end: usize) -> Self {
46 Self(self.0.slice(CodeUnitRange::new(
47 CodeUnitOffset::new(start),
48 CodeUnitOffset::new(end),
49 )))
50 }
51 pub fn code_units(&self) -> impl Iterator<Item = u16> + '_ {
53 self.0.code_units()
54 }
55 pub fn iter_strings(&self) -> JavascriptStringIterator<'_> {
57 JavascriptStringIterator(self.0.iter_code_points())
58 }
59 pub fn to_scalar(&self) -> Result<String, JavascriptTextError> {
61 self.0.to_scalar().map_err(|error| match error {
62 CodeUnitStringError::LoneSurrogate(invalid) => JavascriptTextError::LoneSurrogate {
63 index: invalid.offset.get(),
64 unit: invalid.unit,
65 },
66 CodeUnitStringError::TooLong { .. } => {
67 unreachable!("an existing code-unit string already satisfies the allocation limit")
68 }
69 })
70 }
71}
72pub struct JavascriptStringIterator<'a>(sim_text::CodePointIter<'a>);
74impl Iterator for JavascriptStringIterator<'_> {
75 type Item = JavascriptCodeUnitString;
76 fn next(&mut self) -> Option<Self::Item> {
77 self.0.next().map(JavascriptCodeUnitString)
78 }
79}
80
81#[cfg(test)]
82mod law_fixtures {
83 use super::*;
84 use std::sync::Arc;
85
86 use sim_kernel::{Cx, Datum, DefaultFactory, NoopEvalPolicy, Ref, Symbol};
87 use sim_lib_standard_core::{
88 BoundedLane, CanonicalObservation, CanonicalOutcome, CharacterizationCapture,
89 ScenarioLimits, ScenarioObservationLane, ScenarioSpec, publish_characterization_capture,
90 };
91
92 fn units_datum(units: impl IntoIterator<Item = u16>) -> Datum {
93 Datum::Vector(
94 units
95 .into_iter()
96 .map(|unit| Datum::String(format!("{unit:04x}")))
97 .collect(),
98 )
99 }
100
101 fn characterized_case(name: &str, value: JavascriptCodeUnitString) -> Datum {
102 Datum::Node {
103 tag: Symbol::qualified("javascript-utf16", "case/v1"),
104 fields: vec![
105 (Symbol::new("name"), Datum::String(name.to_owned())),
106 (
107 Symbol::new("length"),
108 Datum::String(value.len().to_string()),
109 ),
110 (Symbol::new("code-units"), units_datum(value.code_units())),
111 (
112 Symbol::new("string-iterator"),
113 Datum::Vector(
114 value
115 .iter_strings()
116 .map(|chunk| units_datum(chunk.code_units()))
117 .collect(),
118 ),
119 ),
120 ],
121 }
122 }
123
124 fn characterization_capture() -> (ScenarioSpec, CharacterizationCapture) {
125 let astral = JavascriptCodeUnitString::from_scalar("😀");
126 let cases = vec![
127 characterized_case("empty", JavascriptCodeUnitString::default()),
128 characterized_case("astral-pair", astral.clone()),
129 characterized_case(
130 "lone-high-start",
131 JavascriptCodeUnitString::from_code_units(vec![0xd800, 0x0061]),
132 ),
133 characterized_case(
134 "lone-high-middle",
135 JavascriptCodeUnitString::from_code_units(vec![0x0061, 0xd800, 0x0062]),
136 ),
137 characterized_case(
138 "lone-high-end",
139 JavascriptCodeUnitString::from_code_units(vec![0x0061, 0xd800]),
140 ),
141 characterized_case(
142 "lone-low",
143 JavascriptCodeUnitString::from_code_units(vec![0xdc00]),
144 ),
145 characterized_case("nul", JavascriptCodeUnitString::from_scalar("\0")),
146 characterized_case("slice-across-pair-high", astral.slice(0, 1)),
147 characterized_case("slice-across-pair-low", astral.slice(1, 2)),
148 ];
149 let scenario = ScenarioSpec::new(
150 Symbol::qualified("javascript", "characterize-utf16/v1"),
151 Symbol::qualified("javascript", "text-current/v1"),
152 )
153 .with_limits(ScenarioLimits::new(0, 1))
154 .observing(ScenarioObservationLane::ValueOrFailure);
155 let capture = CharacterizationCapture::new(
156 Symbol::qualified("javascript", "utf16-code-units/v1"),
157 CanonicalObservation {
158 outcome: Some(CanonicalOutcome::Success(Datum::Vector(cases))),
159 events: BoundedLane::Absent,
160 receipts: BoundedLane::Absent,
161 browse: BoundedLane::Absent,
162 },
163 );
164 (scenario, capture)
165 }
166
167 fn test_cx() -> Cx {
168 Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory))
169 }
170
171 #[test]
172 fn characterize_1_current_code_unit_behavior_has_stable_content_identity() {
173 let (scenario, capture) = characterization_capture();
174 let first = publish_characterization_capture(&mut test_cx(), &scenario, &capture).unwrap();
175 let second = publish_characterization_capture(&mut test_cx(), &scenario, &capture).unwrap();
176 assert!(matches!(first, Ref::Content(_)));
177 assert_eq!(first, second);
178 }
179
180 #[test]
181 fn length_index_slice_and_code_unit_iteration_are_utf16() {
182 let s = JavascriptCodeUnitString::from_scalar("A😀B");
183 assert_eq!(s.len(), 4);
184 assert_eq!(s.code_unit_at(1), Some(0xd83d));
185 assert_eq!(
186 s.slice(1, 3).code_units().collect::<Vec<_>>(),
187 vec![0xd83d, 0xde00]
188 );
189 assert_eq!(s.code_units().count(), 4);
190 }
191 #[test]
192 fn scalar_conversion_and_paired_iteration_are_exact() {
193 let s = JavascriptCodeUnitString::from_code_units(vec![0xd83d, 0xde00]);
194 assert_eq!(s.to_scalar().unwrap(), "😀");
195 assert_eq!(s.iter_strings().next().unwrap().len(), 2);
196 }
197 #[test]
198 fn lone_high_surrogate_is_preserved_and_rejected_by_scalar_face() {
199 let s = JavascriptCodeUnitString::from_code_units(vec![0xd800]);
200 assert_eq!(s.code_unit_at(0), Some(0xd800));
201 assert_eq!(
202 s.iter_strings()
203 .next()
204 .unwrap()
205 .code_units()
206 .collect::<Vec<_>>(),
207 vec![0xd800]
208 );
209 assert_eq!(
210 s.to_scalar(),
211 Err(JavascriptTextError::LoneSurrogate {
212 index: 0,
213 unit: 0xd800
214 })
215 );
216 }
217 #[test]
218 fn lone_low_surrogate_is_preserved_and_rejected_by_scalar_face() {
219 let s = JavascriptCodeUnitString::from_code_units(vec![0xdc00]);
220 assert_eq!(s.slice(0, 1), s);
221 assert_eq!(
222 s.to_scalar(),
223 Err(JavascriptTextError::LoneSurrogate {
224 index: 0,
225 unit: 0xdc00
226 })
227 );
228 }
229
230 #[test]
231 fn scalar_error_preserves_javascript_code_unit_position() {
232 let s = JavascriptCodeUnitString::from_code_units(vec![0x0061, 0xd800, 0x0062]);
233 assert_eq!(
234 s.to_scalar(),
235 Err(JavascriptTextError::LoneSurrogate {
236 index: 1,
237 unit: 0xd800,
238 })
239 );
240 }
241}