@tishlang/tish-format 1.0.12 β†’ 2.0.1

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