@tishlang/tish 1.0.27 → 1.0.29
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/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/main.rs +7 -54
- package/crates/tish_compile_js/Cargo.toml +0 -1
- package/crates/tish_compile_js/src/codegen.rs +7 -55
- package/crates/tish_compile_js/src/lib.rs +4 -6
- package/crates/tish_compile_js/src/tests_jsx.rs +65 -17
- package/crates/tish_compiler_wasm/src/lib.rs +2 -3
- package/crates/tish_jsx_web/Cargo.toml +1 -1
- package/crates/tish_jsx_web/README.md +3 -16
- package/crates/tish_jsx_web/src/lib.rs +2 -157
- package/crates/tish_lexer/src/lib.rs +56 -12
- 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_jsx_web/vendor/Lattish.tish +0 -362
package/crates/tish/Cargo.toml
CHANGED
package/crates/tish/src/main.rs
CHANGED
|
@@ -56,9 +56,6 @@ struct CompileArgs {
|
|
|
56
56
|
features: Vec<String>,
|
|
57
57
|
#[arg(long)]
|
|
58
58
|
no_optimize: bool,
|
|
59
|
-
/// JS target only: `lattish` (default), `vdom` (vnode + patch; use with Lattish createRoot).
|
|
60
|
-
#[arg(long = "jsx", value_name = "MODE", default_value = "lattish")]
|
|
61
|
-
jsx_mode: String,
|
|
62
59
|
#[arg(required = true)]
|
|
63
60
|
file: String,
|
|
64
61
|
}
|
|
@@ -94,7 +91,6 @@ fn main() {
|
|
|
94
91
|
&a.native_backend,
|
|
95
92
|
&a.features,
|
|
96
93
|
a.no_optimize || no_opt_env,
|
|
97
|
-
&a.jsx_mode,
|
|
98
94
|
),
|
|
99
95
|
Some(Commands::DumpAst { file }) => dump_ast(&file),
|
|
100
96
|
None => run_repl("vm", false), // No args = REPL
|
|
@@ -339,29 +335,7 @@ fn tish_history_path() -> Option<PathBuf> {
|
|
|
339
335
|
home.map(|h| PathBuf::from(h).join(".tish_history"))
|
|
340
336
|
}
|
|
341
337
|
|
|
342
|
-
fn
|
|
343
|
-
match s {
|
|
344
|
-
"legacy" => Err(
|
|
345
|
-
"--jsx legacy was removed. Use --jsx lattish (default) with lattish merged into your \
|
|
346
|
-
bundle, or --jsx vdom with Lattish's createRoot."
|
|
347
|
-
.to_string(),
|
|
348
|
-
),
|
|
349
|
-
"vdom" => Ok(tishlang_compile_js::JsxMode::Vdom),
|
|
350
|
-
"lattish" => Ok(tishlang_compile_js::JsxMode::LattishH),
|
|
351
|
-
other => Err(format!(
|
|
352
|
-
"Unknown --jsx {:?}: use lattish (default) or vdom.",
|
|
353
|
-
other
|
|
354
|
-
)),
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
fn compile_to_js(
|
|
359
|
-
input_path: &Path,
|
|
360
|
-
output_path: &str,
|
|
361
|
-
optimize: bool,
|
|
362
|
-
jsx: &str,
|
|
363
|
-
) -> Result<(), String> {
|
|
364
|
-
let jsx_mode = parse_jsx_mode(jsx)?;
|
|
338
|
+
fn compile_to_js(input_path: &Path, output_path: &str, optimize: bool) -> Result<(), String> {
|
|
365
339
|
let project_root = input_path.parent().and_then(|p| {
|
|
366
340
|
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
367
341
|
p.parent()
|
|
@@ -382,13 +356,13 @@ fn compile_to_js(
|
|
|
382
356
|
} else {
|
|
383
357
|
program
|
|
384
358
|
};
|
|
385
|
-
tishlang_compile_js::compile_with_jsx(&p, optimize
|
|
359
|
+
tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?
|
|
386
360
|
} else if input_path.extension().map(|e| e == "js") == Some(true) {
|
|
387
361
|
let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
|
|
388
362
|
let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
|
|
389
|
-
tishlang_compile_js::compile_with_jsx(&program, optimize
|
|
363
|
+
tishlang_compile_js::compile_with_jsx(&program, optimize).map_err(|e| format!("{}", e))?
|
|
390
364
|
} else {
|
|
391
|
-
tishlang_compile_js::compile_project_with_jsx(input_path, project_root, optimize
|
|
365
|
+
tishlang_compile_js::compile_project_with_jsx(input_path, project_root, optimize)
|
|
392
366
|
.map_err(|e| format!("{}", e))?
|
|
393
367
|
};
|
|
394
368
|
|
|
@@ -418,7 +392,6 @@ fn compile_file(
|
|
|
418
392
|
native_backend: &str,
|
|
419
393
|
cli_features: &[String],
|
|
420
394
|
no_optimize: bool,
|
|
421
|
-
jsx: &str,
|
|
422
395
|
) -> Result<(), String> {
|
|
423
396
|
let optimize = !no_optimize;
|
|
424
397
|
let input_path =
|
|
@@ -427,7 +400,7 @@ fn compile_file(
|
|
|
427
400
|
let is_js = input_path.extension().map(|e| e == "js") == Some(true);
|
|
428
401
|
|
|
429
402
|
if target == "js" {
|
|
430
|
-
return compile_to_js(&input_path, output_path, optimize
|
|
403
|
+
return compile_to_js(&input_path, output_path, optimize);
|
|
431
404
|
}
|
|
432
405
|
|
|
433
406
|
if target == "wasm" && is_js {
|
|
@@ -539,7 +512,7 @@ mod cli_tests {
|
|
|
539
512
|
use super::{Cli, Commands};
|
|
540
513
|
|
|
541
514
|
#[test]
|
|
542
|
-
fn
|
|
515
|
+
fn compile_js_target_parses() {
|
|
543
516
|
let cli = Cli::try_parse_from([
|
|
544
517
|
"tish",
|
|
545
518
|
"compile",
|
|
@@ -551,27 +524,7 @@ mod cli_tests {
|
|
|
551
524
|
])
|
|
552
525
|
.unwrap();
|
|
553
526
|
match cli.command {
|
|
554
|
-
Some(Commands::Compile(a)) => assert_eq!(a.
|
|
555
|
-
_ => panic!("expected Compile"),
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
#[test]
|
|
560
|
-
fn compile_jsx_flag_vdom() {
|
|
561
|
-
let cli = Cli::try_parse_from([
|
|
562
|
-
"tish",
|
|
563
|
-
"compile",
|
|
564
|
-
"a.tish",
|
|
565
|
-
"--target",
|
|
566
|
-
"js",
|
|
567
|
-
"--jsx",
|
|
568
|
-
"vdom",
|
|
569
|
-
"-o",
|
|
570
|
-
"x.js",
|
|
571
|
-
])
|
|
572
|
-
.unwrap();
|
|
573
|
-
match cli.command {
|
|
574
|
-
Some(Commands::Compile(a)) => assert_eq!(a.jsx_mode.as_str(), "vdom"),
|
|
527
|
+
Some(Commands::Compile(a)) => assert_eq!(a.file, "m.tish"),
|
|
575
528
|
_ => panic!("expected Compile"),
|
|
576
529
|
}
|
|
577
530
|
}
|
|
@@ -10,7 +10,6 @@ repository = { workspace = true }
|
|
|
10
10
|
default = []
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
|
-
tishlang_jsx_web = { path = "../tish_jsx_web", version = ">=0.1" }
|
|
14
13
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
15
14
|
tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
|
|
16
15
|
tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
|
|
@@ -9,23 +9,11 @@ use tishlang_ast::{
|
|
|
9
9
|
use crate::error::CompileError;
|
|
10
10
|
use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};
|
|
11
11
|
|
|
12
|
-
/// How JSX lowers for `--target js`. Native targets never use this.
|
|
13
|
-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
14
|
-
pub enum JsxMode {
|
|
15
|
-
/// `h(tag, props, [children])` + `Fragment` (Lattish.tish). Import `h` / `Fragment` from lattish.
|
|
16
|
-
#[default]
|
|
17
|
-
LattishH,
|
|
18
|
-
/// Vnode `__vdom_h` + `window.__lattishVdomPatch` (Lattish createRoot must use patch).
|
|
19
|
-
Vdom,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
struct Codegen {
|
|
23
13
|
output: String,
|
|
24
14
|
indent: usize,
|
|
25
15
|
in_async: bool,
|
|
26
|
-
uses_jsx: bool,
|
|
27
16
|
intrinsics: JsIntrinsics,
|
|
28
|
-
jsx_mode: JsxMode,
|
|
29
17
|
}
|
|
30
18
|
|
|
31
19
|
fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
@@ -36,14 +24,12 @@ fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
|
|
|
36
24
|
}
|
|
37
25
|
|
|
38
26
|
impl Codegen {
|
|
39
|
-
fn new(
|
|
27
|
+
fn new() -> Self {
|
|
40
28
|
Self {
|
|
41
29
|
output: String::new(),
|
|
42
30
|
indent: 0,
|
|
43
31
|
in_async: false,
|
|
44
|
-
uses_jsx: false,
|
|
45
32
|
intrinsics: JsIntrinsics::new(),
|
|
46
|
-
jsx_mode,
|
|
47
33
|
}
|
|
48
34
|
}
|
|
49
35
|
|
|
@@ -72,25 +58,12 @@ impl Codegen {
|
|
|
72
58
|
|
|
73
59
|
fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
74
60
|
self.write("// Generated by tishlang_compile_js\n");
|
|
75
|
-
// First pass: check if JSX is used (we'll set uses_jsx during emit)
|
|
76
61
|
for stmt in &program.statements {
|
|
77
62
|
self.emit_statement(stmt)?;
|
|
78
63
|
}
|
|
79
64
|
self.output = self
|
|
80
65
|
.intrinsics
|
|
81
66
|
.prepend_runtime_preamble(std::mem::take(&mut self.output));
|
|
82
|
-
if self.uses_jsx {
|
|
83
|
-
match self.jsx_mode {
|
|
84
|
-
JsxMode::Vdom => {
|
|
85
|
-
self.output = format!(
|
|
86
|
-
"{}\n{}",
|
|
87
|
-
tishlang_jsx_web::VDOM_PRELUDE,
|
|
88
|
-
self.output
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
JsxMode::LattishH => {}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
67
|
Ok(())
|
|
95
68
|
}
|
|
96
69
|
|
|
@@ -653,7 +626,6 @@ impl Codegen {
|
|
|
653
626
|
format!("(await {})", o)
|
|
654
627
|
}
|
|
655
628
|
Expr::JsxElement { tag, props, children, .. } => {
|
|
656
|
-
self.uses_jsx = true;
|
|
657
629
|
let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
|
|
658
630
|
tag.as_ref().to_string()
|
|
659
631
|
} else {
|
|
@@ -663,28 +635,13 @@ impl Codegen {
|
|
|
663
635
|
let children_strs: Result<Vec<_>, _> =
|
|
664
636
|
children.iter().map(|c| self.emit_jsx_child(c)).collect();
|
|
665
637
|
let children_str = children_strs?.join(", ");
|
|
666
|
-
|
|
667
|
-
JsxMode::LattishH => {
|
|
668
|
-
format!("h({}, {}, [{}])", tag_str, props_str, children_str)
|
|
669
|
-
}
|
|
670
|
-
JsxMode::Vdom => {
|
|
671
|
-
format!("__vdom_h({}, {}, [{}])", tag_str, props_str, children_str)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
638
|
+
format!("h({}, {}, [{}])", tag_str, props_str, children_str)
|
|
674
639
|
}
|
|
675
640
|
Expr::JsxFragment { children, .. } => {
|
|
676
|
-
self.uses_jsx = true;
|
|
677
641
|
let children_strs: Result<Vec<_>, _> =
|
|
678
642
|
children.iter().map(|c| self.emit_jsx_child(c)).collect();
|
|
679
643
|
let children_str = children_strs?.join(", ");
|
|
680
|
-
|
|
681
|
-
JsxMode::LattishH => {
|
|
682
|
-
format!("h(Fragment, null, [{}])", children_str)
|
|
683
|
-
}
|
|
684
|
-
JsxMode::Vdom => {
|
|
685
|
-
format!("__vdom_h(__Fragment, null, [{}])", children_str)
|
|
686
|
-
}
|
|
687
|
-
}
|
|
644
|
+
format!("h(Fragment, null, [{}])", children_str)
|
|
688
645
|
}
|
|
689
646
|
Expr::NativeModuleLoad { spec, .. } => {
|
|
690
647
|
return Err(CompileError {
|
|
@@ -754,18 +711,14 @@ impl Codegen {
|
|
|
754
711
|
}
|
|
755
712
|
}
|
|
756
713
|
|
|
757
|
-
/// Compile a single program (no imports) to JavaScript.
|
|
758
|
-
pub fn compile_with_jsx(
|
|
759
|
-
program: &Program,
|
|
760
|
-
optimize: bool,
|
|
761
|
-
jsx_mode: JsxMode,
|
|
762
|
-
) -> Result<String, CompileError> {
|
|
714
|
+
/// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
|
|
715
|
+
pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, CompileError> {
|
|
763
716
|
let program = if optimize {
|
|
764
717
|
tishlang_opt::optimize(program)
|
|
765
718
|
} else {
|
|
766
719
|
program.clone()
|
|
767
720
|
};
|
|
768
|
-
let mut g = Codegen::new(
|
|
721
|
+
let mut g = Codegen::new();
|
|
769
722
|
g.emit_program(&program)?;
|
|
770
723
|
Ok(g.output)
|
|
771
724
|
}
|
|
@@ -776,7 +729,6 @@ pub fn compile_project_with_jsx(
|
|
|
776
729
|
entry_path: &std::path::Path,
|
|
777
730
|
project_root: Option<&std::path::Path>,
|
|
778
731
|
optimize: bool,
|
|
779
|
-
jsx_mode: JsxMode,
|
|
780
732
|
) -> Result<String, CompileError> {
|
|
781
733
|
use tishlang_ast::Statement;
|
|
782
734
|
let modules = tishlang_compile::resolve_project(entry_path, project_root)
|
|
@@ -802,7 +754,7 @@ pub fn compile_project_with_jsx(
|
|
|
802
754
|
None
|
|
803
755
|
}
|
|
804
756
|
});
|
|
805
|
-
let mut js = compile_with_jsx(&program, optimize
|
|
757
|
+
let mut js = compile_with_jsx(&program, optimize)?;
|
|
806
758
|
if let Some(name) = default_export {
|
|
807
759
|
js.push_str(&format!("\nexport default {};\n", name));
|
|
808
760
|
}
|
|
@@ -8,14 +8,12 @@ mod js_intrinsics;
|
|
|
8
8
|
#[cfg(test)]
|
|
9
9
|
mod tests_jsx;
|
|
10
10
|
|
|
11
|
-
pub use codegen::{compile_project_with_jsx, compile_with_jsx
|
|
11
|
+
pub use codegen::{compile_project_with_jsx, compile_with_jsx};
|
|
12
12
|
pub use error::CompileError;
|
|
13
13
|
|
|
14
|
-
///
|
|
15
|
-
/// `lattish` (e.g. `useState`, `createRoot`); the merged bundle includes the JSX runtime from that
|
|
16
|
-
/// module. JSX-only files can use `import {} from "lattish"` to pull it in without bindings.
|
|
14
|
+
/// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.
|
|
17
15
|
pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
|
|
18
|
-
compile_with_jsx(program, optimize
|
|
16
|
+
compile_with_jsx(program, optimize)
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
pub fn compile_project(
|
|
@@ -23,5 +21,5 @@ pub fn compile_project(
|
|
|
23
21
|
project_root: Option<&std::path::Path>,
|
|
24
22
|
optimize: bool,
|
|
25
23
|
) -> Result<String, CompileError> {
|
|
26
|
-
compile_project_with_jsx(entry_path, project_root, optimize
|
|
24
|
+
compile_project_with_jsx(entry_path, project_root, optimize)
|
|
27
25
|
}
|
|
@@ -4,13 +4,13 @@ mod tests {
|
|
|
4
4
|
|
|
5
5
|
use tishlang_parser::parse;
|
|
6
6
|
|
|
7
|
-
use crate::{compile_project_with_jsx, compile_with_jsx
|
|
7
|
+
use crate::{compile_project_with_jsx, compile_with_jsx};
|
|
8
8
|
|
|
9
9
|
#[test]
|
|
10
10
|
fn lattish_jsx_emits_h_with_children_array() {
|
|
11
11
|
let src = r#"fn X() { return <div class="a">{"hi"}</div> }"#;
|
|
12
12
|
let program = parse(src).unwrap();
|
|
13
|
-
let js = compile_with_jsx(&program, false
|
|
13
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
14
14
|
assert!(js.contains("h(\"div\", { class: \"a\" }, [\"hi\"])"), "{}", js);
|
|
15
15
|
assert!(!js.contains("function __h("));
|
|
16
16
|
}
|
|
@@ -19,7 +19,7 @@ mod tests {
|
|
|
19
19
|
fn fragment_lattish_uses_fragment_symbol() {
|
|
20
20
|
let src = "fn X() { return <><b>{\"1\"}</b></> }";
|
|
21
21
|
let program = parse(src).unwrap();
|
|
22
|
-
let js = compile_with_jsx(&program, false
|
|
22
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
23
23
|
assert!(js.contains("h(Fragment, null, ["));
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ mod tests {
|
|
|
27
27
|
fn jsx_text_whitespace_coalesced() {
|
|
28
28
|
let src = r#"fn X() { return <p>First paragraph</p> }"#;
|
|
29
29
|
let program = parse(src).unwrap();
|
|
30
|
-
let js = compile_with_jsx(&program, false
|
|
30
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
31
31
|
assert!(
|
|
32
32
|
js.contains("\"First paragraph\""),
|
|
33
33
|
"expected \"First paragraph\" in output, got: {}",
|
|
@@ -43,7 +43,7 @@ mod tests {
|
|
|
43
43
|
fn jsx_text_whitespace_coalesced_multiline() {
|
|
44
44
|
let src = "fn App() {\n return <p>First paragraph</p>\n}";
|
|
45
45
|
let program = parse(src).unwrap();
|
|
46
|
-
let js = compile_with_jsx(&program, false
|
|
46
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
47
47
|
assert!(
|
|
48
48
|
js.contains("\"First paragraph\""),
|
|
49
49
|
"multiline: expected \"First paragraph\", got: {}",
|
|
@@ -56,7 +56,7 @@ mod tests {
|
|
|
56
56
|
// Punctuation (e.g. !) concatenates without space: "work!" not "work !"
|
|
57
57
|
let src = r#"fn X() { return <p>work!</p> }"#;
|
|
58
58
|
let program = parse(src).unwrap();
|
|
59
|
-
let js = compile_with_jsx(&program, false
|
|
59
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
60
60
|
assert!(js.contains(r#""work!""#), "expected 'work!', got: {}", &js[..400.min(js.len())]);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -64,7 +64,7 @@ mod tests {
|
|
|
64
64
|
fn jsx_text_emojis() {
|
|
65
65
|
let src = r#"fn X() { return <p>hello 😔</p> }"#;
|
|
66
66
|
let program = parse(src).unwrap();
|
|
67
|
-
let js = compile_with_jsx(&program, false
|
|
67
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
68
68
|
assert!(js.contains("😔"), "expected emoji, got: {}", &js[..400.min(js.len())]);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -78,7 +78,7 @@ mod tests {
|
|
|
78
78
|
f.write_all(src.as_bytes()).unwrap();
|
|
79
79
|
f.sync_all().unwrap();
|
|
80
80
|
drop(f);
|
|
81
|
-
let js = compile_project_with_jsx(&path, Some(&dir), false
|
|
81
|
+
let js = compile_project_with_jsx(&path, Some(&dir), false)
|
|
82
82
|
.expect("compile_project_with_jsx failed");
|
|
83
83
|
assert!(
|
|
84
84
|
js.contains("\"First paragraph\""),
|
|
@@ -89,12 +89,14 @@ mod tests {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
#[test]
|
|
92
|
-
fn
|
|
93
|
-
let src = r#"fn X() { return <
|
|
92
|
+
fn jsx_never_emits_vdom_helpers_or_prelude_flags() {
|
|
93
|
+
let src = r#"fn X() { return <div class="x">{"a"}</div> }"#;
|
|
94
94
|
let program = parse(src).unwrap();
|
|
95
|
-
let js = compile_with_jsx(&program, false
|
|
96
|
-
assert!(js.contains("
|
|
97
|
-
assert!(js.contains("
|
|
95
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
96
|
+
assert!(js.contains("h(\"div\", { class: \"x\" }"), "{}", &js[..500.min(js.len())]);
|
|
97
|
+
assert!(!js.contains("__vdom_h"), "{}", &js[..600.min(js.len())]);
|
|
98
|
+
assert!(!js.contains("window.__LATTISH_JSX_VDOM"), "{}", &js[..600.min(js.len())]);
|
|
99
|
+
assert!(!js.contains("__lattishVdomPatch"), "{}", &js[..600.min(js.len())]);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
|
|
@@ -105,7 +107,7 @@ fn Panel() { return <div class="p">content</div> }
|
|
|
105
107
|
fn App() { return <div>{Panel()}</div> }
|
|
106
108
|
"#;
|
|
107
109
|
let program = parse(src).unwrap();
|
|
108
|
-
let js = compile_with_jsx(&program, false
|
|
110
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
109
111
|
assert!(
|
|
110
112
|
js.contains("Panel()"),
|
|
111
113
|
"component call should appear as Panel(), got: {}",
|
|
@@ -123,7 +125,7 @@ fn App() { return <div>{Panel()}</div> }
|
|
|
123
125
|
fn jsx_nested_element_not_wrapped_in_string() {
|
|
124
126
|
let src = r#"fn X() { return <div><span>inner</span></div> }"#;
|
|
125
127
|
let program = parse(src).unwrap();
|
|
126
|
-
let js = compile_with_jsx(&program, false
|
|
128
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
127
129
|
assert!(
|
|
128
130
|
!js.contains("String(h("),
|
|
129
131
|
"nested JSX elements must NOT be wrapped in String(). got: {}",
|
|
@@ -136,7 +138,7 @@ fn App() { return <div>{Panel()}</div> }
|
|
|
136
138
|
fn jsx_literal_number_wrapped_in_string() {
|
|
137
139
|
let src = r#"fn X() { return <span>{42}</span> }"#;
|
|
138
140
|
let program = parse(src).unwrap();
|
|
139
|
-
let js = compile_with_jsx(&program, false
|
|
141
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
140
142
|
assert!(
|
|
141
143
|
js.contains("String(42)"),
|
|
142
144
|
"literal number in JSX should be wrapped in String(). got: {}",
|
|
@@ -156,11 +158,57 @@ fn FileList() {
|
|
|
156
158
|
}
|
|
157
159
|
"#;
|
|
158
160
|
let program = parse(src).unwrap();
|
|
159
|
-
let js = compile_with_jsx(&program, false
|
|
161
|
+
let js = compile_with_jsx(&program, false).unwrap();
|
|
160
162
|
assert!(
|
|
161
163
|
!js.contains("String(items)"),
|
|
162
164
|
"array/ident in JSX must NOT be wrapped in String() - causes [object HTMLButtonElement]. got: {}",
|
|
163
165
|
&js[..600.min(js.len())]
|
|
164
166
|
);
|
|
165
167
|
}
|
|
168
|
+
|
|
169
|
+
/// `>` inside `{ ... }` attribute values must be a comparison operator, not end of opening tag.
|
|
170
|
+
#[test]
|
|
171
|
+
fn jsx_gt_comparison_inside_attribute_expression() {
|
|
172
|
+
let src = r#"fn X() {
|
|
173
|
+
return <button
|
|
174
|
+
type="button"
|
|
175
|
+
onclick={() => {
|
|
176
|
+
let nm = "a"
|
|
177
|
+
if (nm && nm.length > 0) { print(nm) }
|
|
178
|
+
}}
|
|
179
|
+
>{"ok"}</button>
|
|
180
|
+
}"#;
|
|
181
|
+
let program = parse(src).expect("parse multi-line JSX with > comparison in attr");
|
|
182
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
183
|
+
assert!(
|
|
184
|
+
js.contains("length > 0") || js.contains("length>0"),
|
|
185
|
+
"expected compiled JS to preserve greater-than comparison, got: {}",
|
|
186
|
+
&js[..800.min(js.len())]
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Nested JSX inside an attribute callback must still close inner `<tag>` correctly.
|
|
191
|
+
#[test]
|
|
192
|
+
fn jsx_nested_element_inside_attribute_expression() {
|
|
193
|
+
let src = r#"fn X() {
|
|
194
|
+
return <button
|
|
195
|
+
onclick={() => {
|
|
196
|
+
let x = <span>{"inner"}</span>
|
|
197
|
+
print(x)
|
|
198
|
+
}}
|
|
199
|
+
>{"outer"}</button>
|
|
200
|
+
}"#;
|
|
201
|
+
let program = parse(src).expect("parse nested JSX inside onclick");
|
|
202
|
+
let js = compile_with_jsx(&program, false).expect("compile");
|
|
203
|
+
assert!(
|
|
204
|
+
js.contains("\"inner\""),
|
|
205
|
+
"expected nested span text in output, got: {}",
|
|
206
|
+
&js[..900.min(js.len())]
|
|
207
|
+
);
|
|
208
|
+
assert!(
|
|
209
|
+
js.contains("\"outer\""),
|
|
210
|
+
"expected button child text in output, got: {}",
|
|
211
|
+
&js[..900.min(js.len())]
|
|
212
|
+
);
|
|
213
|
+
}
|
|
166
214
|
}
|
|
@@ -10,7 +10,6 @@ mod resolve_virtual;
|
|
|
10
10
|
use base64::Engine;
|
|
11
11
|
use resolve_virtual::{detect_cycles_virtual, merge_modules_virtual, resolve_virtual};
|
|
12
12
|
use std::collections::HashMap;
|
|
13
|
-
use tishlang_compile_js::JsxMode;
|
|
14
13
|
use wasm_bindgen::prelude::*;
|
|
15
14
|
|
|
16
15
|
#[wasm_bindgen]
|
|
@@ -24,7 +23,7 @@ pub fn compile_to_bytecode(source: &str) -> Result<String, JsValue> {
|
|
|
24
23
|
#[wasm_bindgen]
|
|
25
24
|
pub fn compile_to_js(source: &str) -> Result<String, JsValue> {
|
|
26
25
|
let program = tishlang_parser::parse(source.trim()).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
27
|
-
tishlang_compile_js::compile_with_jsx(&program, true
|
|
26
|
+
tishlang_compile_js::compile_with_jsx(&program, true)
|
|
28
27
|
.map_err(|e| JsValue::from_str(&e.message))
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -50,6 +49,6 @@ pub fn compile_to_js_with_imports(entry_path: &str, files_json: &str) -> Result<
|
|
|
50
49
|
detect_cycles_virtual(&modules).map_err(|e| JsValue::from_str(&e))?;
|
|
51
50
|
let program = merge_modules_virtual(modules).map_err(|e| JsValue::from_str(&e))?;
|
|
52
51
|
let program = tishlang_opt::optimize(&program);
|
|
53
|
-
tishlang_compile_js::compile_with_jsx(&program, true
|
|
52
|
+
tishlang_compile_js::compile_with_jsx(&program, true)
|
|
54
53
|
.map_err(|e| JsValue::from_str(&e.message))
|
|
55
54
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "tishlang_jsx_web"
|
|
3
3
|
version = "0.1.0"
|
|
4
4
|
edition = "2021"
|
|
5
|
-
description = "
|
|
5
|
+
description = "Workspace placeholder; Lattish runtime is the lattish npm/git repo (not vendored here)"
|
|
6
6
|
|
|
7
7
|
license-file = { workspace = true }
|
|
8
8
|
repository = { workspace = true }
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tishlang_jsx_web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Workspace placeholder. **Lattish** (`h`, `Fragment`, hooks, reconciler) lives in the [lattish](https://github.com/tishlang/lattish) repository — edit **`lattish/src/Lattish.tish` only**.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Native / non-JS compiler targets must not depend on this crate; only `tish_compile_js` pulls it in.
|
|
8
|
-
|
|
9
|
-
## Vendor runtime
|
|
10
|
-
|
|
11
|
-
`vendor/Lattish.tish` is a copy refreshed from the **lattish** npm package. Run `just refresh-lattish` to update from the sibling lattish package.
|
|
12
|
-
|
|
13
|
-
## JSX modes (`--jsx`)
|
|
14
|
-
|
|
15
|
-
| Mode | JSX lowers to | Preamble |
|
|
16
|
-
|------|----------------|----------|
|
|
17
|
-
| `lattish` (default) | Lattish-style JSX lowering | none — merge `Lattish.tish` by importing any export you need (or `import {} from "lattish"` for JSX-only) |
|
|
18
|
-
| `vdom` | `__vdom_h(...)` | VDOM prelude; Lattish `createRoot` patches the tree when `window.__LATTISH_JSX_VDOM` is set |
|
|
5
|
+
Tish lowers JSX to `h` / `Fragment` via `tishlang_compile_js`; apps merge a compiled `Lattish.js` from npm or a path dependency.
|
|
@@ -1,157 +1,2 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//!
|
|
3
|
-
|
|
4
|
-
/// VDOM: vnode `__vdom_h` + `window.__lattishVdomPatch` for Lattish batched flush.
|
|
5
|
-
pub const VDOM_PRELUDE: &str = r#"window.__LATTISH_JSX_VDOM = true;
|
|
6
|
-
const __Fragment = Symbol('Fragment');
|
|
7
|
-
function __vdom_h(tag, props, children) {
|
|
8
|
-
if (children === undefined || children === null) children = [];
|
|
9
|
-
if (!Array.isArray(children)) children = [children];
|
|
10
|
-
return { tag: tag, props: props || null, children: children, _el: null };
|
|
11
|
-
}
|
|
12
|
-
function __vdom_flatten(ch) {
|
|
13
|
-
const out = [];
|
|
14
|
-
function w(c) {
|
|
15
|
-
if (c == null) return;
|
|
16
|
-
if (Array.isArray(c)) { for (let i = 0; i < c.length; i++) w(c[i]); return; }
|
|
17
|
-
if (typeof c === 'object' && c && c.tag === __Fragment) {
|
|
18
|
-
const inner = c.children;
|
|
19
|
-
if (Array.isArray(inner)) for (let i = 0; i < inner.length; i++) w(inner[i]);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
out.push(c);
|
|
23
|
-
}
|
|
24
|
-
if (Array.isArray(ch)) for (let i = 0; i < ch.length; i++) w(ch[i]);
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
function __vdom_mount(v) {
|
|
28
|
-
if (typeof v === 'string') return document.createTextNode(v);
|
|
29
|
-
if (v.tag === __Fragment) {
|
|
30
|
-
const f = document.createDocumentFragment();
|
|
31
|
-
const ch = __vdom_flatten(v.children);
|
|
32
|
-
for (let i = 0; i < ch.length; i++) f.appendChild(__vdom_mount(ch[i]));
|
|
33
|
-
return f;
|
|
34
|
-
}
|
|
35
|
-
const el = document.createElement(v.tag);
|
|
36
|
-
const p = v.props || {};
|
|
37
|
-
for (const k of Object.keys(p)) {
|
|
38
|
-
const val = p[k];
|
|
39
|
-
if (val === true) el.setAttribute(k, k);
|
|
40
|
-
else if (val !== false && val != null) {
|
|
41
|
-
if (k === 'class' || k === 'className') el.className = val;
|
|
42
|
-
else if (k.startsWith('on') && typeof val === 'function') el[k.toLowerCase()] = val;
|
|
43
|
-
else if (k === 'value' && (v.tag === 'input' || v.tag === 'textarea' || v.tag === 'select')) el.value = val;
|
|
44
|
-
else el.setAttribute(k, String(val));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const ch = __vdom_flatten(v.children);
|
|
48
|
-
for (let i = 0; i < ch.length; i++) el.appendChild(__vdom_mount(ch[i]));
|
|
49
|
-
v._el = el;
|
|
50
|
-
return el;
|
|
51
|
-
}
|
|
52
|
-
function __vdomPatchEl(el, ov, nv) {
|
|
53
|
-
if (!el || !ov || !nv) return false;
|
|
54
|
-
if (typeof nv === 'string') {
|
|
55
|
-
if (el.nodeType === 3) el.textContent = nv;
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
if (ov.tag !== nv.tag) return false;
|
|
59
|
-
nv._el = el;
|
|
60
|
-
const p = nv.props || {};
|
|
61
|
-
const op = ov.props || {};
|
|
62
|
-
for (const k of Object.keys(p)) {
|
|
63
|
-
if (p[k] !== op[k]) {
|
|
64
|
-
const val = p[k];
|
|
65
|
-
if (k === 'class' || k === 'className') el.className = val || '';
|
|
66
|
-
else if (k.startsWith('on')) el[k.toLowerCase()] = val || null;
|
|
67
|
-
else if (k === 'value' && (nv.tag === 'input' || nv.tag === 'textarea' || nv.tag === 'select')) {
|
|
68
|
-
if (el !== document.activeElement) el.value = val;
|
|
69
|
-
} else el.setAttribute(k, String(val));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const ocx = __vdom_flatten(ov.children);
|
|
73
|
-
const ncx = __vdom_flatten(nv.children);
|
|
74
|
-
let fullCh = ocx.length !== ncx.length;
|
|
75
|
-
if (!fullCh) {
|
|
76
|
-
for (let j = 0; j < ncx.length; j++) {
|
|
77
|
-
const o = ocx[j], n = ncx[j];
|
|
78
|
-
if (typeof n === 'string') { if (typeof o !== 'string') fullCh = true; }
|
|
79
|
-
else if (typeof o === 'string') fullCh = true;
|
|
80
|
-
else if (!o || o.tag !== n.tag) fullCh = true;
|
|
81
|
-
if (fullCh) break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (fullCh) {
|
|
85
|
-
const nodes = [];
|
|
86
|
-
for (let j = 0; j < ncx.length; j++) nodes.push(__vdom_mount(ncx[j]));
|
|
87
|
-
el.replaceChildren(...nodes);
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
let i = 0;
|
|
91
|
-
while (i < ncx.length) {
|
|
92
|
-
const o = ocx[i], n = ncx[i];
|
|
93
|
-
const childEl = el.childNodes[i];
|
|
94
|
-
if (n == null) {
|
|
95
|
-
if (childEl) el.removeChild(childEl);
|
|
96
|
-
i++;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (o == null) {
|
|
100
|
-
el.appendChild(__vdom_mount(n));
|
|
101
|
-
i++;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (typeof n === 'string') {
|
|
105
|
-
if (childEl && childEl.nodeType === 3) childEl.textContent = n;
|
|
106
|
-
else {
|
|
107
|
-
const m = __vdom_mount(n);
|
|
108
|
-
if (childEl) el.replaceChild(m, childEl);
|
|
109
|
-
else el.appendChild(m);
|
|
110
|
-
}
|
|
111
|
-
i++;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (typeof o === 'string' || o.tag !== n.tag) {
|
|
115
|
-
const m = __vdom_mount(n);
|
|
116
|
-
if (childEl) el.replaceChild(m, childEl);
|
|
117
|
-
else el.appendChild(m);
|
|
118
|
-
i++;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (!childEl || !__vdomPatchEl(childEl, o, n)) {
|
|
122
|
-
const m = __vdom_mount(n);
|
|
123
|
-
if (childEl) el.replaceChild(m, childEl);
|
|
124
|
-
else el.appendChild(m);
|
|
125
|
-
}
|
|
126
|
-
i++;
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
window.__lattishVdomPatch = function(container, oldTree, newTree) {
|
|
131
|
-
try {
|
|
132
|
-
if (oldTree == null) {
|
|
133
|
-
container.replaceChildren(__vdom_mount(newTree));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const el = container.firstChild;
|
|
137
|
-
if (!el) {
|
|
138
|
-
container.appendChild(__vdom_mount(newTree));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (typeof newTree === 'string' || oldTree.tag !== newTree.tag) {
|
|
142
|
-
container.replaceChildren(__vdom_mount(newTree));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (!__vdomPatchEl(el, oldTree, newTree)) {
|
|
146
|
-
container.replaceChildren(__vdom_mount(newTree));
|
|
147
|
-
}
|
|
148
|
-
} catch (err) {
|
|
149
|
-
console.warn('Lattish VDOM patch failed, full remount', err);
|
|
150
|
-
try {
|
|
151
|
-
container.replaceChildren(__vdom_mount(newTree));
|
|
152
|
-
} catch (e2) {
|
|
153
|
-
console.error('Lattish VDOM remount failed', e2);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
"#;
|
|
1
|
+
//! Placeholder crate kept for workspace / crates.io ordering. The JSX runtime is maintained in the
|
|
2
|
+
//! [lattish](https://github.com/tishlang/lattish) repo (`src/Lattish.tish` only — do not vendor a copy here).
|
|
@@ -14,6 +14,16 @@ use std::str::Chars;
|
|
|
14
14
|
const INDENT_WIDTH: usize = 2;
|
|
15
15
|
const TAB_AS_LEVELS: usize = 1;
|
|
16
16
|
|
|
17
|
+
/// One JSX element on the stack: tracks whether we are still in its opening tag (`<Tag ...`)
|
|
18
|
+
/// and how many `{` are open inside that element's **attribute values** (embedded JS).
|
|
19
|
+
/// This lets `>` be a comparison operator inside `{...}` while still closing `<span>` when
|
|
20
|
+
/// `attr_value_braces == 0` for the innermost element (React-like).
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
struct JsxEl {
|
|
23
|
+
in_opener: bool,
|
|
24
|
+
attr_value_braces: i32,
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
#[derive(Debug, Clone)]
|
|
18
28
|
pub struct Lexer<'a> {
|
|
19
29
|
chars: Peekable<Chars<'a>>,
|
|
@@ -27,7 +37,7 @@ pub struct Lexer<'a> {
|
|
|
27
37
|
jsx_after_gt: bool,
|
|
28
38
|
jsx_in_opening_tag: bool,
|
|
29
39
|
jsx_saw_slash_before_gt: bool,
|
|
30
|
-
|
|
40
|
+
jsx_stack: Vec<JsxEl>,
|
|
31
41
|
jsx_depth: i32,
|
|
32
42
|
jsx_child_brace_depth: i32,
|
|
33
43
|
jsx_in_closing_tag: bool,
|
|
@@ -47,13 +57,18 @@ impl<'a> Lexer<'a> {
|
|
|
47
57
|
jsx_after_gt: false,
|
|
48
58
|
jsx_in_opening_tag: false,
|
|
49
59
|
jsx_saw_slash_before_gt: false,
|
|
50
|
-
|
|
60
|
+
jsx_stack: Vec::new(),
|
|
51
61
|
jsx_depth: 0,
|
|
52
62
|
jsx_child_brace_depth: 0,
|
|
53
63
|
jsx_in_closing_tag: false,
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
#[inline]
|
|
68
|
+
fn jsx_sync_in_opening_tag(&mut self) {
|
|
69
|
+
self.jsx_in_opening_tag = self.jsx_stack.last().map(|e| e.in_opener).unwrap_or(false);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
fn read_jsx_text(&mut self, start: (usize, usize)) -> Result<Option<Token>, String> {
|
|
58
73
|
let mut s = String::new();
|
|
59
74
|
loop {
|
|
@@ -305,18 +320,33 @@ impl<'a> Lexer<'a> {
|
|
|
305
320
|
'(' => TokenKind::LParen,
|
|
306
321
|
')' => TokenKind::RParen,
|
|
307
322
|
'{' => {
|
|
308
|
-
if self.jsx_in_opening_tag {
|
|
309
|
-
|
|
323
|
+
if self.jsx_in_opening_tag {
|
|
324
|
+
if let Some(top) = self.jsx_stack.last_mut() {
|
|
325
|
+
top.attr_value_braces += 1;
|
|
326
|
+
}
|
|
327
|
+
} else if self.jsx_depth > 0 {
|
|
328
|
+
self.jsx_child_brace_depth += 1;
|
|
329
|
+
}
|
|
310
330
|
if let Some(depth) = self.template_brace_stack.last_mut() {
|
|
311
331
|
*depth += 1;
|
|
312
332
|
}
|
|
313
333
|
TokenKind::LBrace
|
|
314
334
|
}
|
|
315
335
|
'}' => {
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
let mut handled = false;
|
|
337
|
+
if let Some(top) = self.jsx_stack.last() {
|
|
338
|
+
if top.in_opener && top.attr_value_braces > 0 {
|
|
339
|
+
if let Some(top) = self.jsx_stack.last_mut() {
|
|
340
|
+
top.attr_value_braces -= 1;
|
|
341
|
+
}
|
|
342
|
+
handled = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if !handled && self.jsx_child_brace_depth > 0 {
|
|
318
346
|
self.jsx_child_brace_depth -= 1;
|
|
319
|
-
if self.jsx_child_brace_depth == 0 {
|
|
347
|
+
if self.jsx_child_brace_depth == 0 {
|
|
348
|
+
self.jsx_after_gt = true;
|
|
349
|
+
}
|
|
320
350
|
}
|
|
321
351
|
if let Some(depth) = self.template_brace_stack.last_mut() {
|
|
322
352
|
*depth -= 1;
|
|
@@ -358,6 +388,10 @@ impl<'a> Lexer<'a> {
|
|
|
358
388
|
else if self.peek() == Some('/') { self.jsx_in_closing_tag = true; TokenKind::Lt }
|
|
359
389
|
else if self.peek() == Some('>') || self.peek().map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
|
|
360
390
|
self.jsx_depth += 1;
|
|
391
|
+
self.jsx_stack.push(JsxEl {
|
|
392
|
+
in_opener: true,
|
|
393
|
+
attr_value_braces: 0,
|
|
394
|
+
});
|
|
361
395
|
self.jsx_in_opening_tag = true;
|
|
362
396
|
TokenKind::Lt
|
|
363
397
|
} else { TokenKind::Lt }
|
|
@@ -366,13 +400,23 @@ impl<'a> Lexer<'a> {
|
|
|
366
400
|
if self.peek() == Some('=') { self.advance(); TokenKind::Ge }
|
|
367
401
|
else if self.peek() == Some('>') { self.advance(); TokenKind::Shr }
|
|
368
402
|
else {
|
|
369
|
-
if self.
|
|
370
|
-
self.
|
|
371
|
-
|
|
372
|
-
|
|
403
|
+
if self.jsx_in_closing_tag {
|
|
404
|
+
self.jsx_depth = (self.jsx_depth - 1).max(0);
|
|
405
|
+
self.jsx_stack.pop();
|
|
406
|
+
self.jsx_sync_in_opening_tag();
|
|
407
|
+
} else if self.jsx_in_opening_tag && self.jsx_saw_slash_before_gt {
|
|
373
408
|
self.jsx_depth = (self.jsx_depth - 1).max(0);
|
|
409
|
+
self.jsx_stack.pop();
|
|
410
|
+
self.jsx_sync_in_opening_tag();
|
|
411
|
+
} else if let Some(top) = self.jsx_stack.last_mut() {
|
|
412
|
+
if top.in_opener && top.attr_value_braces > 0 {
|
|
413
|
+
// `>` is a comparison (or shift) token inside `{ ... }`, not end of opening tag.
|
|
414
|
+
} else if top.in_opener && !self.jsx_saw_slash_before_gt {
|
|
415
|
+
top.in_opener = false;
|
|
416
|
+
self.jsx_after_gt = true;
|
|
417
|
+
self.jsx_sync_in_opening_tag();
|
|
418
|
+
}
|
|
374
419
|
}
|
|
375
|
-
self.jsx_in_opening_tag = false;
|
|
376
420
|
self.jsx_in_closing_tag = false;
|
|
377
421
|
self.jsx_saw_slash_before_gt = false;
|
|
378
422
|
TokenKind::Gt
|
package/package.json
CHANGED
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
// Lattish: Lattish runtime for compiled JSX. Publish to npm; consumed by tish and tish-playground.
|
|
2
|
-
// With `tish compile --target js --jsx vdom`, the compiler injects a prelude that sets
|
|
3
|
-
// window.__LATTISH_JSX_VDOM and __lattishVdomPatch; createRoot then reconciles vnodes.
|
|
4
|
-
|
|
5
|
-
export let Fragment = Symbol("Lattish.Fragment")
|
|
6
|
-
|
|
7
|
-
fn __lattishAppend(parent, c) {
|
|
8
|
-
if (c === null || c === undefined) {
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
if (Array.isArray(c)) {
|
|
12
|
-
let i = 0
|
|
13
|
-
while (i < c.length) {
|
|
14
|
-
__lattishAppend(parent, c[i])
|
|
15
|
-
i = i + 1
|
|
16
|
-
}
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
if (typeof c === "string") {
|
|
20
|
-
parent.appendChild(document.createTextNode(c))
|
|
21
|
-
} else {
|
|
22
|
-
parent.appendChild(c)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export fn h(tag, props, children) {
|
|
27
|
-
if (children === undefined || children === null) {
|
|
28
|
-
children = []
|
|
29
|
-
}
|
|
30
|
-
if (typeof tag === "function") {
|
|
31
|
-
let p = props && props !== null ? props : {}
|
|
32
|
-
if (children.length > 0) {
|
|
33
|
-
let newProps = {}
|
|
34
|
-
let keys = Object.keys(p)
|
|
35
|
-
let i = 0
|
|
36
|
-
while (i < keys.length) {
|
|
37
|
-
newProps[keys[i]] = p[keys[i]]
|
|
38
|
-
i = i + 1
|
|
39
|
-
}
|
|
40
|
-
newProps.children = children
|
|
41
|
-
p = newProps
|
|
42
|
-
}
|
|
43
|
-
return tag(p)
|
|
44
|
-
}
|
|
45
|
-
if (tag === Fragment) {
|
|
46
|
-
let f = document.createDocumentFragment()
|
|
47
|
-
let i = 0
|
|
48
|
-
while (i < children.length) {
|
|
49
|
-
__lattishAppend(f, children[i])
|
|
50
|
-
i = i + 1
|
|
51
|
-
}
|
|
52
|
-
return f
|
|
53
|
-
}
|
|
54
|
-
let el = document.createElement(tag)
|
|
55
|
-
let refVal = null
|
|
56
|
-
if (props && props !== null) {
|
|
57
|
-
let keys = Object.keys(props)
|
|
58
|
-
let ki = 0
|
|
59
|
-
while (ki < keys.length) {
|
|
60
|
-
let k = keys[ki]
|
|
61
|
-
let v = props[k]
|
|
62
|
-
if (k === "ref") {
|
|
63
|
-
refVal = v
|
|
64
|
-
} else {
|
|
65
|
-
if (k === "disabled") {
|
|
66
|
-
el.disabled = !!v
|
|
67
|
-
} else {
|
|
68
|
-
if (v === true) {
|
|
69
|
-
el.setAttribute(k, k)
|
|
70
|
-
} else {
|
|
71
|
-
if (v !== false && v !== null && v !== undefined) {
|
|
72
|
-
if (k === "class" || k === "className") {
|
|
73
|
-
el.className = v
|
|
74
|
-
} else {
|
|
75
|
-
if (k.length >= 3 && k[0] === "o" && k[1] === "n" && typeof v === "function") {
|
|
76
|
-
let eventType = k.slice(2).toLowerCase()
|
|
77
|
-
el.addEventListener(eventType, v)
|
|
78
|
-
} else {
|
|
79
|
-
if (k === "value" && (tag === "input" || tag === "textarea")) {
|
|
80
|
-
el.value = v
|
|
81
|
-
} else {
|
|
82
|
-
if (k !== "ref") {
|
|
83
|
-
el.setAttribute(k, String(v))
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
ki = ki + 1
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
let ci = 0
|
|
96
|
-
while (ci < children.length) {
|
|
97
|
-
__lattishAppend(el, children[ci])
|
|
98
|
-
ci = ci + 1
|
|
99
|
-
}
|
|
100
|
-
if (refVal !== null && refVal !== undefined && typeof refVal === "object") {
|
|
101
|
-
refVal.current = el
|
|
102
|
-
}
|
|
103
|
-
return el
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export fn text(s) {
|
|
107
|
-
return s
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let __state = []
|
|
111
|
-
let __cursor = 0
|
|
112
|
-
let __container = null
|
|
113
|
-
let __App = null
|
|
114
|
-
let __rootVnode = null
|
|
115
|
-
|
|
116
|
-
let __memoSlots = []
|
|
117
|
-
let __refSlots = []
|
|
118
|
-
let __layoutEffectSlots = []
|
|
119
|
-
let __effectSlots = []
|
|
120
|
-
|
|
121
|
-
let __flushScheduled = false
|
|
122
|
-
let __batchDepth = 0
|
|
123
|
-
let __dirtyFromBatch = false
|
|
124
|
-
let __effectsMicrotaskScheduled = false
|
|
125
|
-
|
|
126
|
-
fn __depsChanged(prev, next) {
|
|
127
|
-
if (prev === null) {
|
|
128
|
-
return true
|
|
129
|
-
}
|
|
130
|
-
if (next === null || prev === null) {
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
if (prev.length !== next.length) {
|
|
134
|
-
return true
|
|
135
|
-
}
|
|
136
|
-
let j = 0
|
|
137
|
-
while (j < next.length) {
|
|
138
|
-
if (prev[j] !== next[j]) {
|
|
139
|
-
return true
|
|
140
|
-
}
|
|
141
|
-
j = j + 1
|
|
142
|
-
}
|
|
143
|
-
return false
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
fn __runLayoutEffects() {
|
|
147
|
-
let i = 0
|
|
148
|
-
while (i < __layoutEffectSlots.length) {
|
|
149
|
-
let s = __layoutEffectSlots[i]
|
|
150
|
-
if (s && s.pending) {
|
|
151
|
-
s.pending = false
|
|
152
|
-
if (s.cleanup) {
|
|
153
|
-
s.cleanup()
|
|
154
|
-
s.cleanup = null
|
|
155
|
-
}
|
|
156
|
-
let c = s.effect()
|
|
157
|
-
if (typeof c === "function") {
|
|
158
|
-
s.cleanup = c
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
i = i + 1
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
fn __scheduleEffectsFlush() {
|
|
166
|
-
if (__effectsMicrotaskScheduled) {
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
__effectsMicrotaskScheduled = true
|
|
170
|
-
queueMicrotask(() => {
|
|
171
|
-
__effectsMicrotaskScheduled = false
|
|
172
|
-
let i = 0
|
|
173
|
-
while (i < __effectSlots.length) {
|
|
174
|
-
let s = __effectSlots[i]
|
|
175
|
-
if (s && s.pending) {
|
|
176
|
-
s.pending = false
|
|
177
|
-
if (s.cleanup) {
|
|
178
|
-
s.cleanup()
|
|
179
|
-
s.cleanup = null
|
|
180
|
-
}
|
|
181
|
-
let c = s.effect()
|
|
182
|
-
if (typeof c === "function") {
|
|
183
|
-
s.cleanup = c
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
i = i + 1
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// Runs synchronously after DOM is updated (before paint in typical browsers).
|
|
192
|
-
export fn useLayoutEffect(effect, deps) {
|
|
193
|
-
let i = __cursor
|
|
194
|
-
__cursor = __cursor + 1
|
|
195
|
-
while (i >= __layoutEffectSlots.length) {
|
|
196
|
-
__layoutEffectSlots.push(null)
|
|
197
|
-
}
|
|
198
|
-
let slot = __layoutEffectSlots[i]
|
|
199
|
-
if (slot === null) {
|
|
200
|
-
slot = { deps: null, effect: null, cleanup: null, pending: false }
|
|
201
|
-
__layoutEffectSlots[i] = slot
|
|
202
|
-
}
|
|
203
|
-
if (__depsChanged(slot.deps, deps)) {
|
|
204
|
-
slot.deps = deps
|
|
205
|
-
slot.effect = effect
|
|
206
|
-
slot.pending = true
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/// Runs after paint (microtask after flush).
|
|
211
|
-
export fn useEffect(effect, deps) {
|
|
212
|
-
let i = __cursor
|
|
213
|
-
__cursor = __cursor + 1
|
|
214
|
-
while (i >= __effectSlots.length) {
|
|
215
|
-
__effectSlots.push(null)
|
|
216
|
-
}
|
|
217
|
-
let slot = __effectSlots[i]
|
|
218
|
-
if (slot === null) {
|
|
219
|
-
slot = { deps: null, effect: null, cleanup: null, pending: false }
|
|
220
|
-
__effectSlots[i] = slot
|
|
221
|
-
}
|
|
222
|
-
if (__depsChanged(slot.deps, deps)) {
|
|
223
|
-
slot.deps = deps
|
|
224
|
-
slot.effect = effect
|
|
225
|
-
slot.pending = true
|
|
226
|
-
__scheduleEffectsFlush()
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
fn __usingVdom() {
|
|
231
|
-
return typeof window !== "undefined" && window.__LATTISH_JSX_VDOM && window.__lattishVdomPatch
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
fn __flushRender() {
|
|
235
|
-
__cursor = 0
|
|
236
|
-
if (__container && __App) {
|
|
237
|
-
if (__usingVdom()) {
|
|
238
|
-
let newTree = __App()
|
|
239
|
-
window.__lattishVdomPatch(__container, __rootVnode, newTree)
|
|
240
|
-
__rootVnode = newTree
|
|
241
|
-
} else {
|
|
242
|
-
let el = __App()
|
|
243
|
-
__container.replaceChildren(el)
|
|
244
|
-
__rootVnode = null
|
|
245
|
-
}
|
|
246
|
-
__runLayoutEffects()
|
|
247
|
-
__scheduleEffectsFlush()
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
fn __scheduleFlush() {
|
|
252
|
-
if (__batchDepth > 0) {
|
|
253
|
-
__dirtyFromBatch = true
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
if (__flushScheduled) {
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
__flushScheduled = true
|
|
260
|
-
queueMicrotask(() => {
|
|
261
|
-
__flushScheduled = false
|
|
262
|
-
__flushRender()
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export fn unstable_batchedUpdates(run) {
|
|
267
|
-
__batchDepth = __batchDepth + 1
|
|
268
|
-
run()
|
|
269
|
-
__batchDepth = __batchDepth - 1
|
|
270
|
-
if (__batchDepth === 0 && __dirtyFromBatch) {
|
|
271
|
-
__dirtyFromBatch = false
|
|
272
|
-
__flushScheduled = false
|
|
273
|
-
__flushRender()
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export fn useState(init) {
|
|
278
|
-
let i = __cursor
|
|
279
|
-
__cursor = __cursor + 1
|
|
280
|
-
if (i >= __state.length) {
|
|
281
|
-
__state.push(init)
|
|
282
|
-
}
|
|
283
|
-
fn setState(v) {
|
|
284
|
-
if (typeof v === "function") {
|
|
285
|
-
__state[i] = v(__state[i])
|
|
286
|
-
} else {
|
|
287
|
-
__state[i] = v
|
|
288
|
-
}
|
|
289
|
-
__scheduleFlush()
|
|
290
|
-
}
|
|
291
|
-
return [__state[i], setState]
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export fn useMemo(deps, factory) {
|
|
295
|
-
let i = __cursor
|
|
296
|
-
__cursor = __cursor + 1
|
|
297
|
-
while (i >= __memoSlots.length) {
|
|
298
|
-
__memoSlots.push(null)
|
|
299
|
-
}
|
|
300
|
-
let slot = __memoSlots[i]
|
|
301
|
-
let stale = true
|
|
302
|
-
if (slot) {
|
|
303
|
-
stale = false
|
|
304
|
-
if (slot.deps.length !== deps.length) {
|
|
305
|
-
stale = true
|
|
306
|
-
} else {
|
|
307
|
-
let j = 0
|
|
308
|
-
while (j < deps.length) {
|
|
309
|
-
if (slot.deps[j] !== deps[j]) {
|
|
310
|
-
stale = true
|
|
311
|
-
break
|
|
312
|
-
}
|
|
313
|
-
j = j + 1
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (stale) {
|
|
318
|
-
__memoSlots[i] = { deps: deps, value: factory() }
|
|
319
|
-
}
|
|
320
|
-
return __memoSlots[i].value
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export fn useRef(initial) {
|
|
324
|
-
let i = __cursor
|
|
325
|
-
__cursor = __cursor + 1
|
|
326
|
-
while (i >= __refSlots.length) {
|
|
327
|
-
__refSlots.push(null)
|
|
328
|
-
}
|
|
329
|
-
if (__refSlots[i] === null) {
|
|
330
|
-
__refSlots[i] = { current: initial }
|
|
331
|
-
}
|
|
332
|
-
return __refSlots[i]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export fn createRoot(container) {
|
|
336
|
-
fn render(App) {
|
|
337
|
-
__container = container
|
|
338
|
-
__App = App
|
|
339
|
-
__state = []
|
|
340
|
-
__memoSlots = []
|
|
341
|
-
__refSlots = []
|
|
342
|
-
__layoutEffectSlots = []
|
|
343
|
-
__effectSlots = []
|
|
344
|
-
__cursor = 0
|
|
345
|
-
__flushScheduled = false
|
|
346
|
-
__batchDepth = 0
|
|
347
|
-
__dirtyFromBatch = false
|
|
348
|
-
__effectsMicrotaskScheduled = false
|
|
349
|
-
__rootVnode = null
|
|
350
|
-
if (__usingVdom()) {
|
|
351
|
-
__rootVnode = App()
|
|
352
|
-
window.__lattishVdomPatch(container, null, __rootVnode)
|
|
353
|
-
} else {
|
|
354
|
-
let el = App()
|
|
355
|
-
container.replaceChildren(el)
|
|
356
|
-
}
|
|
357
|
-
__cursor = 0
|
|
358
|
-
__runLayoutEffects()
|
|
359
|
-
__scheduleEffectsFlush()
|
|
360
|
-
}
|
|
361
|
-
return { render: render }
|
|
362
|
-
}
|