@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.
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +46 -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
|
@@ -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
|
-
|
|
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
|