@tishlang/tish 1.0.27 → 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.27"
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 }
@@ -163,4 +163,50 @@ fn FileList() {
163
163
  &js[..600.min(js.len())]
164
164
  );
165
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
+ }
166
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.27",
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