typescratch 1.6.0__tar.gz → 1.6.2__tar.gz
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.
- {typescratch-1.6.0 → typescratch-1.6.2}/PKG-INFO +1 -1
- {typescratch-1.6.0 → typescratch-1.6.2}/pyproject.toml +1 -1
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/__init__.py +1 -1
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/blocks.py +46 -5
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/decompiler.py +50 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/PKG-INFO +1 -1
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/SOURCES.txt +1 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/__init__.py +1 -1
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/ast_nodes.py +36 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/blocks.py +13 -5
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/cli.py +20 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/codegen.py +129 -3
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/decompiler.py +47 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/lexer.py +3 -0
- typescratch-1.6.2/typescratch_pypi_release/typescratch_src/package_manager.py +127 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/parser.py +129 -8
- {typescratch-1.6.0 → typescratch-1.6.2}/LICENSE +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/README.md +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/setup.cfg +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/setup.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/tests/test_basic.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/__main__.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/assets.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/ast_nodes.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/cli.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/codegen.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/errors.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/ide.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/lexer.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/logo.svg +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/logo_ascii.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/meow.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/package_manager.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/parser.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/sb3.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch/scratchcat_official.svg +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/dependency_links.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/entry_points.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/requires.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch.egg-info/top_level.txt +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/setup.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/__main__.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/assets.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/errors.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/ide.py +0 -0
- {typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/sb3.py +0 -0
|
@@ -153,25 +153,51 @@ STACK_BLOCKS = {
|
|
|
153
153
|
"args": [],
|
|
154
154
|
},
|
|
155
155
|
"goToFront": {
|
|
156
|
+
"opcode": "looks_gotofrontback",
|
|
157
|
+
"fields": [("FRONT_BACK", "front")],
|
|
158
|
+
},
|
|
159
|
+
"goToBack": {
|
|
160
|
+
"opcode": "looks_gotofrontback",
|
|
161
|
+
"fields": [("FRONT_BACK", "back")],
|
|
162
|
+
},
|
|
163
|
+
"goFront": {
|
|
164
|
+
"opcode": "looks_gotofrontback",
|
|
165
|
+
"fields": [("FRONT_BACK", "front")],
|
|
166
|
+
},
|
|
167
|
+
"goBack": {
|
|
168
|
+
"opcode": "looks_gotofrontback",
|
|
169
|
+
"fields": [("FRONT_BACK", "back")],
|
|
170
|
+
},
|
|
171
|
+
"goForwardLayers": {
|
|
156
172
|
"opcode": "looks_goforwardbackwardlayers",
|
|
157
173
|
"fields": [("FRONTBACK", "front")],
|
|
158
|
-
"args": [("NUM", "number", "args"
|
|
174
|
+
"args": [("NUM", "number", "args")],
|
|
159
175
|
},
|
|
160
|
-
"
|
|
176
|
+
"goBackLayers": {
|
|
161
177
|
"opcode": "looks_goforwardbackwardlayers",
|
|
162
178
|
"fields": [("FRONTBACK", "back")],
|
|
163
|
-
"args": [("NUM", "number", "args"
|
|
179
|
+
"args": [("NUM", "number", "args")],
|
|
164
180
|
},
|
|
165
|
-
"
|
|
181
|
+
"goFwdLyrs": {
|
|
182
|
+
"opcode": "looks_goforwardbackwardlayers",
|
|
183
|
+
"fields": [("FRONTBACK", "front")],
|
|
184
|
+
"args": [("NUM", "number", "args")],
|
|
185
|
+
},
|
|
186
|
+
"goBwdLyrs": {
|
|
166
187
|
"opcode": "looks_goforwardbackwardlayers",
|
|
167
188
|
"fields": [("FRONTBACK", "back")],
|
|
168
189
|
"args": [("NUM", "number", "args")],
|
|
169
190
|
},
|
|
170
|
-
"
|
|
191
|
+
"goFwdLayers": {
|
|
171
192
|
"opcode": "looks_goforwardbackwardlayers",
|
|
172
193
|
"fields": [("FRONTBACK", "front")],
|
|
173
194
|
"args": [("NUM", "number", "args")],
|
|
174
195
|
},
|
|
196
|
+
"goBwdLayers": {
|
|
197
|
+
"opcode": "looks_goforwardbackwardlayers",
|
|
198
|
+
"fields": [("FRONTBACK", "back")],
|
|
199
|
+
"args": [("NUM", "number", "args")],
|
|
200
|
+
},
|
|
175
201
|
|
|
176
202
|
# ---- Sound ----
|
|
177
203
|
"playSound": {
|
|
@@ -552,8 +578,23 @@ REPORTERS = {
|
|
|
552
578
|
# Looks
|
|
553
579
|
"costumeName": {"opcode": "looks_costumename", "returns": "string"},
|
|
554
580
|
"costumeNumber": {"opcode": "looks_costumenumber", "returns": "number"},
|
|
581
|
+
"costumeNumberName": {
|
|
582
|
+
"opcode": "looks_costumenumbername",
|
|
583
|
+
"fields": [("NUMBER_NAME", None)],
|
|
584
|
+
"returns": "any",
|
|
585
|
+
},
|
|
586
|
+
"cstmNumName": {
|
|
587
|
+
"opcode": "looks_costumenumbername",
|
|
588
|
+
"fields": [("NUMBER_NAME", None)],
|
|
589
|
+
"returns": "any",
|
|
590
|
+
},
|
|
555
591
|
"backdropName": {"opcode": "looks_backdropname", "returns": "string"},
|
|
556
592
|
"backdropNumber": {"opcode": "looks_backdropnumber", "returns": "number"},
|
|
593
|
+
"backdropNumberName": {
|
|
594
|
+
"opcode": "looks_backdropnumbername",
|
|
595
|
+
"fields": [("NUMBER_NAME", None)],
|
|
596
|
+
"returns": "any",
|
|
597
|
+
},
|
|
557
598
|
"size": {"opcode": "looks_size", "returns": "number"},
|
|
558
599
|
|
|
559
600
|
# Sound
|
|
@@ -478,6 +478,22 @@ class Decompiler:
|
|
|
478
478
|
self._decompile_generic_block(blocks, block, ts_key, spec, indent)
|
|
479
479
|
return
|
|
480
480
|
|
|
481
|
+
# Looks: go to front/back
|
|
482
|
+
if op == "looks_gotofrontback":
|
|
483
|
+
fb = block.get("fields", {}).get("FRONT_BACK", ["front"])[0]
|
|
484
|
+
self.output.append(f"{indent}goTo{fb.capitalize()}")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
# Looks: go forward/backward layers
|
|
488
|
+
if op == "looks_goforwardbackwardlayers":
|
|
489
|
+
fb = block.get("fields", {}).get("FRONTBACK", ["front"])[0]
|
|
490
|
+
num = self._decompile_input(blocks, block, "NUM")
|
|
491
|
+
if fb == "front":
|
|
492
|
+
self.output.append(f"{indent}goForwardLayers({num})")
|
|
493
|
+
else:
|
|
494
|
+
self.output.append(f"{indent}goBackLayers({num})")
|
|
495
|
+
return
|
|
496
|
+
|
|
481
497
|
# Unknown block - output as comment
|
|
482
498
|
self.output.append(f"{indent}// TODO: unknown block {op}")
|
|
483
499
|
|
|
@@ -725,6 +741,12 @@ class Decompiler:
|
|
|
725
741
|
return "costumeName"
|
|
726
742
|
if op == "looks_costumenumber":
|
|
727
743
|
return "costumeNumber"
|
|
744
|
+
if op == "looks_costumenumbername":
|
|
745
|
+
field = block.get("fields", {}).get("NUMBER_NAME", [""])[0]
|
|
746
|
+
return f"costumeNumberName({field})"
|
|
747
|
+
if op == "looks_backdropnumbername":
|
|
748
|
+
field = block.get("fields", {}).get("NUMBER_NAME", [""])[0]
|
|
749
|
+
return f"backdropNumberName({field})"
|
|
728
750
|
|
|
729
751
|
# Sound
|
|
730
752
|
if op == "sound_volume":
|
|
@@ -793,6 +815,34 @@ class Decompiler:
|
|
|
793
815
|
args_str = ", ".join(str(a) for a in all_args)
|
|
794
816
|
return f"{ts_key}({args_str})" if all_args else ts_key
|
|
795
817
|
|
|
818
|
+
# Menu shadow blocks (used as field values in dropdowns)
|
|
819
|
+
if op == "motion_goto_menu":
|
|
820
|
+
return block.get("fields", {}).get("TO", [""])[0]
|
|
821
|
+
if op == "motion_pointtowards_menu":
|
|
822
|
+
return block.get("fields", {}).get("TOWARDS", [""])[0]
|
|
823
|
+
if op == "motion_glideto_menu":
|
|
824
|
+
return block.get("fields", {}).get("TO", [""])[0]
|
|
825
|
+
if op == "sensing_of_object_menu":
|
|
826
|
+
return block.get("fields", {}).get("OBJECT", [""])[0]
|
|
827
|
+
if op == "sensing_touchingobjectmenu":
|
|
828
|
+
return block.get("fields", {}).get("TOUCHINGOBJECTMENU", [""])[0]
|
|
829
|
+
if op == "sensing_distancetomenu":
|
|
830
|
+
return block.get("fields", {}).get("DISTANCETOMENU", [""])[0]
|
|
831
|
+
if op == "sensing_keyoptions":
|
|
832
|
+
return block.get("fields", {}).get("KEY_OPTION", [""])[0]
|
|
833
|
+
if op == "control_create_clone_of_menu":
|
|
834
|
+
return block.get("fields", {}).get("CLONE_OPTION", [""])[0]
|
|
835
|
+
if op == "looks_backdrops":
|
|
836
|
+
return block.get("fields", {}).get("BACKDROP", [""])[0]
|
|
837
|
+
if op == "looks_costume":
|
|
838
|
+
return block.get("fields", {}).get("COSTUME", [""])[0]
|
|
839
|
+
if op == "sound_sounds_menu":
|
|
840
|
+
return block.get("fields", {}).get("SOUND_MENU", [""])[0]
|
|
841
|
+
if op == "sound_beats_menu":
|
|
842
|
+
return block.get("fields", {}).get("BEAT", [""])[0]
|
|
843
|
+
if op == "sound_effects_menu":
|
|
844
|
+
return block.get("fields", {}).get("EFFECT", [""])[0]
|
|
845
|
+
|
|
796
846
|
# Unknown reporter
|
|
797
847
|
return f"/* {op} */"
|
|
798
848
|
|
|
@@ -39,5 +39,6 @@ typescratch_pypi_release/typescratch_src/decompiler.py
|
|
|
39
39
|
typescratch_pypi_release/typescratch_src/errors.py
|
|
40
40
|
typescratch_pypi_release/typescratch_src/ide.py
|
|
41
41
|
typescratch_pypi_release/typescratch_src/lexer.py
|
|
42
|
+
typescratch_pypi_release/typescratch_src/package_manager.py
|
|
42
43
|
typescratch_pypi_release/typescratch_src/parser.py
|
|
43
44
|
typescratch_pypi_release/typescratch_src/sb3.py
|
{typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/ast_nodes.py
RENAMED
|
@@ -361,3 +361,39 @@ class ListReporterRef:
|
|
|
361
361
|
method: str # 'item' | 'length' | 'contains' | 'itemNum' | 'contents'
|
|
362
362
|
args: List[Any]
|
|
363
363
|
line: int = 0
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class TernaryExpr:
|
|
368
|
+
"""Ternary expression: [if (cond) true_val else false_val]
|
|
369
|
+
|
|
370
|
+
Desugared at codegen time into if/else that sets a temp variable.
|
|
371
|
+
"""
|
|
372
|
+
cond: Any
|
|
373
|
+
true_expr: Any
|
|
374
|
+
false_expr: Any
|
|
375
|
+
line: int = 0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dataclass
|
|
379
|
+
class SoundImport:
|
|
380
|
+
"""sound name from "path.wav" - imports a sound file."""
|
|
381
|
+
name: str
|
|
382
|
+
path: str
|
|
383
|
+
line: int = 0
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass
|
|
387
|
+
class CostumeImport:
|
|
388
|
+
"""costume name from "path.png" - imports a costume file."""
|
|
389
|
+
name: str
|
|
390
|
+
path: str
|
|
391
|
+
line: int = 0
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class LocalVarDecl:
|
|
396
|
+
"""private x = value - declares a local variable inside a custom block."""
|
|
397
|
+
name: str
|
|
398
|
+
value: Any
|
|
399
|
+
line: int = 0
|
|
@@ -153,14 +153,17 @@ STACK_BLOCKS = {
|
|
|
153
153
|
"args": [],
|
|
154
154
|
},
|
|
155
155
|
"goToFront": {
|
|
156
|
-
"opcode": "
|
|
157
|
-
"fields": [("
|
|
158
|
-
"args": [("NUM", "number", "args", True)], # optional - default 1
|
|
156
|
+
"opcode": "looks_gotofrontback",
|
|
157
|
+
"fields": [("FRONT_BACK", "front")],
|
|
159
158
|
},
|
|
160
159
|
"goToBack": {
|
|
160
|
+
"opcode": "looks_gotofrontback",
|
|
161
|
+
"fields": [("FRONT_BACK", "back")],
|
|
162
|
+
},
|
|
163
|
+
"goForwardLayers": {
|
|
161
164
|
"opcode": "looks_goforwardbackwardlayers",
|
|
162
|
-
"fields": [("FRONTBACK", "
|
|
163
|
-
"args": [("NUM", "number", "args"
|
|
165
|
+
"fields": [("FRONTBACK", "front")],
|
|
166
|
+
"args": [("NUM", "number", "args")],
|
|
164
167
|
},
|
|
165
168
|
"goBackLayers": {
|
|
166
169
|
"opcode": "looks_goforwardbackwardlayers",
|
|
@@ -552,6 +555,11 @@ REPORTERS = {
|
|
|
552
555
|
# Looks
|
|
553
556
|
"costumeName": {"opcode": "looks_costumename", "returns": "string"},
|
|
554
557
|
"costumeNumber": {"opcode": "looks_costumenumber", "returns": "number"},
|
|
558
|
+
"costumeNumberName": {
|
|
559
|
+
"opcode": "looks_costumenumbername",
|
|
560
|
+
"fields": [("NUMBER_NAME", None)],
|
|
561
|
+
"returns": "any",
|
|
562
|
+
},
|
|
555
563
|
"backdropName": {"opcode": "looks_backdropname", "returns": "string"},
|
|
556
564
|
"backdropNumber": {"opcode": "looks_backdropnumber", "returns": "number"},
|
|
557
565
|
"size": {"opcode": "looks_size", "returns": "number"},
|
|
@@ -51,6 +51,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
51
51
|
|
|
52
52
|
sub.add_parser("meow", help="Print the Scratch Cat in ASCII art")
|
|
53
53
|
|
|
54
|
+
p_install = sub.add_parser("install", help="Install a package from GitHub")
|
|
55
|
+
p_install.add_argument("repo", help="github user/repo or user/repo@tag")
|
|
56
|
+
|
|
57
|
+
p_uninstall = sub.add_parser("uninstall", help="Remove an installed package")
|
|
58
|
+
p_uninstall.add_argument("name", help="package name")
|
|
59
|
+
|
|
60
|
+
sub.add_parser("list", help="List installed packages")
|
|
61
|
+
|
|
54
62
|
sub.add_parser("version", help="Print version")
|
|
55
63
|
|
|
56
64
|
args = parser.parse_args(argv)
|
|
@@ -80,6 +88,18 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
80
88
|
if args.cmd == "meow":
|
|
81
89
|
return _meow()
|
|
82
90
|
|
|
91
|
+
if args.cmd == "install":
|
|
92
|
+
from .package_manager import install
|
|
93
|
+
return install(args.repo)
|
|
94
|
+
|
|
95
|
+
if args.cmd == "uninstall":
|
|
96
|
+
from .package_manager import uninstall
|
|
97
|
+
return uninstall(args.name)
|
|
98
|
+
|
|
99
|
+
if args.cmd == "list":
|
|
100
|
+
from .package_manager import list_packages
|
|
101
|
+
return list_packages()
|
|
102
|
+
|
|
83
103
|
if args.cmd in ("build", "run"):
|
|
84
104
|
try:
|
|
85
105
|
out_path = _file(args.source, out=args.out, debug=args.debug)
|
|
@@ -476,7 +476,7 @@ class Codegen:
|
|
|
476
476
|
"rotationCenterY": rcy,
|
|
477
477
|
})
|
|
478
478
|
else:
|
|
479
|
-
# Sprite costume
|
|
479
|
+
# Sprite costume - use img= if specified
|
|
480
480
|
path = target.img
|
|
481
481
|
data, _ignored_md5ext, fmt, rcx, rcy = self._load_asset(path, is_stage=False)
|
|
482
482
|
asset_id = _md5(data)
|
|
@@ -491,11 +491,73 @@ class Codegen:
|
|
|
491
491
|
"rotationCenterX": rcx,
|
|
492
492
|
"rotationCenterY": rcy,
|
|
493
493
|
})
|
|
494
|
+
# Additional costumes from `costume name from "path.png"` imports
|
|
495
|
+
for cinfo in getattr(target, "_costume_imports", []):
|
|
496
|
+
cname = cinfo["name"]
|
|
497
|
+
cpath = cinfo["path"]
|
|
498
|
+
cdata, _cmd5, cfmt, crcx, crcy = self._load_asset(cpath, is_stage=False)
|
|
499
|
+
casset_id = _md5(cdata)
|
|
500
|
+
cmd5ext = f"{casset_id}.{cfmt}"
|
|
501
|
+
self.assets[cmd5ext] = cdata
|
|
502
|
+
costumes.append({
|
|
503
|
+
"assetId": casset_id,
|
|
504
|
+
"name": cname,
|
|
505
|
+
"bitmapResolution": 1 if cfmt == "svg" else 2,
|
|
506
|
+
"md5ext": cmd5ext,
|
|
507
|
+
"dataFormat": cfmt,
|
|
508
|
+
"rotationCenterX": crcx,
|
|
509
|
+
"rotationCenterY": crcy,
|
|
510
|
+
})
|
|
494
511
|
return costumes
|
|
495
512
|
|
|
496
513
|
def gen_sounds(self, target: A.Target) -> List[dict]:
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
"""Generate sound assets from `sound name from "path.wav"` imports."""
|
|
515
|
+
sounds = []
|
|
516
|
+
for sinfo in getattr(target, "_sound_imports", []):
|
|
517
|
+
name = sinfo["name"]
|
|
518
|
+
path = sinfo["path"]
|
|
519
|
+
full = path
|
|
520
|
+
if self.asset_root and not os.path.isabs(path):
|
|
521
|
+
full = os.path.join(self.asset_root, path)
|
|
522
|
+
if os.path.isfile(full):
|
|
523
|
+
with open(full, "rb") as f:
|
|
524
|
+
data = f.read()
|
|
525
|
+
# Determine format
|
|
526
|
+
if full.lower().endswith(".wav"):
|
|
527
|
+
fmt = "wav"
|
|
528
|
+
elif full.lower().endswith(".mp3"):
|
|
529
|
+
fmt = "mp3"
|
|
530
|
+
else:
|
|
531
|
+
fmt = "wav"
|
|
532
|
+
asset_id = _md5(data)
|
|
533
|
+
md5ext = f"{asset_id}.{fmt}"
|
|
534
|
+
self.assets[md5ext] = data
|
|
535
|
+
sounds.append({
|
|
536
|
+
"assetId": asset_id,
|
|
537
|
+
"name": name,
|
|
538
|
+
"dataFormat": fmt,
|
|
539
|
+
"format": "",
|
|
540
|
+
"rate": 48000,
|
|
541
|
+
"sampleCount": 0,
|
|
542
|
+
"md5ext": md5ext,
|
|
543
|
+
})
|
|
544
|
+
else:
|
|
545
|
+
# File not found - use silent fallback
|
|
546
|
+
from . import assets as ASSETS2
|
|
547
|
+
data = ASSETS2.SILENT_WAV
|
|
548
|
+
asset_id = _md5(data)
|
|
549
|
+
md5ext = f"{asset_id}.wav"
|
|
550
|
+
self.assets[md5ext] = data
|
|
551
|
+
sounds.append({
|
|
552
|
+
"assetId": asset_id,
|
|
553
|
+
"name": name,
|
|
554
|
+
"dataFormat": "wav",
|
|
555
|
+
"format": "",
|
|
556
|
+
"rate": 48000,
|
|
557
|
+
"sampleCount": 0,
|
|
558
|
+
"md5ext": md5ext,
|
|
559
|
+
})
|
|
560
|
+
return sounds
|
|
499
561
|
|
|
500
562
|
def _load_asset(self, path: Optional[str], is_stage: bool) -> Tuple[bytes, str, str, int, int]:
|
|
501
563
|
"""Returns (data, md5ext, format, rotationCenterX, rotationCenterY)."""
|
|
@@ -676,6 +738,11 @@ class Codegen:
|
|
|
676
738
|
parent_id, stmt.line)
|
|
677
739
|
if isinstance(stmt, A.BlockCall):
|
|
678
740
|
return self.gen_block_call(stmt, parent_id)
|
|
741
|
+
if isinstance(stmt, A.LocalVarDecl):
|
|
742
|
+
return self.gen_var_assign(
|
|
743
|
+
A.VarAssign(name=stmt.name, value=stmt.value, line=stmt.line),
|
|
744
|
+
parent_id
|
|
745
|
+
)
|
|
679
746
|
raise CompileError(f"unknown statement type: {type(stmt).__name__}", stmt.line)
|
|
680
747
|
|
|
681
748
|
def gen_simple_stack(self, opcode: str, inputs_spec, parent_id, line) -> Tuple[str, str]:
|
|
@@ -1485,6 +1552,8 @@ class Codegen:
|
|
|
1485
1552
|
return None, block_id
|
|
1486
1553
|
if isinstance(expr, A.ListReporterRef):
|
|
1487
1554
|
return self._gen_list_reporter(expr)
|
|
1555
|
+
if isinstance(expr, A.TernaryExpr):
|
|
1556
|
+
return self._gen_ternary(expr)
|
|
1488
1557
|
if isinstance(expr, A.BuiltinRef):
|
|
1489
1558
|
# Builtin reporter (e.g. answer, mouseX)
|
|
1490
1559
|
spec = B.REPORTERS.get(expr.name)
|
|
@@ -1544,6 +1613,63 @@ class Codegen:
|
|
|
1544
1613
|
# default: string literal
|
|
1545
1614
|
return A.StringLit(value=name, line=ref.line)
|
|
1546
1615
|
|
|
1616
|
+
def _gen_ternary(self, expr: A.TernaryExpr) -> Tuple[Optional[list], Optional[str]]:
|
|
1617
|
+
"""Generate a ternary expression [if (cond) x else y].
|
|
1618
|
+
|
|
1619
|
+
This is desugared into: a temp variable, set by an if/else.
|
|
1620
|
+
We create the if/else blocks inline and return a VarRef to the temp.
|
|
1621
|
+
"""
|
|
1622
|
+
# Create a temp variable
|
|
1623
|
+
tmp_name = f"_ternary_{gen_id()[:8]}"
|
|
1624
|
+
self._declare_var(tmp_name, 0)
|
|
1625
|
+
vid = self.current_vars[tmp_name]
|
|
1626
|
+
|
|
1627
|
+
# Coerce the condition
|
|
1628
|
+
cond = self._coerce_condition(expr.cond)
|
|
1629
|
+
shadow_cond, sub_cond = self.gen_input_value(cond, "boolean")
|
|
1630
|
+
cond_input = self._pack_input("boolean", shadow_cond, sub_cond)
|
|
1631
|
+
|
|
1632
|
+
# Generate the true branch: set tmp = true_expr
|
|
1633
|
+
true_shadow, true_sub = self.gen_input_value(expr.true_expr, "any")
|
|
1634
|
+
true_block_id = gen_id()
|
|
1635
|
+
self.current_blocks[true_block_id] = {
|
|
1636
|
+
"opcode": "data_setvariableto",
|
|
1637
|
+
"next": None, "parent": None,
|
|
1638
|
+
"inputs": {"VALUE": self._pack_input("any", true_shadow, true_sub)},
|
|
1639
|
+
"fields": {"VARIABLE": [tmp_name, vid]},
|
|
1640
|
+
"shadow": False, "topLevel": False,
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
# Generate the false branch: set tmp = false_expr
|
|
1644
|
+
false_shadow, false_sub = self.gen_input_value(expr.false_expr, "any")
|
|
1645
|
+
false_block_id = gen_id()
|
|
1646
|
+
self.current_blocks[false_block_id] = {
|
|
1647
|
+
"opcode": "data_setvariableto",
|
|
1648
|
+
"next": None, "parent": None,
|
|
1649
|
+
"inputs": {"VALUE": self._pack_input("any", false_shadow, false_sub)},
|
|
1650
|
+
"fields": {"VARIABLE": [tmp_name, vid]},
|
|
1651
|
+
"shadow": False, "topLevel": False,
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
# Generate the if/else block
|
|
1655
|
+
if_block_id = gen_id()
|
|
1656
|
+
self.current_blocks[if_block_id] = {
|
|
1657
|
+
"opcode": "control_if_else",
|
|
1658
|
+
"next": None, "parent": None,
|
|
1659
|
+
"inputs": {
|
|
1660
|
+
"CONDITION": cond_input,
|
|
1661
|
+
"SUBSTACK": [2, true_block_id],
|
|
1662
|
+
"SUBSTACK2": [2, false_block_id],
|
|
1663
|
+
},
|
|
1664
|
+
"fields": {},
|
|
1665
|
+
"shadow": False, "topLevel": False,
|
|
1666
|
+
}
|
|
1667
|
+
self.current_blocks[true_block_id]["parent"] = if_block_id
|
|
1668
|
+
self.current_blocks[false_block_id]["parent"] = if_block_id
|
|
1669
|
+
|
|
1670
|
+
# Return a variable reference to the temp
|
|
1671
|
+
return [SHADOW_VAR, tmp_name, vid], None
|
|
1672
|
+
|
|
1547
1673
|
def _gen_list_reporter(self, expr: A.ListReporterRef) -> Tuple[Optional[list], Optional[str]]:
|
|
1548
1674
|
"""Generate a list reporter block (data_itemoflist, data_lengthoflist,
|
|
1549
1675
|
data_listcontainsitem, data_itemnumoflist, data_listcontents).
|
{typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/decompiler.py
RENAMED
|
@@ -478,6 +478,22 @@ class Decompiler:
|
|
|
478
478
|
self._decompile_generic_block(blocks, block, ts_key, spec, indent)
|
|
479
479
|
return
|
|
480
480
|
|
|
481
|
+
# Looks: go to front/back
|
|
482
|
+
if op == "looks_gotofrontback":
|
|
483
|
+
fb = block.get("fields", {}).get("FRONT_BACK", ["front"])[0]
|
|
484
|
+
self.output.append(f"{indent}goTo{fb.capitalize()}")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
# Looks: go forward/backward layers
|
|
488
|
+
if op == "looks_goforwardbackwardlayers":
|
|
489
|
+
fb = block.get("fields", {}).get("FRONTBACK", ["front"])[0]
|
|
490
|
+
num = self._decompile_input(blocks, block, "NUM")
|
|
491
|
+
if fb == "front":
|
|
492
|
+
self.output.append(f"{indent}goForwardLayers({num})")
|
|
493
|
+
else:
|
|
494
|
+
self.output.append(f"{indent}goBackLayers({num})")
|
|
495
|
+
return
|
|
496
|
+
|
|
481
497
|
# Unknown block - output as comment
|
|
482
498
|
self.output.append(f"{indent}// TODO: unknown block {op}")
|
|
483
499
|
|
|
@@ -725,6 +741,9 @@ class Decompiler:
|
|
|
725
741
|
return "costumeName"
|
|
726
742
|
if op == "looks_costumenumber":
|
|
727
743
|
return "costumeNumber"
|
|
744
|
+
if op == "looks_costumenumbername":
|
|
745
|
+
field = block.get("fields", {}).get("NUMBER_NAME", [""])[0]
|
|
746
|
+
return f"costumeNumberName({field})"
|
|
728
747
|
|
|
729
748
|
# Sound
|
|
730
749
|
if op == "sound_volume":
|
|
@@ -793,6 +812,34 @@ class Decompiler:
|
|
|
793
812
|
args_str = ", ".join(str(a) for a in all_args)
|
|
794
813
|
return f"{ts_key}({args_str})" if all_args else ts_key
|
|
795
814
|
|
|
815
|
+
# Menu shadow blocks (used as field values in dropdowns)
|
|
816
|
+
if op == "motion_goto_menu":
|
|
817
|
+
return block.get("fields", {}).get("TO", [""])[0]
|
|
818
|
+
if op == "motion_pointtowards_menu":
|
|
819
|
+
return block.get("fields", {}).get("TOWARDS", [""])[0]
|
|
820
|
+
if op == "motion_glideto_menu":
|
|
821
|
+
return block.get("fields", {}).get("TO", [""])[0]
|
|
822
|
+
if op == "sensing_of_object_menu":
|
|
823
|
+
return block.get("fields", {}).get("OBJECT", [""])[0]
|
|
824
|
+
if op == "sensing_touchingobjectmenu":
|
|
825
|
+
return block.get("fields", {}).get("TOUCHINGOBJECTMENU", [""])[0]
|
|
826
|
+
if op == "sensing_distancetomenu":
|
|
827
|
+
return block.get("fields", {}).get("DISTANCETOMENU", [""])[0]
|
|
828
|
+
if op == "sensing_keyoptions":
|
|
829
|
+
return block.get("fields", {}).get("KEY_OPTION", [""])[0]
|
|
830
|
+
if op == "control_create_clone_of_menu":
|
|
831
|
+
return block.get("fields", {}).get("CLONE_OPTION", [""])[0]
|
|
832
|
+
if op == "looks_backdrops":
|
|
833
|
+
return block.get("fields", {}).get("BACKDROP", [""])[0]
|
|
834
|
+
if op == "looks_costume":
|
|
835
|
+
return block.get("fields", {}).get("COSTUME", [""])[0]
|
|
836
|
+
if op == "sound_sounds_menu":
|
|
837
|
+
return block.get("fields", {}).get("SOUND_MENU", [""])[0]
|
|
838
|
+
if op == "sound_beats_menu":
|
|
839
|
+
return block.get("fields", {}).get("BEAT", [""])[0]
|
|
840
|
+
if op == "sound_effects_menu":
|
|
841
|
+
return block.get("fields", {}).get("EFFECT", [""])[0]
|
|
842
|
+
|
|
796
843
|
# Unknown reporter
|
|
797
844
|
return f"/* {op} */"
|
|
798
845
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""TypeScratch Package Manager - install libraries from GitHub.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
typescratch install <github-user/repo>
|
|
5
|
+
typescratch install <github-user/repo>@<tag>
|
|
6
|
+
typescratch uninstall <package-name>
|
|
7
|
+
typescratch list
|
|
8
|
+
|
|
9
|
+
Packages are cloned into ./typescratch_packages/<repo-name>/
|
|
10
|
+
and can be fused with: fuse "typescratch_packages/<repo-name>/main.tysh"
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
PACKAGES_DIR = "typescratch_packages"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def install(repo: str) -> int:
|
|
24
|
+
"""Install a package from GitHub.
|
|
25
|
+
|
|
26
|
+
repo can be:
|
|
27
|
+
user/repo
|
|
28
|
+
user/repo@tag
|
|
29
|
+
https://github.com/user/repo
|
|
30
|
+
"""
|
|
31
|
+
# Parse the repo spec
|
|
32
|
+
tag = None
|
|
33
|
+
if "@" in repo:
|
|
34
|
+
repo, tag = repo.rsplit("@", 1)
|
|
35
|
+
|
|
36
|
+
# Clean up URL form
|
|
37
|
+
if repo.startswith("https://github.com/"):
|
|
38
|
+
repo = repo[len("https://github.com/"):]
|
|
39
|
+
|
|
40
|
+
# Build the clone URL
|
|
41
|
+
clone_url = f"https://github.com/{repo}.git"
|
|
42
|
+
|
|
43
|
+
# Extract repo name
|
|
44
|
+
repo_name = repo.split("/")[-1] if "/" in repo else repo
|
|
45
|
+
|
|
46
|
+
# Create packages dir
|
|
47
|
+
if not os.path.exists(PACKAGES_DIR):
|
|
48
|
+
os.makedirs(PACKAGES_DIR)
|
|
49
|
+
|
|
50
|
+
dest = os.path.join(PACKAGES_DIR, repo_name)
|
|
51
|
+
|
|
52
|
+
# If already exists, remove it
|
|
53
|
+
if os.path.exists(dest):
|
|
54
|
+
print(f"Package {repo_name} already installed, updating...")
|
|
55
|
+
shutil.rmtree(dest)
|
|
56
|
+
|
|
57
|
+
# Clone
|
|
58
|
+
print(f"Installing {repo_name} from {clone_url}...")
|
|
59
|
+
cmd = ["git", "clone", "--depth", "1"]
|
|
60
|
+
if tag:
|
|
61
|
+
cmd.extend(["--branch", tag])
|
|
62
|
+
cmd.extend([clone_url, dest])
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
66
|
+
if result.returncode != 0:
|
|
67
|
+
print(f"Error: git clone failed: {result.stderr}", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
print("Error: git is not installed. Install git first.", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
except subprocess.TimeoutExpired:
|
|
73
|
+
print("Error: git clone timed out.", file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
# Remove .git directory to save space
|
|
77
|
+
git_dir = os.path.join(dest, ".git")
|
|
78
|
+
if os.path.exists(git_dir):
|
|
79
|
+
shutil.rmtree(git_dir)
|
|
80
|
+
|
|
81
|
+
print(f"Installed {repo_name} to {dest}/")
|
|
82
|
+
|
|
83
|
+
# Check for a main.tysh file
|
|
84
|
+
main_file = os.path.join(dest, "main.tysh")
|
|
85
|
+
if os.path.isfile(main_file):
|
|
86
|
+
print(f"\nTo use this package, add this to your .tysh file:")
|
|
87
|
+
print(f' fuse "{PACKAGES_DIR}/{repo_name}/main.tysh"')
|
|
88
|
+
else:
|
|
89
|
+
# List .tysh files
|
|
90
|
+
tysh_files = [f for f in os.listdir(dest) if f.endswith(".tysh")]
|
|
91
|
+
if tysh_files:
|
|
92
|
+
print(f"\nAvailable .tysh files:")
|
|
93
|
+
for f in tysh_files:
|
|
94
|
+
print(f' fuse "{PACKAGES_DIR}/{repo_name}/{f}"')
|
|
95
|
+
else:
|
|
96
|
+
print(f"\nNo .tysh files found in package.")
|
|
97
|
+
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def uninstall(name: str) -> int:
|
|
102
|
+
"""Remove an installed package."""
|
|
103
|
+
dest = os.path.join(PACKAGES_DIR, name)
|
|
104
|
+
if not os.path.exists(dest):
|
|
105
|
+
print(f"Package {name} is not installed.", file=sys.stderr)
|
|
106
|
+
return 1
|
|
107
|
+
shutil.rmtree(dest)
|
|
108
|
+
print(f"Uninstalled {name}")
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def list_packages() -> int:
|
|
113
|
+
"""List installed packages."""
|
|
114
|
+
if not os.path.exists(PACKAGES_DIR):
|
|
115
|
+
print("No packages installed.")
|
|
116
|
+
return 0
|
|
117
|
+
packages = [d for d in os.listdir(PACKAGES_DIR)
|
|
118
|
+
if os.path.isdir(os.path.join(PACKAGES_DIR, d))]
|
|
119
|
+
if not packages:
|
|
120
|
+
print("No packages installed.")
|
|
121
|
+
return 0
|
|
122
|
+
print(f"Installed packages ({len(packages)}):")
|
|
123
|
+
for p in sorted(packages):
|
|
124
|
+
dest = os.path.join(PACKAGES_DIR, p)
|
|
125
|
+
tysh_files = [f for f in os.listdir(dest) if f.endswith(".tysh")]
|
|
126
|
+
print(f" {p} ({len(tysh_files)} .tysh files)")
|
|
127
|
+
return 0
|
|
@@ -157,6 +157,22 @@ class Parser:
|
|
|
157
157
|
self.skip_eol()
|
|
158
158
|
continue
|
|
159
159
|
|
|
160
|
+
# Sound import: sound name from "path.wav"
|
|
161
|
+
if t.type == "SOUND_KW" and self._is_at_line_start():
|
|
162
|
+
if current_sprite is None:
|
|
163
|
+
self.err("sound import with no sprite context")
|
|
164
|
+
self._parse_sound_import(current_sprite)
|
|
165
|
+
self.skip_eol()
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Costume import: costume name from "path.png"
|
|
169
|
+
if t.type == "COSTUME_KW" and self._is_at_line_start():
|
|
170
|
+
if current_sprite is None:
|
|
171
|
+
self.err("costume import with no sprite context")
|
|
172
|
+
self._parse_costume_import(current_sprite)
|
|
173
|
+
self.skip_eol()
|
|
174
|
+
continue
|
|
175
|
+
|
|
160
176
|
self.err(f"unexpected top-level token {t.type} ({t.value!r})")
|
|
161
177
|
|
|
162
178
|
# Make sure there's always a stage
|
|
@@ -252,7 +268,6 @@ class Parser:
|
|
|
252
268
|
break
|
|
253
269
|
# Stop at top-level `s "..."` or `b "..."` (next sprite/backdrop)
|
|
254
270
|
if t.type == "NAME" and t.value in ("s", "b") and self._is_at_line_start():
|
|
255
|
-
# Peek ahead: is the next token a name/string? Then it's a declarator.
|
|
256
271
|
if self.peek(1).type in ("STRING", "NAME", "TEXT"):
|
|
257
272
|
break
|
|
258
273
|
if t.type == "WHEN":
|
|
@@ -264,6 +279,14 @@ class Parser:
|
|
|
264
279
|
if t.type == "NAME" and t.value == "list" and self._is_at_line_start():
|
|
265
280
|
self._parse_list_decl(sprite)
|
|
266
281
|
continue
|
|
282
|
+
# Sound import: sound name from "path.wav"
|
|
283
|
+
if t.type == "SOUND_KW" and self._is_at_line_start():
|
|
284
|
+
self._parse_sound_import(sprite)
|
|
285
|
+
continue
|
|
286
|
+
# Costume import: costume name from "path.png"
|
|
287
|
+
if t.type == "COSTUME_KW" and self._is_at_line_start():
|
|
288
|
+
self._parse_costume_import(sprite)
|
|
289
|
+
continue
|
|
267
290
|
# otherwise: stop - we're at the next target / eof
|
|
268
291
|
break
|
|
269
292
|
|
|
@@ -459,6 +482,42 @@ class Parser:
|
|
|
459
482
|
self.eat("RBRACKET", "']'")
|
|
460
483
|
return A.Hat(kind="greater_than", args=[what, val], line=line)
|
|
461
484
|
|
|
485
|
+
def _parse_ternary_value(self):
|
|
486
|
+
"""Parse a single value for a ternary expression.
|
|
487
|
+
|
|
488
|
+
This is like _parse_atom but stops at 'else' (doesn't consume it
|
|
489
|
+
as part of a bare string).
|
|
490
|
+
"""
|
|
491
|
+
t = self.peek()
|
|
492
|
+
if t.type == "ELSE":
|
|
493
|
+
self.err("expected a value before 'else' in ternary")
|
|
494
|
+
if t.type == "STRING":
|
|
495
|
+
self.next()
|
|
496
|
+
return A.StringLit(value=t.value, line=t.line)
|
|
497
|
+
if t.type == "NUM":
|
|
498
|
+
self.next()
|
|
499
|
+
val = float(t.value) if "." in t.value else int(t.value)
|
|
500
|
+
return A.NumberLit(value=val, line=t.line)
|
|
501
|
+
if t.type == "HEXCOLOR":
|
|
502
|
+
self.next()
|
|
503
|
+
return A.ColorLit(value=t.value, line=t.line)
|
|
504
|
+
if t.type == "LPAREN":
|
|
505
|
+
self.next()
|
|
506
|
+
self.skip_eol()
|
|
507
|
+
expr = self._parse_expr()
|
|
508
|
+
self.skip_eol()
|
|
509
|
+
self.eat("RPAREN", "')'")
|
|
510
|
+
return expr
|
|
511
|
+
if t.type in ("NAME", "TEXT"):
|
|
512
|
+
# Parse a single bare name or function call, but DON'T
|
|
513
|
+
# accumulate following words (since "else" could be next)
|
|
514
|
+
name = self.next().value
|
|
515
|
+
if self.at("LPAREN"):
|
|
516
|
+
args = self._parse_paren_args()
|
|
517
|
+
return A.FuncCall(name=name, args=args, line=t.line)
|
|
518
|
+
return A.BareRef(name=name, line=t.line)
|
|
519
|
+
self.err(f"unexpected token in ternary value: {t.type} ({t.value!r})")
|
|
520
|
+
|
|
462
521
|
def _read_bracket_text(self) -> str:
|
|
463
522
|
"""Read text inside [ ] - can be multi-word. Returns the joined string.
|
|
464
523
|
|
|
@@ -542,10 +601,6 @@ class Parser:
|
|
|
542
601
|
return A.StopStatement(kind=kind, line=line)
|
|
543
602
|
if t.type == "BROADCAST":
|
|
544
603
|
self.next()
|
|
545
|
-
# Three forms:
|
|
546
|
-
# broadcast [msg] (Scratch-style menu)
|
|
547
|
-
# broadcast(msg) (function-style)
|
|
548
|
-
# broadcast msg (bare)
|
|
549
604
|
msg: Any
|
|
550
605
|
if self.at("LBRACKET"):
|
|
551
606
|
self.next()
|
|
@@ -559,7 +614,6 @@ class Parser:
|
|
|
559
614
|
else:
|
|
560
615
|
msg = self._parse_expr()
|
|
561
616
|
wait = False
|
|
562
|
-
# check for "and wait"
|
|
563
617
|
if self.at("AND") or (self.at("NAME", "TEXT") and self.peek().value == "and"):
|
|
564
618
|
self.next()
|
|
565
619
|
if not (self.at("WAIT") or (self.at("NAME", "TEXT") and self.peek().value == "wait")):
|
|
@@ -567,6 +621,15 @@ class Parser:
|
|
|
567
621
|
self.next()
|
|
568
622
|
wait = True
|
|
569
623
|
return A.BroadcastStatement(msg=msg, wait=wait, line=line)
|
|
624
|
+
|
|
625
|
+
# private x = value (local variable inside a custom block)
|
|
626
|
+
if t.type == "PRIVATE_KW":
|
|
627
|
+
self.next()
|
|
628
|
+
var_name = self._read_name_or_string()
|
|
629
|
+
self.eat("ASSIGN", "'='")
|
|
630
|
+
val = self._parse_expr()
|
|
631
|
+
self.current_vars.add(var_name)
|
|
632
|
+
return A.LocalVarDecl(name=var_name, value=val, line=line)
|
|
570
633
|
if t.type == "DEF":
|
|
571
634
|
# def at statement level (inside a script - unusual but allowed)
|
|
572
635
|
# We need a sprite context; create a dummy def container
|
|
@@ -920,12 +983,28 @@ class Parser:
|
|
|
920
983
|
# [expr] - used for menu choices, but also a grouping
|
|
921
984
|
self.next()
|
|
922
985
|
self.skip_eol()
|
|
986
|
+
# Check for ternary: [if (cond) x else y]
|
|
987
|
+
if self.at("IF"):
|
|
988
|
+
self.next()
|
|
989
|
+
cond = self._parse_expr()
|
|
990
|
+
# Parse true_expr - but stop at ELSE
|
|
991
|
+
# We need to parse a single atom for the true value
|
|
992
|
+
# because _parse_expr would consume "else" as a bare word
|
|
993
|
+
true_expr = self._parse_ternary_value()
|
|
994
|
+
# expect "else"
|
|
995
|
+
if not self.at("ELSE"):
|
|
996
|
+
self.err("expected 'else' in ternary [if (cond) x else y]")
|
|
997
|
+
self.next()
|
|
998
|
+
false_expr = self._parse_ternary_value()
|
|
999
|
+
self.skip_eol()
|
|
1000
|
+
self.eat("RBRACKET", "']'")
|
|
1001
|
+
return A.TernaryExpr(cond=cond, true_expr=true_expr, false_expr=false_expr, line=t.line)
|
|
923
1002
|
expr = self._parse_expr()
|
|
924
1003
|
self.skip_eol()
|
|
925
1004
|
self.eat("RBRACKET", "']'")
|
|
926
1005
|
return expr
|
|
927
1006
|
|
|
928
|
-
if t.type in ("NAME", "TEXT"):
|
|
1007
|
+
if t.type in ("NAME", "TEXT", "SOUND_KW", "COSTUME_KW", "PRIVATE_KW"):
|
|
929
1008
|
# Accumulate consecutive bare words into a string.
|
|
930
1009
|
# If the next token is LPAREN, this is a function call.
|
|
931
1010
|
# If the next token is DOT, this is namespace access.
|
|
@@ -979,7 +1058,8 @@ class Parser:
|
|
|
979
1058
|
"WAIT", "WAIT_UNTIL", "STOP", "BROADCAST", "IF", "ELSE", "ELIF",
|
|
980
1059
|
"REPEAT", "REPEAT_UNTIL", "FOREVER", "WHILE", "WHEN",
|
|
981
1060
|
"FOR_EACH", "ALL_AT_ONCE",
|
|
982
|
-
"DEF", "TRUE", "FALSE", "EXTENSION"
|
|
1061
|
+
"DEF", "TRUE", "FALSE", "EXTENSION",
|
|
1062
|
+
"SOUND_KW", "COSTUME_KW", "PRIVATE_KW")
|
|
983
1063
|
words = [name]
|
|
984
1064
|
while self.at(*WORD_LIKE):
|
|
985
1065
|
words.append(str(self.next().value))
|
|
@@ -1045,6 +1125,47 @@ class Parser:
|
|
|
1045
1125
|
))
|
|
1046
1126
|
|
|
1047
1127
|
# ---- list declarations ----
|
|
1128
|
+
def _parse_sound_import(self, sprite: A.Target) -> None:
|
|
1129
|
+
"""Parse: sound name from "path.wav" """
|
|
1130
|
+
self.next() # consume 'sound'
|
|
1131
|
+
name = self._read_name_or_string()
|
|
1132
|
+
# expect "from"
|
|
1133
|
+
if not (self.at("NAME", "TEXT") and self.peek().value == "from"):
|
|
1134
|
+
self.err("expected 'from' in: sound name from \"path.wav\"")
|
|
1135
|
+
self.next()
|
|
1136
|
+
path_tok = self.peek()
|
|
1137
|
+
if path_tok.type == "STRING":
|
|
1138
|
+
path = self.next().value
|
|
1139
|
+
elif path_tok.type == "PATH":
|
|
1140
|
+
path = self.next().value
|
|
1141
|
+
elif path_tok.type in ("NAME", "TEXT"):
|
|
1142
|
+
path = self.next().value
|
|
1143
|
+
else:
|
|
1144
|
+
self.err("expected file path after 'from'")
|
|
1145
|
+
if not hasattr(sprite, "_sound_imports"):
|
|
1146
|
+
sprite._sound_imports = []
|
|
1147
|
+
sprite._sound_imports.append({"name": name, "path": path})
|
|
1148
|
+
|
|
1149
|
+
def _parse_costume_import(self, sprite: A.Target) -> None:
|
|
1150
|
+
"""Parse: costume name from "path.png" """
|
|
1151
|
+
self.next() # consume 'costume'
|
|
1152
|
+
name = self._read_name_or_string()
|
|
1153
|
+
if not (self.at("NAME", "TEXT") and self.peek().value == "from"):
|
|
1154
|
+
self.err("expected 'from' in: costume name from \"path.png\"")
|
|
1155
|
+
self.next()
|
|
1156
|
+
path_tok = self.peek()
|
|
1157
|
+
if path_tok.type == "STRING":
|
|
1158
|
+
path = self.next().value
|
|
1159
|
+
elif path_tok.type == "PATH":
|
|
1160
|
+
path = self.next().value
|
|
1161
|
+
elif path_tok.type in ("NAME", "TEXT"):
|
|
1162
|
+
path = self.next().value
|
|
1163
|
+
else:
|
|
1164
|
+
self.err("expected file path after 'from'")
|
|
1165
|
+
if not hasattr(sprite, "_costume_imports"):
|
|
1166
|
+
sprite._costume_imports = []
|
|
1167
|
+
sprite._costume_imports.append({"name": name, "path": path})
|
|
1168
|
+
|
|
1048
1169
|
def _parse_list_decl(self, sprite: A.Target) -> None:
|
|
1049
1170
|
# The "list" keyword has already been peeked but not consumed.
|
|
1050
1171
|
list_tok = self.peek()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{typescratch-1.6.0 → typescratch-1.6.2}/typescratch_pypi_release/typescratch_src/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|