submilli_engine/runtime/prelude/string/
transforms.rs1use std::sync::OnceLock;
3
4use regex_syntax::hir::{Class, ClassUnicode, HirKind};
5
6use super::{MAX_RESULT_UNITS, Str};
7use crate::runtime::host::{fatal_host_error, range_error};
8
9pub fn to_upper_case(input: &Str) -> wasmtime::Result<Str> {
10 let mut output = output_buffer(input.len().saturating_mul(3).min(MAX_RESULT_UNITS))?;
11 for decoded in char::decode_utf16(input.units().iter().copied()) {
12 match decoded {
13 Ok(ch) => {
14 for mapped in ch.to_uppercase() {
15 push_char(&mut output, mapped)?;
16 }
17 }
18 Err(surrogate) => push_units(&mut output, &[surrogate.unpaired_surrogate()])?,
19 }
20 }
21 Ok(Str::from_units(output))
22}
23
24pub fn to_lower_case(input: &Str) -> wasmtime::Result<Str> {
25 let properties = case_properties()?;
26 let mut output = output_buffer(input.len().saturating_mul(3).min(MAX_RESULT_UNITS))?;
27 let mut preceded_by_cased = false;
28 let mut decoded = char::decode_utf16(input.units().iter().copied());
29 while let Some(unit) = decoded.next() {
30 let ch = match unit {
31 Ok(ch) => ch,
32 Err(surrogate) => {
33 push_units(&mut output, &[surrogate.unpaired_surrogate()])?;
34 preceded_by_cased = false;
35 continue;
36 }
37 };
38 if ch == 'Σ' && preceded_by_cased && !followed_by_cased(decoded.clone(), properties) {
39 push_units(&mut output, &['ς' as u16])?;
40 } else {
41 for mapped in ch.to_lowercase() {
42 push_char(&mut output, mapped)?;
43 }
44 }
45 if !contains(&properties.ignorable, ch) {
47 preceded_by_cased = contains(&properties.cased, ch);
48 }
49 }
50 Ok(Str::from_units(output))
51}
52
53fn followed_by_cased(
54 following: impl Iterator<Item = std::result::Result<char, std::char::DecodeUtf16Error>>,
55 properties: &CaseProperties,
56) -> bool {
57 for decoded in following {
60 let Ok(ch) = decoded else {
61 return false;
62 };
63 if !contains(&properties.ignorable, ch) {
64 return contains(&properties.cased, ch);
65 }
66 }
67 false
68}
69
70fn push_char(output: &mut Vec<u16>, ch: char) -> wasmtime::Result<()> {
71 let mut units = [0_u16; 2];
72 push_units(output, ch.encode_utf16(&mut units))
73}
74
75fn push_units(output: &mut Vec<u16>, units: &[u16]) -> wasmtime::Result<()> {
76 if units.len() > output.capacity().saturating_sub(output.len()) {
77 return Err(range_error("Invalid string length"));
78 }
79 output.extend_from_slice(units);
80 Ok(())
81}
82
83struct CaseProperties {
84 cased: ClassUnicode,
85 ignorable: ClassUnicode,
86}
87
88pub(super) fn prepare_case_properties() -> wasmtime::Result<()> {
89 case_properties().map(|_| ())
90}
91
92fn case_properties() -> wasmtime::Result<&'static CaseProperties> {
93 static PROPERTIES: OnceLock<std::result::Result<CaseProperties, &'static str>> =
94 OnceLock::new();
95 PROPERTIES
96 .get_or_init(|| {
97 Ok(CaseProperties {
98 cased: property_class(r"\p{Cased}")?,
99 ignorable: property_class(r"\p{Case_Ignorable}")?,
100 })
101 })
102 .as_ref()
103 .map_err(|message| fatal_host_error(*message))
104}
105
106fn property_class(pattern: &str) -> std::result::Result<ClassUnicode, &'static str> {
107 let hir = regex_syntax::Parser::new()
108 .parse(pattern)
109 .map_err(|_| "could not parse built-in Unicode case property")?;
110 match hir.into_kind() {
111 HirKind::Class(Class::Unicode(class)) => Ok(class),
112 _ => Err("built-in Unicode case property is not a character class"),
113 }
114}
115
116fn contains(class: &ClassUnicode, ch: char) -> bool {
117 let ranges = class.ranges();
118 let index = ranges.partition_point(|range| range.end() < ch);
119 ranges.get(index).is_some_and(|range| range.start() <= ch)
120}
121
122pub fn trim(input: &Str) -> wasmtime::Result<Str> {
123 trim_boundaries(input, true, true)
124}
125
126pub fn trim_start(input: &Str) -> wasmtime::Result<Str> {
127 trim_boundaries(input, true, false)
128}
129
130pub fn trim_end(input: &Str) -> wasmtime::Result<Str> {
131 trim_boundaries(input, false, true)
132}
133
134fn trim_boundaries(input: &Str, leading: bool, trailing: bool) -> wasmtime::Result<Str> {
135 let units = input.units();
136 let start = if leading {
137 units
138 .iter()
139 .position(|unit| !whitespace(*unit))
140 .unwrap_or(units.len())
141 } else {
142 0
143 };
144 let end = if trailing {
145 units
146 .get(start..)
147 .and_then(|rest| rest.iter().rposition(|unit| !whitespace(*unit)))
148 .map_or(start, |index| start + index + 1)
149 } else {
150 units.len()
151 };
152 let retained = units
153 .get(start..end)
154 .ok_or_else(|| fatal_host_error("invalid trim boundaries"))?;
155 let mut output = output_buffer(retained.len())?;
156 output.extend_from_slice(retained);
157 Ok(Str::from_units(output))
158}
159
160fn whitespace(unit: u16) -> bool {
161 matches!(unit, 0x0009..=0x000D | 0x0020 | 0x00A0 | 0x1680 | 0x2000..=0x200A | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 | 0xFEFF)
162}
163
164fn output_buffer(capacity: usize) -> wasmtime::Result<Vec<u16>> {
165 if capacity > MAX_RESULT_UNITS {
166 return Err(range_error("Invalid string length"));
167 }
168 let mut output = Vec::new();
169 output
170 .try_reserve_exact(capacity)
171 .map_err(fatal_host_error)?;
172 Ok(output)
173}