@tishlang/tish-format 1.0.12 β†’ 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,647 @@
1
+ //! String builtin methods.
2
+ //!
3
+ //! All indices use character (Unicode scalar) positions for consistency with
4
+ //! JavaScript, matching .length and .charAt(). Byte offsets are never exposed.
5
+
6
+ use crate::helpers::normalize_index;
7
+ use std::sync::Arc;
8
+ use tishlang_core::Value;
9
+ use tishlang_core::VmRef;
10
+
11
+ /// Byte offset -> character index.
12
+ fn byte_to_char_index(s: &str, byte_offset: usize) -> usize {
13
+ s.char_indices()
14
+ .take_while(|(i, _)| *i < byte_offset)
15
+ .count()
16
+ }
17
+
18
+ /// Character index -> byte offset.
19
+ fn char_to_byte_offset(s: &str, char_index: usize) -> usize {
20
+ s.char_indices()
21
+ .nth(char_index)
22
+ .map(|(i, _)| i)
23
+ .unwrap_or(s.len())
24
+ }
25
+
26
+ /// Create a new string Value from a string slice.
27
+ pub fn from_str(s: &str) -> Value {
28
+ Value::String(Arc::from(s))
29
+ }
30
+
31
+ /// Get the length of a string (character count).
32
+ pub fn len(s: &Value) -> Option<usize> {
33
+ match s {
34
+ Value::String(str) => Some(str.chars().count()),
35
+ _ => None,
36
+ }
37
+ }
38
+
39
+ /// JS `ToIntegerOrInfinity` then clamp for `lastIndexOf` `position` (character index).
40
+ fn last_index_of_position_to_start(position: &Value, len: usize) -> usize {
41
+ let pos = match position {
42
+ Value::Null => 0.0,
43
+ Value::Bool(false) => 0.0,
44
+ Value::Bool(true) => 1.0,
45
+ Value::Number(n) => {
46
+ if n.is_nan() || *n == 0.0 {
47
+ 0.0
48
+ } else if n.is_infinite() {
49
+ *n
50
+ } else {
51
+ n.trunc()
52
+ }
53
+ }
54
+ _ => 0.0,
55
+ };
56
+ if pos.is_infinite() {
57
+ if pos > 0.0 {
58
+ len
59
+ } else {
60
+ 0
61
+ }
62
+ } else if pos <= 0.0 {
63
+ 0
64
+ } else {
65
+ (pos as usize).min(len)
66
+ }
67
+ }
68
+
69
+ /// Character index of last occurrence of `needle` in `haystack`, or `-1`.
70
+ /// `position` is JS `lastIndexOf`'s second argument: use `Number(INFINITY)` when omitted;
71
+ /// `Null` is JS `null` β†’ 0. Indices are Unicode scalar positions (same as `.length` / `indexOf`).
72
+ pub fn last_index_of_str(haystack: &str, needle: &str, position: &Value) -> Value {
73
+ let len = haystack.chars().count();
74
+ let start = last_index_of_position_to_start(position, len);
75
+ let hay: Vec<char> = haystack.chars().collect();
76
+ let needle_chars: Vec<char> = needle.chars().collect();
77
+ let search_len = needle_chars.len();
78
+ if search_len == 0 {
79
+ return Value::Number(start as f64);
80
+ }
81
+ if search_len > len {
82
+ return Value::Number(-1.0);
83
+ }
84
+ // Match must fit in the string and end at or before `start` (ECMA `lastIndexOf` position).
85
+ if start + 1 < search_len {
86
+ return Value::Number(-1.0);
87
+ }
88
+ let k_max_by_len = len - search_len;
89
+ let k_max_by_start = start + 1 - search_len;
90
+ let k_max = k_max_by_len.min(k_max_by_start);
91
+ let mut k = k_max;
92
+ loop {
93
+ if hay[k..k + search_len] == needle_chars[..] {
94
+ return Value::Number(k as f64);
95
+ }
96
+ if k == 0 {
97
+ break;
98
+ }
99
+ k -= 1;
100
+ }
101
+ Value::Number(-1.0)
102
+ }
103
+
104
+ /// Like [`last_index_of_str`] but takes string `Value`s; non-strings β†’ `-1`.
105
+ pub fn last_index_of(s: &Value, search: &Value, position: &Value) -> Value {
106
+ if let (Value::String(h), Value::String(n)) = (s, search) {
107
+ last_index_of_str(h.as_ref(), n.as_ref(), position)
108
+ } else {
109
+ Value::Number(-1.0)
110
+ }
111
+ }
112
+
113
+ /// Returns character index of first occurrence, or -1. Optional fromIndex (JS indexOf).
114
+ pub fn index_of(s: &Value, search: &Value, from: Option<&Value>) -> Value {
115
+ if let (Value::String(s), Value::String(search)) = (s, search) {
116
+ let from_char = match from {
117
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(s.chars().count()),
118
+ _ => 0,
119
+ };
120
+ let byte_start = char_to_byte_offset(s, from_char);
121
+ let search_str = search.as_ref();
122
+ if let Some(byte_pos) = s[byte_start..].find(search_str) {
123
+ let char_idx = from_char + byte_to_char_index(&s[byte_start..], byte_pos);
124
+ Value::Number(char_idx as f64)
125
+ } else {
126
+ Value::Number(-1.0)
127
+ }
128
+ } else {
129
+ Value::Number(-1.0)
130
+ }
131
+ }
132
+
133
+ pub fn includes(s: &Value, search: &Value, from: Option<&Value>) -> Value {
134
+ if let (Value::String(s), Value::String(search)) = (s, search) {
135
+ let from_char = match from {
136
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(s.chars().count()),
137
+ Some(Value::Number(n)) if *n < 0.0 => {
138
+ let len = s.chars().count() as i64;
139
+ ((len + *n as i64).max(0)) as usize
140
+ }
141
+ _ => 0,
142
+ };
143
+ let byte_start = char_to_byte_offset(s, from_char);
144
+ Value::Bool(s[byte_start..].contains(search.as_ref()))
145
+ } else {
146
+ Value::Bool(false)
147
+ }
148
+ }
149
+
150
+ pub fn slice(s: &Value, start: &Value, end: &Value) -> Value {
151
+ if let Value::String(s) = s {
152
+ let chars: Vec<char> = s.chars().collect();
153
+ let len = chars.len() as i64;
154
+ let (si, ei) = (
155
+ normalize_index(start, len, 0),
156
+ normalize_index(end, len, len as usize),
157
+ );
158
+ let result: String = if si < ei {
159
+ chars[si..ei].iter().collect()
160
+ } else {
161
+ String::new()
162
+ };
163
+ Value::String(result.into())
164
+ } else {
165
+ Value::Null
166
+ }
167
+ }
168
+
169
+ pub fn substring(s: &Value, start: &Value, end: &Value) -> Value {
170
+ fn bounds(start: &Value, end: &Value, len: usize) -> (usize, usize) {
171
+ let si = match start {
172
+ Value::Number(n) => (*n as usize).min(len),
173
+ _ => 0,
174
+ };
175
+ let ei = match end {
176
+ Value::Null => len,
177
+ Value::Number(n) => (*n as usize).min(len),
178
+ _ => len,
179
+ };
180
+ (si.min(ei), si.max(ei))
181
+ }
182
+ if let Value::String(s) = s {
183
+ let chars: Vec<char> = s.chars().collect();
184
+ let (ss, ee) = bounds(start, end, chars.len());
185
+ let result: String = chars[ss..ee].iter().collect();
186
+ Value::String(result.into())
187
+ } else {
188
+ Value::Null
189
+ }
190
+ }
191
+
192
+ /// JS `String.prototype.substr(start, length)`.
193
+ pub fn substr(s: &Value, start: &Value, length: &Value) -> Value {
194
+ if let Value::String(s) = s {
195
+ let chars: Vec<char> = s.chars().collect();
196
+ let len = chars.len();
197
+ let mut start_idx = match start {
198
+ Value::Number(n) => *n as i64,
199
+ _ => 0,
200
+ };
201
+ if start_idx < 0 {
202
+ start_idx = (len as i64 + start_idx).max(0);
203
+ }
204
+ let start_idx = (start_idx as usize).min(len);
205
+ let count = match length {
206
+ Value::Null => len - start_idx,
207
+ Value::Number(n) => (*n as i64).max(0) as usize,
208
+ _ => len - start_idx,
209
+ };
210
+ let end_idx = (start_idx + count).min(len);
211
+ let result: String = chars[start_idx..end_idx].iter().collect();
212
+ Value::String(result.into())
213
+ } else {
214
+ Value::Null
215
+ }
216
+ }
217
+
218
+ pub fn split(s: &Value, sep: &Value) -> Value {
219
+ if let Value::String(s) = s {
220
+ let separator = match sep {
221
+ Value::String(ss) => ss.as_ref(),
222
+ _ => return Value::Array(VmRef::new(vec![Value::String(Arc::clone(s))])),
223
+ };
224
+ let parts: Vec<Value> = s
225
+ .split(separator)
226
+ .map(|p| Value::String(p.into()))
227
+ .collect();
228
+ Value::Array(VmRef::new(parts))
229
+ } else {
230
+ Value::Null
231
+ }
232
+ }
233
+
234
+ pub fn trim(s: &Value) -> Value {
235
+ if let Value::String(s) = s {
236
+ Value::String(s.trim().into())
237
+ } else {
238
+ Value::Null
239
+ }
240
+ }
241
+
242
+ pub fn to_upper_case(s: &Value) -> Value {
243
+ if let Value::String(s) = s {
244
+ Value::String(s.to_uppercase().into())
245
+ } else {
246
+ Value::Null
247
+ }
248
+ }
249
+
250
+ pub fn to_lower_case(s: &Value) -> Value {
251
+ if let Value::String(s) = s {
252
+ Value::String(s.to_lowercase().into())
253
+ } else {
254
+ Value::Null
255
+ }
256
+ }
257
+
258
+ pub fn starts_with(s: &Value, search: &Value) -> Value {
259
+ if let (Value::String(s), Value::String(search)) = (s, search) {
260
+ Value::Bool(s.starts_with(search.as_ref()))
261
+ } else {
262
+ Value::Bool(false)
263
+ }
264
+ }
265
+
266
+ pub fn ends_with(s: &Value, search: &Value) -> Value {
267
+ if let (Value::String(s), Value::String(search)) = (s, search) {
268
+ Value::Bool(s.ends_with(search.as_ref()))
269
+ } else {
270
+ Value::Bool(false)
271
+ }
272
+ }
273
+
274
+ fn replace_impl(s: &Value, search: &Value, replacement: &Value, all: bool) -> Value {
275
+ if let Value::String(s) = s {
276
+ let search_str = match search {
277
+ Value::String(ss) => ss.as_ref(),
278
+ _ => return Value::String(Arc::clone(s)),
279
+ };
280
+ let repl_str = match replacement {
281
+ Value::String(ss) => ss.as_ref(),
282
+ _ => "",
283
+ };
284
+ let result = if all {
285
+ s.replace(search_str, repl_str)
286
+ } else {
287
+ s.replacen(search_str, repl_str, 1)
288
+ };
289
+ Value::String(result.into())
290
+ } else {
291
+ Value::Null
292
+ }
293
+ }
294
+
295
+ pub fn replace(s: &Value, search: &Value, replacement: &Value) -> Value {
296
+ replace_impl(s, search, replacement, false)
297
+ }
298
+
299
+ pub fn replace_all(s: &Value, search: &Value, replacement: &Value) -> Value {
300
+ replace_impl(s, search, replacement, true)
301
+ }
302
+
303
+ /// HTML entity escape for the five canonical characters (`& < > " '`).
304
+ /// Single linear pass over the input; takes a zero-copy fast path when no
305
+ /// character needs escaping. Matches TFB's fortunes verifier byte-for-byte.
306
+ pub fn escape_html(s: &Value) -> Value {
307
+ let input = match s {
308
+ Value::String(s) => s.as_ref(),
309
+ Value::Null => return Value::String(Arc::from("")),
310
+ _ => return Value::Null,
311
+ };
312
+ let bytes = input.as_bytes();
313
+ let mut extra = 0usize;
314
+ for b in bytes {
315
+ match b {
316
+ b'&' => extra += 4,
317
+ b'<' | b'>' => extra += 3,
318
+ b'"' => extra += 5,
319
+ b'\'' => extra += 4,
320
+ _ => {}
321
+ }
322
+ }
323
+ if extra == 0 {
324
+ return Value::String(Arc::clone(match s {
325
+ Value::String(s) => s,
326
+ _ => unreachable!(),
327
+ }));
328
+ }
329
+ let mut out = String::with_capacity(input.len() + extra);
330
+ let mut last = 0usize;
331
+ for (i, b) in bytes.iter().enumerate() {
332
+ let repl: Option<&'static str> = match b {
333
+ b'&' => Some("&amp;"),
334
+ b'<' => Some("&lt;"),
335
+ b'>' => Some("&gt;"),
336
+ b'"' => Some("&quot;"),
337
+ b'\'' => Some("&#39;"),
338
+ _ => None,
339
+ };
340
+ if let Some(r) = repl {
341
+ out.push_str(&input[last..i]);
342
+ out.push_str(r);
343
+ last = i + 1;
344
+ }
345
+ }
346
+ out.push_str(&input[last..]);
347
+ Value::String(Arc::from(out))
348
+ }
349
+
350
+ fn char_at_idx(s: &str, idx: usize) -> Option<char> {
351
+ s.chars().nth(idx)
352
+ }
353
+
354
+ pub fn char_at(s: &Value, idx: &Value) -> Value {
355
+ if let Value::String(s) = s {
356
+ let idx = match idx {
357
+ Value::Number(n) => *n as usize,
358
+ _ => 0,
359
+ };
360
+ char_at_idx(s, idx)
361
+ .map(|c| Value::String(c.to_string().into()))
362
+ .unwrap_or(Value::String("".into()))
363
+ } else {
364
+ Value::Null
365
+ }
366
+ }
367
+
368
+ pub fn char_code_at(s: &Value, idx: &Value) -> Value {
369
+ if let Value::String(s) = s {
370
+ let idx = match idx {
371
+ Value::Number(n) => *n as usize,
372
+ _ => 0,
373
+ };
374
+ char_at_idx(s, idx)
375
+ .map(|c| Value::Number(c as u32 as f64))
376
+ .unwrap_or(Value::Number(f64::NAN))
377
+ } else {
378
+ Value::Null
379
+ }
380
+ }
381
+
382
+ pub fn repeat(s: &Value, count: &Value) -> Value {
383
+ if let Value::String(s) = s {
384
+ let count = match count {
385
+ Value::Number(n) if *n >= 0.0 => *n as usize,
386
+ _ => 0,
387
+ };
388
+ Value::String(s.repeat(count).into())
389
+ } else {
390
+ Value::Null
391
+ }
392
+ }
393
+
394
+ fn pad_impl(s: &Value, target_len: &Value, pad: &Value, at_start: bool) -> Value {
395
+ if let Value::String(s) = s {
396
+ let target_len = match target_len {
397
+ Value::Number(n) => *n as usize,
398
+ _ => return Value::String(Arc::clone(s)),
399
+ };
400
+ let pad_str = match pad {
401
+ Value::String(p) if !p.is_empty() => p.as_ref(),
402
+ _ => " ",
403
+ };
404
+ let char_count = s.chars().count();
405
+ if char_count >= target_len {
406
+ return Value::String(Arc::clone(s));
407
+ }
408
+ let needed = target_len - char_count;
409
+ let padding: String = pad_str.chars().cycle().take(needed).collect();
410
+ let result = if at_start {
411
+ format!("{}{}", padding, s)
412
+ } else {
413
+ format!("{}{}", s, padding)
414
+ };
415
+ Value::String(result.into())
416
+ } else {
417
+ Value::Null
418
+ }
419
+ }
420
+
421
+ pub fn pad_start(s: &Value, target_len: &Value, pad: &Value) -> Value {
422
+ pad_impl(s, target_len, pad, true)
423
+ }
424
+
425
+ pub fn pad_end(s: &Value, target_len: &Value, pad: &Value) -> Value {
426
+ pad_impl(s, target_len, pad, false)
427
+ }
428
+
429
+ #[cfg(test)]
430
+ mod tests {
431
+ use super::*;
432
+
433
+ fn s(x: &str) -> Value {
434
+ Value::String(x.into())
435
+ }
436
+
437
+ fn n(x: f64) -> Value {
438
+ Value::Number(x)
439
+ }
440
+
441
+ fn same(a: &Value, b: &Value) -> bool {
442
+ match (a, b) {
443
+ (Value::String(x), Value::String(y)) => x == y,
444
+ (Value::Number(x), Value::Number(y)) => {
445
+ if x.is_nan() && y.is_nan() {
446
+ true
447
+ } else {
448
+ x == y
449
+ }
450
+ }
451
+ (Value::Bool(x), Value::Bool(y)) => x == y,
452
+ (Value::Null, Value::Null) => true,
453
+ (Value::Array(ax), Value::Array(ay)) => {
454
+ let bx = ax.borrow();
455
+ let by = ay.borrow();
456
+ bx.len() == by.len() && bx.iter().zip(by.iter()).all(|(u, v)| same(u, v))
457
+ }
458
+ _ => false,
459
+ }
460
+ }
461
+
462
+ macro_rules! assert_same {
463
+ ($left:expr, $right:expr) => {
464
+ assert!(same(&$left, &$right), "left={:?} right={:?}", $left, $right);
465
+ };
466
+ }
467
+
468
+ #[test]
469
+ fn index_of_basic() {
470
+ assert_same!(index_of(&s("abc"), &s("b"), None), n(1.0));
471
+ assert_same!(index_of(&s("abc"), &s("x"), None), n(-1.0));
472
+ assert_same!(index_of(&s("abca"), &s("a"), Some(&n(1.0))), n(3.0));
473
+ }
474
+
475
+ #[test]
476
+ fn index_of_non_string() {
477
+ assert_same!(index_of(&n(1.0), &s("a"), None), n(-1.0));
478
+ assert_same!(index_of(&s("a"), &n(1.0), None), n(-1.0));
479
+ }
480
+
481
+ #[test]
482
+ fn includes_basic() {
483
+ assert_same!(includes(&s("hello"), &s("ll"), None), Value::Bool(true));
484
+ assert_same!(includes(&s("hello"), &s("x"), None), Value::Bool(false));
485
+ assert_same!(
486
+ includes(&s("hello"), &s("l"), Some(&n(3.0))),
487
+ Value::Bool(true)
488
+ );
489
+ assert_same!(
490
+ includes(&s("hello"), &s("l"), Some(&n(4.0))),
491
+ Value::Bool(false)
492
+ );
493
+ }
494
+
495
+ #[test]
496
+ fn includes_negative_from() {
497
+ assert_same!(
498
+ includes(&s("hello"), &s("o"), Some(&n(-1.0))),
499
+ Value::Bool(true)
500
+ );
501
+ assert_same!(
502
+ includes(&s("hello"), &s("h"), Some(&n(-5.0))),
503
+ Value::Bool(true)
504
+ );
505
+ // fromIndex -1 β†’ start at len-1 = 1 ("i" only), "h" not found
506
+ assert_same!(
507
+ includes(&s("hi"), &s("h"), Some(&n(-1.0))),
508
+ Value::Bool(false)
509
+ );
510
+ }
511
+
512
+ #[test]
513
+ fn includes_non_string() {
514
+ assert_same!(includes(&n(1.0), &s("a"), None), Value::Bool(false));
515
+ }
516
+
517
+ #[test]
518
+ fn slice_substring() {
519
+ assert_same!(slice(&s("hello"), &n(1.0), &n(4.0)), s("ell"));
520
+ assert_same!(slice(&s("hello"), &n(-3.0), &Value::Null), s("llo"));
521
+ assert_same!(substring(&s("hello"), &n(4.0), &n(1.0)), s("ell"));
522
+ assert_same!(slice(&s("ab"), &n(1.0), &n(1.0)), s(""));
523
+ }
524
+
525
+ #[test]
526
+ fn slice_non_string() {
527
+ assert_same!(slice(&n(1.0), &n(0.0), &Value::Null), Value::Null);
528
+ }
529
+
530
+ #[test]
531
+ fn split_trim() {
532
+ let Value::Array(a) = split(&s("a,b"), &s(",")) else {
533
+ panic!();
534
+ };
535
+ assert_eq!(a.borrow().len(), 2);
536
+ assert_same!(
537
+ split(&s("x"), &n(1.0)),
538
+ Value::Array(VmRef::new(vec![s("x")]))
539
+ );
540
+ assert_same!(split(&n(1.0), &s(",")), Value::Null);
541
+ assert_same!(trim(&s(" x ")), s("x"));
542
+ assert_same!(trim(&n(1.0)), Value::Null);
543
+ }
544
+
545
+ #[test]
546
+ fn case_and_prefix_suffix() {
547
+ assert_same!(to_upper_case(&s("aB")), s("AB"));
548
+ assert_same!(to_lower_case(&s("aB")), s("ab"));
549
+ assert_same!(starts_with(&s("/api"), &s("/api")), Value::Bool(true));
550
+ assert_same!(ends_with(&s("x.js"), &s(".js")), Value::Bool(true));
551
+ assert_same!(starts_with(&n(1.0), &s("")), Value::Bool(false));
552
+ }
553
+
554
+ #[test]
555
+ fn replace_family() {
556
+ assert_same!(replace(&s("aa"), &s("a"), &s("b")), s("ba"));
557
+ assert_same!(replace_all(&s("aa"), &s("a"), &s("b")), s("bb"));
558
+ assert_same!(replace(&n(1.0), &s("a"), &s("b")), Value::Null);
559
+ }
560
+
561
+ #[test]
562
+ fn char_at_code() {
563
+ assert_same!(char_at(&s("ab"), &n(0.0)), s("a"));
564
+ assert_same!(char_at(&s("ab"), &n(99.0)), s(""));
565
+ if let Value::Number(x) = char_code_at(&s("A"), &n(0.0)) {
566
+ assert_eq!(x, 65.0);
567
+ } else {
568
+ panic!();
569
+ }
570
+ assert!(matches!(char_code_at(&s("x"), &n(9.0)), Value::Number(x) if x.is_nan()));
571
+ }
572
+
573
+ #[test]
574
+ fn repeat_pad() {
575
+ assert_same!(repeat(&s("ab"), &n(2.0)), s("abab"));
576
+ assert_same!(repeat(&s("x"), &n(0.0)), s(""));
577
+ assert_same!(pad_start(&s("5"), &n(3.0), &s("0")), s("005"));
578
+ assert_same!(pad_end(&s("hi"), &n(5.0), &s("!")), s("hi!!!"));
579
+ assert_same!(pad_start(&s("hello"), &n(3.0), &Value::Null), s("hello"));
580
+ }
581
+
582
+ #[test]
583
+ fn last_index_of_basic() {
584
+ assert_same!(
585
+ last_index_of(&s("abcabc"), &s("a"), &n(f64::INFINITY)),
586
+ n(3.0)
587
+ );
588
+ assert_same!(last_index_of(&s("abcabc"), &s("a"), &n(2.0)), n(0.0));
589
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(3.0)), n(3.0));
590
+ assert_same!(last_index_of(&s("hello"), &s("l"), &n(1.0)), n(-1.0));
591
+ }
592
+
593
+ #[test]
594
+ fn last_index_of_omit_and_null() {
595
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::INFINITY)), n(2.0));
596
+ assert_same!(last_index_of(&s("aba"), &s("a"), &Value::Null), n(0.0));
597
+ }
598
+
599
+ #[test]
600
+ fn last_index_of_empty_needle() {
601
+ assert_same!(last_index_of(&s("abc"), &s(""), &n(2.0)), n(2.0));
602
+ }
603
+
604
+ #[test]
605
+ fn last_index_of_nan_position() {
606
+ assert_same!(last_index_of(&s("aba"), &s("a"), &n(f64::NAN)), n(0.0));
607
+ }
608
+
609
+ #[test]
610
+ fn last_index_of_unicode() {
611
+ assert_same!(
612
+ last_index_of(&s("πŸ˜€aπŸ˜€"), &s("a"), &n(f64::INFINITY)),
613
+ n(1.0)
614
+ );
615
+ assert_same!(
616
+ last_index_of(&s("πŸ˜€aπŸ˜€"), &s("πŸ˜€"), &n(f64::INFINITY)),
617
+ n(2.0)
618
+ );
619
+ }
620
+
621
+ #[test]
622
+ fn last_index_of_non_string() {
623
+ assert_same!(last_index_of(&n(1.0), &s("a"), &n(0.0)), n(-1.0));
624
+ }
625
+
626
+ #[test]
627
+ fn escape_html_basic() {
628
+ assert_same!(escape_html(&s("plain text")), s("plain text"));
629
+ assert_same!(
630
+ escape_html(&s("<script>alert(\"xss\")</script>")),
631
+ s("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;")
632
+ );
633
+ assert_same!(escape_html(&s("tom & jerry")), s("tom &amp; jerry"));
634
+ assert_same!(escape_html(&s("it's")), s("it&#39;s"));
635
+ assert_same!(
636
+ escape_html(&s("<script>alert('x' & \"y\");</script>")),
637
+ s("&lt;script&gt;alert(&#39;x&#39; &amp; &quot;y&quot;);&lt;/script&gt;")
638
+ );
639
+ }
640
+
641
+ #[test]
642
+ fn escape_html_unicode_preserved() {
643
+ // Astral symbols / non-ASCII must round-trip unchanged.
644
+ assert_same!(escape_html(&s("フレーム")), s("フレーム"));
645
+ assert_same!(escape_html(&s("πŸŽ‰ & πŸ’₯")), s("πŸŽ‰ &amp; πŸ’₯"));
646
+ }
647
+ }