@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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.0.26"
3
+ version = "1.0.28"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -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
- let needs_string = !matches!(
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::String(_),
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
- jsx_brace_depth: i32,
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
- jsx_brace_depth: 0,
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 { self.jsx_brace_depth += 1; }
309
- else if self.jsx_depth > 0 { self.jsx_child_brace_depth += 1; }
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
- if self.jsx_brace_depth > 0 { self.jsx_brace_depth -= 1; }
317
- else if self.jsx_child_brace_depth > 0 {
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 { self.jsx_after_gt = true; }
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.jsx_in_opening_tag && self.jsx_brace_depth == 0 && !self.jsx_saw_slash_before_gt {
370
- self.jsx_after_gt = true;
371
- }
372
- if self.jsx_in_closing_tag || (self.jsx_in_opening_tag && self.jsx_saw_slash_before_gt) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, compile to native.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file