@tishlang/tish 1.0.26 → 1.0.28
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_compile_js/src/codegen.rs +4 -5
- package/crates/tish_compile_js/src/tests_jsx.rs +113 -0
- 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/Cargo.toml
CHANGED
|
@@ -735,15 +735,14 @@ impl Codegen {
|
|
|
735
735
|
JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
|
|
736
736
|
JsxChild::Expr(e) => {
|
|
737
737
|
let inner = self.emit_expr(e)?;
|
|
738
|
-
|
|
738
|
+
// Only wrap literals we know are primitives (number, bool, null). Never wrap:
|
|
739
|
+
// string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
|
|
740
|
+
let needs_string = matches!(
|
|
739
741
|
e,
|
|
740
742
|
Expr::Literal {
|
|
741
|
-
value: Literal::
|
|
743
|
+
value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
|
|
742
744
|
..
|
|
743
745
|
}
|
|
744
|
-
| Expr::TemplateLiteral { .. }
|
|
745
|
-
| Expr::JsxElement { .. }
|
|
746
|
-
| Expr::JsxFragment { .. }
|
|
747
746
|
);
|
|
748
747
|
Ok(if needs_string {
|
|
749
748
|
format!("String({})", inner)
|
|
@@ -96,4 +96,117 @@ mod tests {
|
|
|
96
96
|
assert!(js.contains("__vdom_h(\"p\", null, [])"), "{}", &js[..600.min(js.len())]);
|
|
97
97
|
assert!(js.contains("__lattishVdomPatch"));
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/// Component calls like {Panel()} return DOM elements. Wrapping in String() produces [object HTMLDivElement].
|
|
101
|
+
#[test]
|
|
102
|
+
fn jsx_component_call_not_wrapped_in_string() {
|
|
103
|
+
let src = r#"
|
|
104
|
+
fn Panel() { return <div class="p">content</div> }
|
|
105
|
+
fn App() { return <div>{Panel()}</div> }
|
|
106
|
+
"#;
|
|
107
|
+
let program = parse(src).unwrap();
|
|
108
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
109
|
+
assert!(
|
|
110
|
+
js.contains("Panel()"),
|
|
111
|
+
"component call should appear as Panel(), got: {}",
|
|
112
|
+
&js[..500.min(js.len())]
|
|
113
|
+
);
|
|
114
|
+
assert!(
|
|
115
|
+
!js.contains("String(Panel()"),
|
|
116
|
+
"component calls must NOT be wrapped in String() - causes [object HTMLDivElement]. got: {}",
|
|
117
|
+
&js[..600.min(js.len())]
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Nested JSX elements must not be String()'d or they render as [object HTMLDivElement].
|
|
122
|
+
#[test]
|
|
123
|
+
fn jsx_nested_element_not_wrapped_in_string() {
|
|
124
|
+
let src = r#"fn X() { return <div><span>inner</span></div> }"#;
|
|
125
|
+
let program = parse(src).unwrap();
|
|
126
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
127
|
+
assert!(
|
|
128
|
+
!js.contains("String(h("),
|
|
129
|
+
"nested JSX elements must NOT be wrapped in String(). got: {}",
|
|
130
|
+
&js[..500.min(js.len())]
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Literal number/bool/null get String() for display. Idents (e.g. {items}) are NOT wrapped—they may hold elements.
|
|
135
|
+
#[test]
|
|
136
|
+
fn jsx_literal_number_wrapped_in_string() {
|
|
137
|
+
let src = r#"fn X() { return <span>{42}</span> }"#;
|
|
138
|
+
let program = parse(src).unwrap();
|
|
139
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
140
|
+
assert!(
|
|
141
|
+
js.contains("String(42)"),
|
|
142
|
+
"literal number in JSX should be wrapped in String(). got: {}",
|
|
143
|
+
&js[..500.min(js.len())]
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Array/ident like {items} (array of buttons) must NOT be String()'d or we get [object HTMLButtonElement].
|
|
148
|
+
#[test]
|
|
149
|
+
fn jsx_array_of_elements_not_wrapped_in_string() {
|
|
150
|
+
let src = r#"
|
|
151
|
+
fn FileList() {
|
|
152
|
+
let items = []
|
|
153
|
+
items.push(<button>a</button>)
|
|
154
|
+
items.push(<button>b</button>)
|
|
155
|
+
return <div>{items}</div>
|
|
156
|
+
}
|
|
157
|
+
"#;
|
|
158
|
+
let program = parse(src).unwrap();
|
|
159
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
160
|
+
assert!(
|
|
161
|
+
!js.contains("String(items)"),
|
|
162
|
+
"array/ident in JSX must NOT be wrapped in String() - causes [object HTMLButtonElement]. got: {}",
|
|
163
|
+
&js[..600.min(js.len())]
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// `>` inside `{ ... }` attribute values must be a comparison operator, not end of opening tag.
|
|
168
|
+
#[test]
|
|
169
|
+
fn jsx_gt_comparison_inside_attribute_expression() {
|
|
170
|
+
let src = r#"fn X() {
|
|
171
|
+
return <button
|
|
172
|
+
type="button"
|
|
173
|
+
onclick={() => {
|
|
174
|
+
let nm = "a"
|
|
175
|
+
if (nm && nm.length > 0) { print(nm) }
|
|
176
|
+
}}
|
|
177
|
+
>{"ok"}</button>
|
|
178
|
+
}"#;
|
|
179
|
+
let program = parse(src).expect("parse multi-line JSX with > comparison in attr");
|
|
180
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).expect("compile");
|
|
181
|
+
assert!(
|
|
182
|
+
js.contains("length > 0") || js.contains("length>0"),
|
|
183
|
+
"expected compiled JS to preserve greater-than comparison, got: {}",
|
|
184
|
+
&js[..800.min(js.len())]
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Nested JSX inside an attribute callback must still close inner `<tag>` correctly.
|
|
189
|
+
#[test]
|
|
190
|
+
fn jsx_nested_element_inside_attribute_expression() {
|
|
191
|
+
let src = r#"fn X() {
|
|
192
|
+
return <button
|
|
193
|
+
onclick={() => {
|
|
194
|
+
let x = <span>{"inner"}</span>
|
|
195
|
+
print(x)
|
|
196
|
+
}}
|
|
197
|
+
>{"outer"}</button>
|
|
198
|
+
}"#;
|
|
199
|
+
let program = parse(src).expect("parse nested JSX inside onclick");
|
|
200
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).expect("compile");
|
|
201
|
+
assert!(
|
|
202
|
+
js.contains("\"inner\""),
|
|
203
|
+
"expected nested span text in output, got: {}",
|
|
204
|
+
&js[..900.min(js.len())]
|
|
205
|
+
);
|
|
206
|
+
assert!(
|
|
207
|
+
js.contains("\"outer\""),
|
|
208
|
+
"expected button child text in output, got: {}",
|
|
209
|
+
&js[..900.min(js.len())]
|
|
210
|
+
);
|
|
211
|
+
}
|
|
99
212
|
}
|
|
@@ -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
|