@tishlang/tish 1.0.29 → 1.0.33
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.
- package/Cargo.toml +1 -0
- package/crates/js_to_tish/src/transform/expr.rs +15 -6
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/main.rs +1 -1
- package/crates/tish/tests/integration_test.rs +4 -3
- package/crates/tish_ast/src/ast.rs +65 -2
- package/crates/tish_build_utils/src/lib.rs +10 -2
- package/crates/tish_builtins/src/construct.rs +177 -0
- package/crates/tish_builtins/src/globals.rs +3 -5
- package/crates/tish_builtins/src/helpers.rs +2 -3
- package/crates/tish_builtins/src/lib.rs +1 -0
- package/crates/tish_builtins/src/object.rs +3 -4
- package/crates/tish_bytecode/src/compiler.rs +85 -11
- package/crates/tish_bytecode/src/opcode.rs +7 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +233 -71
- package/crates/tish_compile/src/lib.rs +35 -0
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +38 -94
- package/crates/tish_compile_js/src/lib.rs +0 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +68 -0
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/console_style.rs +7 -1
- package/crates/tish_core/src/json.rs +1 -2
- package/crates/tish_core/src/macros.rs +2 -3
- package/crates/tish_core/src/value.rs +10 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +149 -72
- package/crates/tish_eval/src/http.rs +3 -4
- package/crates/tish_eval/src/regex.rs +3 -2
- package/crates/tish_eval/src/value.rs +11 -13
- package/crates/tish_eval/src/value_convert.rs +4 -8
- package/crates/tish_fmt/src/lib.rs +49 -10
- package/crates/tish_lexer/src/token.rs +2 -0
- package/crates/tish_lint/src/lib.rs +9 -0
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_native/src/build.rs +16 -2
- package/crates/tish_opt/src/lib.rs +15 -0
- package/crates/tish_parser/src/lib.rs +101 -1
- package/crates/tish_parser/src/parser.rs +161 -50
- package/crates/tish_runtime/src/http.rs +4 -5
- package/crates/tish_runtime/src/http_fetch.rs +9 -10
- package/crates/tish_runtime/src/lib.rs +9 -2
- package/crates/tish_runtime/src/promise.rs +2 -3
- package/crates/tish_runtime/src/promise_io.rs +2 -3
- package/crates/tish_runtime/src/ws.rs +7 -7
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +390 -0
- package/crates/tish_ui/src/lib.rs +16 -0
- package/crates/tish_ui/src/runtime/hooks.rs +122 -0
- package/crates/tish_ui/src/runtime/mod.rs +173 -0
- package/crates/tish_vm/src/vm.rs +121 -27
- package/justfile +3 -3
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
- package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
//! Web Fetch–aligned Response, ReadableStream, reader.read(), text()/json().
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
-
use std::collections::HashMap;
|
|
5
4
|
use std::pin::Pin;
|
|
6
5
|
use std::rc::Rc;
|
|
7
6
|
use std::sync::{Arc, Mutex};
|
|
@@ -9,7 +8,7 @@ use std::sync::{Arc, Mutex};
|
|
|
9
8
|
use bytes::Bytes;
|
|
10
9
|
use futures::Stream;
|
|
11
10
|
use futures::StreamExt;
|
|
12
|
-
use tishlang_core::{NativeFn, TishOpaque, TishPromise, Value};
|
|
11
|
+
use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
|
|
13
12
|
|
|
14
13
|
use crate::http::{build_error_response, extract_body, extract_headers, extract_method};
|
|
15
14
|
|
|
@@ -77,14 +76,14 @@ impl TishPromise for ReadChunkPromise {
|
|
|
77
76
|
let r = crate::http::block_on_http(rx);
|
|
78
77
|
match r {
|
|
79
78
|
Ok(Ok(ReadChunk::Done)) => {
|
|
80
|
-
let mut o =
|
|
79
|
+
let mut o = ObjectMap::default();
|
|
81
80
|
o.insert(Arc::from("done"), Value::Bool(true));
|
|
82
81
|
o.insert(Arc::from("value"), Value::Null);
|
|
83
82
|
Ok(Value::Object(Rc::new(RefCell::new(o))))
|
|
84
83
|
}
|
|
85
84
|
Ok(Ok(ReadChunk::Bytes(b))) => {
|
|
86
85
|
let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
|
|
87
|
-
let mut o =
|
|
86
|
+
let mut o = ObjectMap::default();
|
|
88
87
|
o.insert(Arc::from("done"), Value::Bool(false));
|
|
89
88
|
o.insert(
|
|
90
89
|
Arc::from("value"),
|
|
@@ -93,7 +92,7 @@ impl TishPromise for ReadChunkPromise {
|
|
|
93
92
|
Ok(Value::Object(Rc::new(RefCell::new(o))))
|
|
94
93
|
}
|
|
95
94
|
Ok(Err(e)) => Err({
|
|
96
|
-
let mut obj =
|
|
95
|
+
let mut obj = ObjectMap::default();
|
|
97
96
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
98
97
|
Value::Object(Rc::new(RefCell::new(obj)))
|
|
99
98
|
}),
|
|
@@ -118,13 +117,13 @@ impl TishPromise for JsonTextPromise {
|
|
|
118
117
|
Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
|
|
119
118
|
Ok(v) => Ok(v),
|
|
120
119
|
Err(e) => Err({
|
|
121
|
-
let mut obj =
|
|
120
|
+
let mut obj = ObjectMap::default();
|
|
122
121
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
123
122
|
Value::Object(Rc::new(RefCell::new(obj)))
|
|
124
123
|
}),
|
|
125
124
|
},
|
|
126
125
|
Ok(Err(e)) => Err({
|
|
127
|
-
let mut obj =
|
|
126
|
+
let mut obj = ObjectMap::default();
|
|
128
127
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
129
128
|
Value::Object(Rc::new(RefCell::new(obj)))
|
|
130
129
|
}),
|
|
@@ -226,7 +225,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
226
225
|
}))
|
|
227
226
|
}
|
|
228
227
|
Err(e) => {
|
|
229
|
-
let mut m =
|
|
228
|
+
let mut m = ObjectMap::default();
|
|
230
229
|
m.insert(Arc::from("error"), Value::String(e.into()));
|
|
231
230
|
Value::Object(Rc::new(RefCell::new(m)))
|
|
232
231
|
}
|
|
@@ -283,7 +282,7 @@ impl TishOpaque for HttpStreamReader {
|
|
|
283
282
|
}
|
|
284
283
|
|
|
285
284
|
fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
|
|
286
|
-
let mut headers_obj:
|
|
285
|
+
let mut headers_obj: ObjectMap = ObjectMap::with_capacity(headers.len());
|
|
287
286
|
for (key, value) in headers.iter() {
|
|
288
287
|
if let Ok(v) = value.to_str() {
|
|
289
288
|
headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
|
|
@@ -327,7 +326,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
327
326
|
rx: Mutex::new(Some(rx)),
|
|
328
327
|
}))
|
|
329
328
|
});
|
|
330
|
-
let mut obj:
|
|
329
|
+
let mut obj: ObjectMap = ObjectMap::default();
|
|
331
330
|
obj.insert(Arc::from("status"), Value::Number(status));
|
|
332
331
|
obj.insert(Arc::from("ok"), Value::Bool(ok));
|
|
333
332
|
obj.insert(Arc::from("headers"), headers_val);
|
|
@@ -13,8 +13,15 @@ use tishlang_builtins::helpers::extract_num;
|
|
|
13
13
|
#[cfg(feature = "fs")]
|
|
14
14
|
use tishlang_builtins::helpers::make_error_value;
|
|
15
15
|
|
|
16
|
+
pub use tishlang_core::ObjectMap;
|
|
16
17
|
pub use tishlang_core::Value;
|
|
17
18
|
|
|
19
|
+
pub use tishlang_builtins::construct::{
|
|
20
|
+
audio_context_constructor_value as tish_audio_context_constructor,
|
|
21
|
+
construct as tish_construct,
|
|
22
|
+
uint8_array_constructor_value as tish_uint8_array_constructor,
|
|
23
|
+
};
|
|
24
|
+
|
|
18
25
|
// Re-export array methods from tishlang_builtins
|
|
19
26
|
pub use tishlang_builtins::array::{
|
|
20
27
|
push as array_push_impl,
|
|
@@ -754,7 +761,7 @@ pub fn regexp_exec(re: &Value, input: &Value) -> Value {
|
|
|
754
761
|
|
|
755
762
|
#[cfg(feature = "regex")]
|
|
756
763
|
fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
|
|
757
|
-
use
|
|
764
|
+
use tishlang_core::ObjectMap;
|
|
758
765
|
|
|
759
766
|
let start = if re.flags.global || re.flags.sticky {
|
|
760
767
|
re.last_index
|
|
@@ -785,7 +792,7 @@ fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
|
|
|
785
792
|
let match_byte_start = byte_start + full_match.start();
|
|
786
793
|
let match_char_index = input[..match_byte_start].chars().count();
|
|
787
794
|
|
|
788
|
-
let mut obj:
|
|
795
|
+
let mut obj: ObjectMap = ObjectMap::default();
|
|
789
796
|
obj.insert(Arc::from("0"), Value::String(full_match.as_str().into()));
|
|
790
797
|
for i in 1..caps.len() {
|
|
791
798
|
let val = match caps.get(i) {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
//! Promise static methods for compiled Tish (resolve, reject, all, race).
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
-
use std::collections::HashMap;
|
|
5
4
|
use std::rc::Rc;
|
|
6
5
|
use std::sync::Arc;
|
|
7
|
-
use tishlang_core::Value;
|
|
6
|
+
use tishlang_core::{ObjectMap, Value};
|
|
8
7
|
|
|
9
8
|
/// Promise.resolve(value) - returns the value (immediate resolve).
|
|
10
9
|
pub fn promise_resolve(args: &[Value]) -> Value {
|
|
@@ -56,7 +55,7 @@ pub fn promise_race(args: &[Value]) -> Value {
|
|
|
56
55
|
|
|
57
56
|
/// Build the Promise object with resolve, reject, all, race static methods.
|
|
58
57
|
pub fn promise_object() -> Value {
|
|
59
|
-
let mut map:
|
|
58
|
+
let mut map: ObjectMap = ObjectMap::default();
|
|
60
59
|
map.insert(
|
|
61
60
|
Arc::from("resolve"),
|
|
62
61
|
Value::Function(Rc::new(|args: &[Value]| promise_resolve(args))),
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
//! Promises carrying only Send payloads (string results for text(), etc.).
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
-
use std::collections::HashMap;
|
|
5
4
|
use std::rc::Rc;
|
|
6
5
|
use std::sync::{Arc, Mutex};
|
|
7
|
-
use tishlang_core::{
|
|
6
|
+
use tishlang_core::{ObjectMap, TishPromise, Value};
|
|
8
7
|
use tokio::sync::oneshot;
|
|
9
8
|
|
|
10
9
|
fn error_value(msg: String) -> Value {
|
|
11
|
-
let mut obj:
|
|
10
|
+
let mut obj: ObjectMap = ObjectMap::with_capacity(2);
|
|
12
11
|
obj.insert(Arc::from("error"), Value::String(msg.into()));
|
|
13
12
|
obj.insert(Arc::from("ok"), Value::Bool(false));
|
|
14
13
|
Value::Object(Rc::new(RefCell::new(obj)))
|
|
@@ -15,7 +15,7 @@ use std::time::{Duration, Instant};
|
|
|
15
15
|
|
|
16
16
|
use futures_util::{SinkExt, StreamExt};
|
|
17
17
|
use lazy_static::lazy_static;
|
|
18
|
-
use tishlang_core::Value;
|
|
18
|
+
use tishlang_core::{ObjectMap, Value};
|
|
19
19
|
use tokio::sync::mpsc as tokio_mpsc;
|
|
20
20
|
use tokio::runtime::Runtime;
|
|
21
21
|
|
|
@@ -194,7 +194,7 @@ pub fn ws_broadcast_native(args: &[Value]) -> Value {
|
|
|
194
194
|
|
|
195
195
|
/// Build connection object: { _id, send, close, readyState, receive }. JS-like.
|
|
196
196
|
fn conn_object(id: u32) -> Value {
|
|
197
|
-
let mut obj:
|
|
197
|
+
let mut obj: ObjectMap = ObjectMap::default();
|
|
198
198
|
obj.insert(Arc::from("_id"), Value::Number(id as f64));
|
|
199
199
|
obj.insert(Arc::from("readyState"), Value::Number(1.0)); // OPEN
|
|
200
200
|
obj.insert(
|
|
@@ -216,7 +216,7 @@ fn conn_object(id: u32) -> Value {
|
|
|
216
216
|
Value::Function(Rc::new(move |_args: &[Value]| {
|
|
217
217
|
match conn_receive(id) {
|
|
218
218
|
Some(s) => {
|
|
219
|
-
let mut ev:
|
|
219
|
+
let mut ev: ObjectMap = ObjectMap::default();
|
|
220
220
|
ev.insert(Arc::from("data"), Value::String(s.into()));
|
|
221
221
|
Value::Object(Rc::new(RefCell::new(ev)))
|
|
222
222
|
}
|
|
@@ -237,7 +237,7 @@ fn conn_object(id: u32) -> Value {
|
|
|
237
237
|
.unwrap_or(1000);
|
|
238
238
|
match conn_receive_timeout(id_timeout, timeout_ms) {
|
|
239
239
|
Some(s) => {
|
|
240
|
-
let mut ev:
|
|
240
|
+
let mut ev: ObjectMap = ObjectMap::default();
|
|
241
241
|
ev.insert(Arc::from("data"), Value::String(s.into()));
|
|
242
242
|
Value::Object(Rc::new(RefCell::new(ev)))
|
|
243
243
|
}
|
|
@@ -529,7 +529,7 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
|
529
529
|
ws
|
|
530
530
|
});
|
|
531
531
|
|
|
532
|
-
let mut m:
|
|
532
|
+
let mut m: ObjectMap = ObjectMap::default();
|
|
533
533
|
m.insert(Arc::from("_handle"), handle_val);
|
|
534
534
|
m.insert(Arc::from("_onConnection"), Value::Null);
|
|
535
535
|
m.insert(Arc::from("clients"), Value::Array(clients));
|
|
@@ -549,7 +549,7 @@ mod tests {
|
|
|
549
549
|
fn ws_echo_roundtrip() {
|
|
550
550
|
let port: u16 = 18_742;
|
|
551
551
|
let opts = {
|
|
552
|
-
let mut m:
|
|
552
|
+
let mut m: ObjectMap = ObjectMap::default();
|
|
553
553
|
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
554
554
|
Value::Object(Rc::new(RefCell::new(m)))
|
|
555
555
|
};
|
|
@@ -632,7 +632,7 @@ mod tests {
|
|
|
632
632
|
fn ws_gateway_agent_flow() {
|
|
633
633
|
let port: u16 = 18_743;
|
|
634
634
|
let opts = {
|
|
635
|
-
let mut m:
|
|
635
|
+
let mut m: ObjectMap = ObjectMap::default();
|
|
636
636
|
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
637
637
|
Value::Object(Rc::new(RefCell::new(m)))
|
|
638
638
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_ui"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Shared JSX lowering and UI runtime (vnode, hooks, host protocol) for Tish"
|
|
6
|
+
|
|
7
|
+
license-file = { workspace = true }
|
|
8
|
+
repository = { workspace = true }
|
|
9
|
+
|
|
10
|
+
[features]
|
|
11
|
+
default = ["runtime"]
|
|
12
|
+
compiler = ["dep:tishlang_ast"]
|
|
13
|
+
runtime = ["dep:tishlang_core"]
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
tishlang_ast = { path = "../tish_ast", version = ">=0.1", optional = true }
|
|
17
|
+
tishlang_core = { path = "../tish_core", version = ">=0.1", optional = true }
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
//! Shared JSX lowering: emit `h(tag, props, children)` as JavaScript or Rust (`Value`) source.
|
|
2
|
+
|
|
3
|
+
use tishlang_ast::{
|
|
4
|
+
ArrayElement, Expr, JsxAttrValue, JsxChild, JsxProp, Literal, ObjectProp,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/// Escape a Tish identifier for Rust output (matches `tishlang_compile` conventions).
|
|
8
|
+
pub fn escape_ident_rust(s: &str) -> String {
|
|
9
|
+
if s == "await" || s == "default" {
|
|
10
|
+
format!("_{}", s)
|
|
11
|
+
} else {
|
|
12
|
+
s.to_string()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Emit JSX expression as JavaScript (same rules as legacy `tishlang_compile_js`).
|
|
17
|
+
pub fn emit_jsx_js<F, E>(expr: &Expr, emit_expr: &mut F) -> Result<String, E>
|
|
18
|
+
where
|
|
19
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
20
|
+
E: From<String>,
|
|
21
|
+
{
|
|
22
|
+
match expr {
|
|
23
|
+
Expr::JsxElement {
|
|
24
|
+
tag,
|
|
25
|
+
props,
|
|
26
|
+
children,
|
|
27
|
+
..
|
|
28
|
+
} => {
|
|
29
|
+
let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
|
|
30
|
+
tag.as_ref().to_string()
|
|
31
|
+
} else {
|
|
32
|
+
format!("{:?}", tag.as_ref())
|
|
33
|
+
};
|
|
34
|
+
let props_str = emit_jsx_props_js(props, emit_expr)?;
|
|
35
|
+
let children_strs: Result<Vec<_>, _> =
|
|
36
|
+
children.iter().map(|c| emit_jsx_child_js(c, emit_expr)).collect();
|
|
37
|
+
let children_str = children_strs?.join(", ");
|
|
38
|
+
Ok(format!("h({}, {}, [{}])", tag_str, props_str, children_str))
|
|
39
|
+
}
|
|
40
|
+
Expr::JsxFragment { children, .. } => {
|
|
41
|
+
let children_strs: Result<Vec<_>, _> =
|
|
42
|
+
children.iter().map(|c| emit_jsx_child_js(c, emit_expr)).collect();
|
|
43
|
+
let children_str = children_strs?.join(", ");
|
|
44
|
+
Ok(format!("h(Fragment, null, [{}])", children_str))
|
|
45
|
+
}
|
|
46
|
+
_ => Err(emit_err("emit_jsx_js: not a JSX expression")),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn emit_err<E>(msg: &str) -> E
|
|
51
|
+
where
|
|
52
|
+
E: From<String>,
|
|
53
|
+
{
|
|
54
|
+
E::from(msg.to_string())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn emit_jsx_props_js<F, E>(props: &[JsxProp], emit_expr: &mut F) -> Result<String, E>
|
|
58
|
+
where
|
|
59
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
60
|
+
{
|
|
61
|
+
if props.is_empty() {
|
|
62
|
+
return Ok("null".to_string());
|
|
63
|
+
}
|
|
64
|
+
let parts: Result<Vec<_>, _> = props
|
|
65
|
+
.iter()
|
|
66
|
+
.map(|p| match p {
|
|
67
|
+
JsxProp::Attr { name, value } => {
|
|
68
|
+
let val = match value {
|
|
69
|
+
JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
|
|
70
|
+
JsxAttrValue::Expr(e) => emit_expr(e)?,
|
|
71
|
+
JsxAttrValue::ImplicitTrue => "true".to_string(),
|
|
72
|
+
};
|
|
73
|
+
let key = name.as_ref();
|
|
74
|
+
Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
75
|
+
format!("{}: {}", key, val)
|
|
76
|
+
} else {
|
|
77
|
+
format!("{:?}: {}", key, val)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
JsxProp::Spread(e) => Ok(format!("...{}", emit_expr(e)?)),
|
|
81
|
+
})
|
|
82
|
+
.collect();
|
|
83
|
+
Ok(format!("{{ {} }}", parts?.join(", ")))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn emit_jsx_child_js<F, E>(child: &JsxChild, emit_expr: &mut F) -> Result<String, E>
|
|
87
|
+
where
|
|
88
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
89
|
+
{
|
|
90
|
+
match child {
|
|
91
|
+
JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
|
|
92
|
+
JsxChild::Expr(e) => {
|
|
93
|
+
let inner = emit_expr(e)?;
|
|
94
|
+
let needs_string = matches!(
|
|
95
|
+
e,
|
|
96
|
+
Expr::Literal {
|
|
97
|
+
value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
|
|
98
|
+
..
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
Ok(if needs_string {
|
|
102
|
+
format!("String({})", inner)
|
|
103
|
+
} else {
|
|
104
|
+
inner
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Emit JSX as Rust `Value` by calling `tishlang_ui::ui_h` directly (no closure capture of a local `h` binding).
|
|
111
|
+
pub fn emit_jsx_rust<F, E>(expr: &Expr, emit_expr: &mut F) -> Result<String, E>
|
|
112
|
+
where
|
|
113
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
114
|
+
E: From<String>,
|
|
115
|
+
{
|
|
116
|
+
match expr {
|
|
117
|
+
Expr::JsxElement {
|
|
118
|
+
tag,
|
|
119
|
+
props,
|
|
120
|
+
children,
|
|
121
|
+
..
|
|
122
|
+
} => {
|
|
123
|
+
let is_component = tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
|
|
124
|
+
let tag_rust = if is_component {
|
|
125
|
+
escape_ident_rust(tag.as_ref())
|
|
126
|
+
} else {
|
|
127
|
+
format!("Value::String({:?}.into())", tag.as_ref())
|
|
128
|
+
};
|
|
129
|
+
let props_rust = emit_jsx_props_rust(props, emit_expr)?;
|
|
130
|
+
let child_parts: Result<Vec<_>, _> = children
|
|
131
|
+
.iter()
|
|
132
|
+
.map(|c| emit_jsx_child_rust(c, emit_expr))
|
|
133
|
+
.collect();
|
|
134
|
+
let children_rust = format!(
|
|
135
|
+
"Value::Array(Rc::new(RefCell::new(vec![{}])))",
|
|
136
|
+
child_parts?.join(", ")
|
|
137
|
+
);
|
|
138
|
+
Ok(wrap_h_call_rust(&tag_rust, &props_rust, &children_rust))
|
|
139
|
+
}
|
|
140
|
+
Expr::JsxFragment { children, .. } => {
|
|
141
|
+
let child_parts: Result<Vec<_>, _> = children
|
|
142
|
+
.iter()
|
|
143
|
+
.map(|c| emit_jsx_child_rust(c, emit_expr))
|
|
144
|
+
.collect();
|
|
145
|
+
let children_rust = format!(
|
|
146
|
+
"Value::Array(Rc::new(RefCell::new(vec![{}])))",
|
|
147
|
+
child_parts?.join(", ")
|
|
148
|
+
);
|
|
149
|
+
Ok(wrap_h_call_rust(
|
|
150
|
+
"Fragment",
|
|
151
|
+
"Value::Null",
|
|
152
|
+
&children_rust,
|
|
153
|
+
))
|
|
154
|
+
}
|
|
155
|
+
_ => Err(E::from("emit_jsx_rust: not a JSX expression".to_string())),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn wrap_h_call_rust(tag: &str, props: &str, children: &str) -> String {
|
|
160
|
+
format!(
|
|
161
|
+
"tishlang_ui::ui_h(&[({}).clone(), ({}).clone(), ({}).clone()])",
|
|
162
|
+
tag, props, children
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn emit_jsx_props_rust<F, E>(props: &[JsxProp], emit_expr: &mut F) -> Result<String, E>
|
|
167
|
+
where
|
|
168
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
169
|
+
E: From<String>,
|
|
170
|
+
{
|
|
171
|
+
if props.is_empty() {
|
|
172
|
+
return Ok("Value::Null".to_string());
|
|
173
|
+
}
|
|
174
|
+
let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
|
|
175
|
+
if has_spread {
|
|
176
|
+
let mut parts = Vec::new();
|
|
177
|
+
for prop in props {
|
|
178
|
+
match prop {
|
|
179
|
+
JsxProp::Attr { name, value } => {
|
|
180
|
+
let val = match value {
|
|
181
|
+
JsxAttrValue::String(s) => {
|
|
182
|
+
format!("Value::String({:?}.into())", s.as_ref())
|
|
183
|
+
}
|
|
184
|
+
JsxAttrValue::Expr(e) => emit_expr(e)?,
|
|
185
|
+
JsxAttrValue::ImplicitTrue => "Value::Bool(true)".to_string(),
|
|
186
|
+
};
|
|
187
|
+
parts.push(format!(
|
|
188
|
+
"_obj.insert(Arc::from({:?}), ({}).clone());",
|
|
189
|
+
name.as_ref(),
|
|
190
|
+
val
|
|
191
|
+
));
|
|
192
|
+
}
|
|
193
|
+
JsxProp::Spread(e) => {
|
|
194
|
+
let val = emit_expr(e)?;
|
|
195
|
+
parts.push(format!(
|
|
196
|
+
"if let Value::Object(ref _spread) = {} {{ for (k, v) in _spread.borrow().iter() {{ _obj.insert(Arc::clone(k), v.clone()); }} }}",
|
|
197
|
+
val
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
Ok(format!(
|
|
203
|
+
"{{ let mut _obj: ObjectMap = ObjectMap::default(); {} Value::Object(Rc::new(RefCell::new(_obj))) }}",
|
|
204
|
+
parts.join(" ")
|
|
205
|
+
))
|
|
206
|
+
} else {
|
|
207
|
+
let mut kv = Vec::new();
|
|
208
|
+
for prop in props {
|
|
209
|
+
if let JsxProp::Attr { name, value } = prop {
|
|
210
|
+
let val = match value {
|
|
211
|
+
JsxAttrValue::String(s) => {
|
|
212
|
+
format!("Value::String({:?}.into())", s.as_ref())
|
|
213
|
+
}
|
|
214
|
+
JsxAttrValue::Expr(e) => emit_expr(e)?,
|
|
215
|
+
JsxAttrValue::ImplicitTrue => "Value::Bool(true)".to_string(),
|
|
216
|
+
};
|
|
217
|
+
kv.push(format!(
|
|
218
|
+
"(Arc::from({:?}), ({}).clone())",
|
|
219
|
+
name.as_ref(),
|
|
220
|
+
val
|
|
221
|
+
));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
Ok(format!(
|
|
225
|
+
"Value::Object(Rc::new(RefCell::new(ObjectMap::from([{}]))))",
|
|
226
|
+
kv.join(", ")
|
|
227
|
+
))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn emit_jsx_child_rust<F, E>(child: &JsxChild, emit_expr: &mut F) -> Result<String, E>
|
|
232
|
+
where
|
|
233
|
+
F: FnMut(&Expr) -> Result<String, E>,
|
|
234
|
+
E: From<String>,
|
|
235
|
+
{
|
|
236
|
+
match child {
|
|
237
|
+
JsxChild::Text(s) => Ok(format!("Value::String({:?}.into())", s.as_ref())),
|
|
238
|
+
JsxChild::Expr(e) => {
|
|
239
|
+
let inner = emit_expr(e)?;
|
|
240
|
+
let needs_string = matches!(
|
|
241
|
+
e,
|
|
242
|
+
Expr::Literal {
|
|
243
|
+
value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
|
|
244
|
+
..
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
Ok(if needs_string {
|
|
248
|
+
format!("Value::String(({}).to_display_string().into())", inner)
|
|
249
|
+
} else {
|
|
250
|
+
format!("({}).clone()", inner)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Whether the program contains any JSX syntax (for conditional native UI globals).
|
|
257
|
+
pub fn program_contains_jsx(program: &tishlang_ast::Program) -> bool {
|
|
258
|
+
program.statements.iter().any(stmt_contains_jsx)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
|
|
262
|
+
use tishlang_ast::{ExportDeclaration, Statement};
|
|
263
|
+
match stmt {
|
|
264
|
+
Statement::Block { statements, .. } => statements.iter().any(stmt_contains_jsx),
|
|
265
|
+
Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_contains_jsx),
|
|
266
|
+
Statement::VarDeclDestructure { init, .. } => expr_contains_jsx(init),
|
|
267
|
+
Statement::ExprStmt { expr, .. } => expr_contains_jsx(expr),
|
|
268
|
+
Statement::Return { value, .. } => value.as_ref().is_some_and(expr_contains_jsx),
|
|
269
|
+
Statement::If {
|
|
270
|
+
cond,
|
|
271
|
+
then_branch,
|
|
272
|
+
else_branch,
|
|
273
|
+
..
|
|
274
|
+
} => {
|
|
275
|
+
expr_contains_jsx(cond)
|
|
276
|
+
|| stmt_contains_jsx(then_branch)
|
|
277
|
+
|| else_branch.as_ref().is_some_and(|s| stmt_contains_jsx(s))
|
|
278
|
+
}
|
|
279
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
|
|
280
|
+
expr_contains_jsx(cond) || stmt_contains_jsx(body)
|
|
281
|
+
}
|
|
282
|
+
Statement::For { init, cond, update, body, .. } => {
|
|
283
|
+
init.as_ref().is_some_and(|s| stmt_contains_jsx(s))
|
|
284
|
+
|| cond.as_ref().is_some_and(expr_contains_jsx)
|
|
285
|
+
|| update.as_ref().is_some_and(expr_contains_jsx)
|
|
286
|
+
|| stmt_contains_jsx(body)
|
|
287
|
+
}
|
|
288
|
+
Statement::ForOf { iterable, body, .. } => {
|
|
289
|
+
expr_contains_jsx(iterable) || stmt_contains_jsx(body)
|
|
290
|
+
}
|
|
291
|
+
Statement::Switch { expr, cases, default_body, .. } => {
|
|
292
|
+
expr_contains_jsx(expr)
|
|
293
|
+
|| cases.iter().any(|(e, ss)| {
|
|
294
|
+
e.as_ref().is_some_and(expr_contains_jsx) || ss.iter().any(stmt_contains_jsx)
|
|
295
|
+
})
|
|
296
|
+
|| default_body
|
|
297
|
+
.as_ref()
|
|
298
|
+
.is_some_and(|ss| ss.iter().any(stmt_contains_jsx))
|
|
299
|
+
}
|
|
300
|
+
Statement::Try {
|
|
301
|
+
body,
|
|
302
|
+
catch_body,
|
|
303
|
+
finally_body,
|
|
304
|
+
..
|
|
305
|
+
} => {
|
|
306
|
+
stmt_contains_jsx(body)
|
|
307
|
+
|| catch_body.as_ref().is_some_and(|s| stmt_contains_jsx(s))
|
|
308
|
+
|| finally_body.as_ref().is_some_and(|s| stmt_contains_jsx(s))
|
|
309
|
+
}
|
|
310
|
+
Statement::FunDecl { body, .. } => stmt_contains_jsx(body),
|
|
311
|
+
Statement::Throw { value, .. } => expr_contains_jsx(value),
|
|
312
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
313
|
+
ExportDeclaration::Named(inner) => stmt_contains_jsx(inner),
|
|
314
|
+
ExportDeclaration::Default(e) => expr_contains_jsx(e),
|
|
315
|
+
},
|
|
316
|
+
Statement::Import { .. } | Statement::Break { .. } | Statement::Continue { .. } => false,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fn expr_contains_jsx(expr: &Expr) -> bool {
|
|
321
|
+
match expr {
|
|
322
|
+
Expr::JsxElement { .. } | Expr::JsxFragment { .. } => true,
|
|
323
|
+
Expr::Binary { left, right, .. } => expr_contains_jsx(left) || expr_contains_jsx(right),
|
|
324
|
+
Expr::Unary { operand, .. } => expr_contains_jsx(operand),
|
|
325
|
+
Expr::Assign { value, .. } => expr_contains_jsx(value),
|
|
326
|
+
Expr::Call { callee, args, .. } => {
|
|
327
|
+
expr_contains_jsx(callee)
|
|
328
|
+
|| args.iter().any(|a| match a {
|
|
329
|
+
tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
|
|
330
|
+
expr_contains_jsx(e)
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
Expr::Member { object, prop, .. } => {
|
|
335
|
+
expr_contains_jsx(object)
|
|
336
|
+
|| matches!(prop, tishlang_ast::MemberProp::Expr(e) if expr_contains_jsx(e))
|
|
337
|
+
}
|
|
338
|
+
Expr::Index { object, index, .. } => expr_contains_jsx(object) || expr_contains_jsx(index),
|
|
339
|
+
Expr::Conditional {
|
|
340
|
+
cond,
|
|
341
|
+
then_branch,
|
|
342
|
+
else_branch,
|
|
343
|
+
..
|
|
344
|
+
} => {
|
|
345
|
+
expr_contains_jsx(cond)
|
|
346
|
+
|| expr_contains_jsx(then_branch)
|
|
347
|
+
|| expr_contains_jsx(else_branch)
|
|
348
|
+
}
|
|
349
|
+
Expr::Array { elements, .. } => elements.iter().any(|el| match el {
|
|
350
|
+
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_contains_jsx(e),
|
|
351
|
+
}),
|
|
352
|
+
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
353
|
+
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_contains_jsx(e),
|
|
354
|
+
}),
|
|
355
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
356
|
+
tishlang_ast::ArrowBody::Expr(e) => expr_contains_jsx(e),
|
|
357
|
+
tishlang_ast::ArrowBody::Block(s) => stmt_contains_jsx(s),
|
|
358
|
+
},
|
|
359
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
360
|
+
expr_contains_jsx(left) || expr_contains_jsx(right)
|
|
361
|
+
}
|
|
362
|
+
Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(expr_contains_jsx),
|
|
363
|
+
Expr::Await { operand, .. } => expr_contains_jsx(operand),
|
|
364
|
+
Expr::TypeOf { operand, .. } => expr_contains_jsx(operand),
|
|
365
|
+
Expr::PostfixInc { .. }
|
|
366
|
+
| Expr::PrefixInc { .. }
|
|
367
|
+
| Expr::PostfixDec { .. }
|
|
368
|
+
| Expr::PrefixDec { .. } => false,
|
|
369
|
+
Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
|
|
370
|
+
expr_contains_jsx(value)
|
|
371
|
+
}
|
|
372
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
373
|
+
expr_contains_jsx(object) || expr_contains_jsx(value)
|
|
374
|
+
}
|
|
375
|
+
Expr::IndexAssign { object, index, value, .. } => {
|
|
376
|
+
expr_contains_jsx(object) || expr_contains_jsx(index) || expr_contains_jsx(value)
|
|
377
|
+
}
|
|
378
|
+
Expr::New { callee, args, .. } => {
|
|
379
|
+
expr_contains_jsx(callee)
|
|
380
|
+
|| args.iter().any(|a| match a {
|
|
381
|
+
tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
|
|
382
|
+
expr_contains_jsx(e)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
Expr::Literal { .. }
|
|
387
|
+
| Expr::Ident { .. }
|
|
388
|
+
| Expr::NativeModuleLoad { .. } => false,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//! JSX lowering (compiler) and UI runtime (vnode + hooks + host) for cross-target Tish UI.
|
|
2
|
+
//!
|
|
3
|
+
//! - Feature **`compiler`**: AST → JS / Rust `h(...)` emission helpers (depends on `tishlang_ast`).
|
|
4
|
+
//! - Feature **`runtime`**: `Value`-based `h`, `Fragment`, hooks, and [`Host`] (depends on `tishlang_core`).
|
|
5
|
+
|
|
6
|
+
#[cfg(feature = "compiler")]
|
|
7
|
+
pub mod jsx;
|
|
8
|
+
|
|
9
|
+
#[cfg(feature = "runtime")]
|
|
10
|
+
pub mod runtime;
|
|
11
|
+
|
|
12
|
+
#[cfg(feature = "runtime")]
|
|
13
|
+
pub use runtime::{
|
|
14
|
+
fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h,
|
|
15
|
+
ui_text, with_thread_local_host, Host, HeadlessHost, FRAGMENT_SENTINEL,
|
|
16
|
+
};
|