typescratch 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,146 @@
1
+ """TypeScratch - a small programming language that compiles to Scratch 3 (.sb3) files.
2
+
3
+ Library usage:
4
+
5
+ import typescratch
6
+
7
+ # Compile a .tysh file to a .sb3 next to it
8
+ typescratch.file("C:/Users/pc/Desktop/thing.tysh")
9
+
10
+ # Or specify the output path explicitly
11
+ typescratch.file("thing.tysh", out="thing.sb3")
12
+
13
+ # Compile from a source string
14
+ src = '''
15
+ s "Sprite1"
16
+ when gf clicked {
17
+ say(Hello, World!)(2)
18
+ }
19
+ '''
20
+ sb3_bytes = typescratch.source(src) # returns bytes
21
+ typescratch.source(src, out="out.sb3") # writes to file
22
+
23
+ # Lower-level: compile and return (project_json, assets_dict)
24
+ project, assets = typescratch.compile_source(src)
25
+
26
+ CLI usage:
27
+
28
+ typescratch build thing.tysh # writes thing.sb3
29
+ typescratch build thing.tysh --out out.sb3
30
+ typescratch build thing.tysh --debug # verbose AST/block dump
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import os
36
+ import sys
37
+ from typing import Optional, Tuple, Dict, Any
38
+
39
+ from .errors import CompileError, Debug
40
+ from .lexer import tokenize
41
+ from .parser import Parser
42
+ from .codegen import Codegen
43
+ from .sb3 import write_sb3, write_sb3_bytes
44
+
45
+
46
+ __version__ = "1.0.0"
47
+
48
+
49
+ def compile_source(src: str, filename: str = "<tysh>", debug: bool = False,
50
+ asset_root: Optional[str] = None
51
+ ) -> Tuple[dict, Dict[str, bytes]]:
52
+ """Compile a TypeScratch source string to (project_json, assets_dict).
53
+
54
+ `asset_root` is the base directory for resolving relative `img=` paths.
55
+ If None, defaults to the directory of `filename` (or cwd).
56
+ """
57
+ dbg = Debug(enabled=debug)
58
+ dbg.section("Tokens")
59
+ toks = tokenize(src, filename)
60
+ for t in toks:
61
+ dbg.line(f"{t.line:>4}:{t.col:<3} {t.type:<14} {t.value!r}")
62
+
63
+ dbg.section("AST")
64
+ program = Parser(toks, filename).parse_program()
65
+ dbg.line(repr(program))
66
+ for tgt in program.targets:
67
+ dbg.line(f" target {tgt.kind} {tgt.name!r} scripts={len(tgt.scripts)} "
68
+ f"custom={len(tgt.custom_blocks)} costumes={len(tgt.costumes)}")
69
+ for s in tgt.scripts:
70
+ dbg.line(f" hat={s.hat.kind} body={len(s.body)} stmts")
71
+
72
+ if asset_root is None:
73
+ if filename and filename != "<tysh>":
74
+ asset_root = os.path.dirname(os.path.abspath(filename))
75
+ else:
76
+ asset_root = os.getcwd()
77
+
78
+ cg = Codegen(program, debug=dbg, asset_root=asset_root)
79
+ project, assets = cg.generate()
80
+ return project, assets
81
+
82
+
83
+ def source(src: str, out: Optional[str] = None, debug: bool = False,
84
+ filename: str = "<tysh>") -> bytes:
85
+ """Compile a TypeScratch source string and return the .sb3 bytes.
86
+
87
+ If `out` is given, also write to that file path.
88
+ """
89
+ project, assets = compile_source(src, filename=filename, debug=debug)
90
+ sb3_bytes = write_sb3_bytes(project, assets)
91
+ if out:
92
+ with open(out, "wb") as f:
93
+ f.write(sb3_bytes)
94
+ return sb3_bytes
95
+
96
+
97
+ def file(path: str, out: Optional[str] = None, debug: bool = False) -> str:
98
+ """Compile a .tysh file to a .sb3 file.
99
+
100
+ Returns the output file path.
101
+ """
102
+ if not os.path.isfile(path):
103
+ raise FileNotFoundError(f"TypeScratch source file not found: {path}")
104
+ with open(path, "r", encoding="utf-8") as f:
105
+ src = f.read()
106
+ if out is None:
107
+ # default: same name, .sb3 extension, next to input
108
+ base, _ = os.path.splitext(path)
109
+ out = base + ".sb3"
110
+ source(src, out=out, debug=debug, filename=path)
111
+ return out
112
+
113
+
114
+ # Friendly alias - `typescratch.compile("out.sb3")` after `typescratch.file("in.tysh")`
115
+ # Actually the user wanted `typescratch.file(...)` then `typescratch.compile(...)`.
116
+ # Let me support both styles:
117
+
118
+ def compile(out_path: str, src_path: Optional[str] = None, debug: bool = False) -> str:
119
+ """Compile a .tysh source to a specific .sb3 output path.
120
+
121
+ If `src_path` is None, looks for a .tysh file with the same basename
122
+ as `out_path` (e.g. for `out.sb3`, looks for `out.tysh` in the same dir).
123
+ """
124
+ if src_path is None:
125
+ base, _ = os.path.splitext(out_path)
126
+ for ext in (".tysh", ".ts", ".tsh"):
127
+ candidate = base + ext
128
+ if os.path.isfile(candidate):
129
+ src_path = candidate
130
+ break
131
+ if src_path is None:
132
+ raise FileNotFoundError(
133
+ f"no source file found for output {out_path!r} "
134
+ f"(looked for {base}.tysh)"
135
+ )
136
+ return file(src_path, out=out_path, debug=debug)
137
+
138
+
139
+ __all__ = [
140
+ "file",
141
+ "source",
142
+ "compile",
143
+ "compile_source",
144
+ "CompileError",
145
+ "__version__",
146
+ ]
@@ -0,0 +1,10 @@
1
+ """Allow running TypeScratch as `python -m typescratch build ...`.
2
+
3
+ This file is executed when you run `python -m typescratch` — it delegates
4
+ to the CLI's main function.
5
+ """
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ raise SystemExit(main())
typescratch/assets.py ADDED
@@ -0,0 +1,69 @@
1
+ """Default Scratch assets used when a sprite/backdrop has no image.
2
+
3
+ We embed the OFFICIAL Scratch Cat costume SVG (costume1 from the
4
+ default Scratch 3.0 project) - extracted from an actual .sb3 file -
5
+ so that any project we generate looks correct in the Scratch editor.
6
+
7
+ A blank white backdrop SVG is also provided.
8
+ """
9
+
10
+ import os
11
+
12
+ # Path to the official Scratch Cat SVG (sitting alongside this module)
13
+ _CAT_SVG_PATH = os.path.join(os.path.dirname(__file__), "scratchcat_official.svg")
14
+
15
+
16
+ def _load_official_cat() -> bytes:
17
+ """Load the official Scratch Cat SVG from disk."""
18
+ try:
19
+ with open(_CAT_SVG_PATH, "rb") as f:
20
+ return f.read()
21
+ except FileNotFoundError:
22
+ # Fallback to a placeholder if the SVG file is missing (e.g. if
23
+ # the package was installed without the data file)
24
+ return PLACEHOLDER_CAT_SVG.encode("utf-8")
25
+
26
+
27
+ # Official Scratch Cat - the real deal, loaded from scratchcat_official.svg
28
+ # Rotation center: (48, 50) - matches Scratch's default.
29
+ SCRATCH_CAT_SVG = _load_official_cat().decode("utf-8", errors="replace")
30
+
31
+
32
+ # A simple blank white backdrop, the size of a default Scratch stage.
33
+ BLANK_BACKDROP_SVG = """<?xml version="1.0" encoding="UTF-8"?>
34
+ <svg xmlns="http://www.w3.org/2000/svg" width="480" height="360" viewBox="0 0 480 360">
35
+ <rect width="480" height="360" fill="#ffffff"/>
36
+ </svg>
37
+ """
38
+
39
+ # A neutral grey costume used as a last-resort fallback if the official
40
+ # SVG file is missing from the install.
41
+ PLACEHOLDER_CAT_SVG = """<?xml version="1.0" encoding="UTF-8"?>
42
+ <svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
43
+ <rect width="96" height="96" fill="#cccccc"/>
44
+ <text x="48" y="50" text-anchor="middle" font-family="sans-serif" font-size="14">cat</text>
45
+ </svg>
46
+ """
47
+
48
+ # A short silent WAV used when a sound is referenced but no file exists.
49
+ SILENT_WAV = (
50
+ b"RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00"
51
+ b"\x44\xac\x00\x00\x88\x58\x01\x00\x02\x00\x10\x00data\x00\x00\x00\x00"
52
+ )
53
+
54
+
55
+ def default_sprite_costume():
56
+ """Return (name, svg_bytes, md5ext, rotation_center_x, rotation_center_y)
57
+ for the official Scratch Cat."""
58
+ data = SCRATCH_CAT_SVG.encode("utf-8")
59
+ return ("costume1", data, "scratchcat.svg", 48, 50)
60
+
61
+
62
+ def default_backdrop():
63
+ """Return (name, svg_bytes, md5ext, rotation_center_x, rotation_center_y)
64
+ for a blank white backdrop."""
65
+ return ("backdrop1", BLANK_BACKDROP_SVG.encode("utf-8"), "backdrop1.svg", 240, 180)
66
+
67
+
68
+ def placeholder_costume(name="costume1"):
69
+ return (name, PLACEHOLDER_CAT_SVG.encode("utf-8"), "placeholder.svg", 30, 30)
@@ -0,0 +1,363 @@
1
+ """AST node definitions for TypeScratch.
2
+
3
+ The grammar (informally):
4
+
5
+ program := (extension | target)*
6
+
7
+ extension := 'extension' EXT_NAME
8
+ target := sprite | backdrop
9
+ sprite := 's' STRING [sprite_attrs]
10
+ backdrop := 'b' STRING [backdrop_attrs]
11
+ sprite_attrs := ('xy=' NUM ',' NUM | 'dir=' NUM | 'size=' NUM
12
+ | 'visible=' BOOL | 'rot=' ROT | 'img=' PATH | 'costume=' NAME)*
13
+ backdrop_attrs:= ('img=' PATH | 'costume=' NAME)*
14
+
15
+ script := hat '{' statement* '}'
16
+ hat := 'when' hat_kind hat_args?
17
+
18
+ statement := block_call
19
+ | var_assign
20
+ | var_modify
21
+ | list_op
22
+ | 'if' expr block ('else' block)?
23
+ | 'repeat' expr block
24
+ | 'repeatUntil' expr block
25
+ | 'forever' block
26
+ | 'while' expr block
27
+ | 'wait' expr
28
+ | 'stop' STOP_ARG
29
+ | 'def' NAME '(' params ')' block # custom block definition
30
+ | custom_call
31
+
32
+ block_call := NAME ('.' NAME)? '(' args? ')' post? # e.g. say(...)(2), pen.Down, goto(1,2)
33
+ | NAME '.' NAME # argument-less form: pen.Down, nextCostume
34
+ | NAME # argument-less: show, hide
35
+
36
+ var_assign := NAME '=' expr
37
+ var_modify := NAME ('+=' | '-=' | '*=' | '/=') expr
38
+
39
+ list_op := NAME '.' ('add' | 'delete' | 'insert' | 'replace') '(' args ')'
40
+
41
+ expr := or_expr
42
+ or_expr := and_expr (('or') and_expr)*
43
+ and_expr := not_expr ('and' not_expr)*
44
+ not_expr := 'not' not_expr | compare
45
+ compare := add (('>' | '<' | '=') add)?
46
+ add := mul (('+' | '-') mul)*
47
+ mul := unary (('*' | '/' | 'mod') unary)*
48
+ unary := '-' unary | atom
49
+ atom := NUM | STRING | NAME | '(' expr ')' | func_call | builtin
50
+ func_call := NAME '(' args? ')'
51
+
52
+ Strings in the .tysh source can be written *without* quotes (because
53
+ "Scratch has no strings, only text"). Where a string vs an identifier
54
+ is ambiguous (e.g. arguments to `say`), the lexer yields a single TEXT
55
+ token whose interpretation is context-sensitive in the parser.
56
+ """
57
+
58
+ from __future__ import annotations
59
+
60
+ from dataclasses import dataclass, field
61
+ from typing import List, Optional, Any, Dict
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Top-level
66
+ # ---------------------------------------------------------------------------
67
+
68
+ @dataclass
69
+ class Program:
70
+ extensions: List[str] = field(default_factory=list)
71
+ targets: List["Target"] = field(default_factory=list)
72
+
73
+ def __repr__(self):
74
+ return f"Program(ext={self.extensions}, targets={len(self.targets)})"
75
+
76
+
77
+ @dataclass
78
+ class Target:
79
+ kind: str # 'sprite' or 'stage'
80
+ name: str
81
+ x: float = 0.0
82
+ y: float = 0.0
83
+ direction: float = 90.0
84
+ size: float = 100.0
85
+ visible: bool = True
86
+ rotation_style: str = "all around" # all around | left-right | don't rotate
87
+ img: Optional[str] = None
88
+ costumes: List[Dict[str, Any]] = field(default_factory=list) # {name, path, is_default}
89
+ sounds: List[Dict[str, Any]] = field(default_factory=list)
90
+ scripts: List["Script"] = field(default_factory=list)
91
+ custom_blocks: List["CustomBlockDef"] = field(default_factory=list)
92
+
93
+
94
+ @dataclass
95
+ class Script:
96
+ hat: "Hat"
97
+ body: List["Statement"] = field(default_factory=list)
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Hats
102
+ # ---------------------------------------------------------------------------
103
+
104
+ @dataclass
105
+ class Hat:
106
+ kind: str # gf | spr | key | backdrop | receive | greater_than | clicked_clone | broadcast
107
+ args: List[Any] = field(default_factory=list)
108
+ line: int = 0
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Statements
113
+ # ---------------------------------------------------------------------------
114
+
115
+ class Statement:
116
+ line: int = 0
117
+
118
+
119
+ @dataclass
120
+ class BlockCall(Statement):
121
+ """A built-in block call: e.g. say(...) , goto(x,y), pen.Down.
122
+
123
+ `namespace` is None for global blocks (say, goto, move, ...).
124
+ `method` is the block name.
125
+ `args` is the list of expressions / atoms inside the parens.
126
+ `post` is the optional trailing paren group, e.g. the seconds in
127
+ `say(Hello, World!)(2)`.
128
+ """
129
+ namespace: Optional[str]
130
+ method: str
131
+ args: List[Any]
132
+ post: Optional[List[Any]] = None
133
+ line: int = 0
134
+
135
+
136
+ @dataclass
137
+ class VarAssign(Statement):
138
+ name: str
139
+ value: Any
140
+ line: int = 0
141
+
142
+
143
+ @dataclass
144
+ class VarModify(Statement):
145
+ name: str
146
+ op: str # '+=' | '-=' | '*=' | '/='
147
+ value: Any
148
+ line: int = 0
149
+
150
+
151
+ @dataclass
152
+ class ListOp(Statement):
153
+ list_name: str
154
+ op: str # 'add' | 'delete' | 'insert' | 'replace' | 'show' | 'hide'
155
+ args: List[Any]
156
+ line: int = 0
157
+
158
+
159
+ @dataclass
160
+ class IfStatement(Statement):
161
+ cond: Any
162
+ then_body: List[Statement]
163
+ else_body: Optional[List[Statement]] = None
164
+ line: int = 0
165
+
166
+
167
+ @dataclass
168
+ class RepeatStatement(Statement):
169
+ times: Any
170
+ body: List[Statement]
171
+ line: int = 0
172
+
173
+
174
+ @dataclass
175
+ class RepeatUntilStatement(Statement):
176
+ cond: Any
177
+ body: List[Statement]
178
+ line: int = 0
179
+
180
+
181
+ @dataclass
182
+ class WaitUntilStatement(Statement):
183
+ """`waitUntil (cond)` - control_wait_until."""
184
+ cond: Any
185
+ line: int = 0
186
+
187
+
188
+ @dataclass
189
+ class ForeverStatement(Statement):
190
+ body: List[Statement]
191
+ line: int = 0
192
+
193
+
194
+ @dataclass
195
+ class WhileStatement(Statement):
196
+ """Syntactic sugar: `while (c) { ... }` -> `repeatUntil(not c) { ... }`."""
197
+ cond: Any
198
+ body: List[Statement]
199
+ line: int = 0
200
+
201
+
202
+ @dataclass
203
+ class ForEachStatement(Statement):
204
+ """`forEach (var) in (number) { ... }` - hidden control_for_each block.
205
+
206
+ Iterates `var` from 1 to `count`, running the body once per value.
207
+ `var_name` is the variable to use as the loop counter.
208
+ """
209
+ var_name: str
210
+ count: Any
211
+ body: List[Statement]
212
+ line: int = 0
213
+
214
+
215
+ @dataclass
216
+ class AllAtOnceStatement(Statement):
217
+ """`allAtOnce { ... }` - hidden control_all_at_once block.
218
+
219
+ Runs the body without yielding to the screen refresh between blocks
220
+ (similar to a warp-mode custom block).
221
+ """
222
+ body: List[Statement]
223
+ line: int = 0
224
+
225
+
226
+ @dataclass
227
+ class WaitStatement(Statement):
228
+ secs: Any
229
+ line: int = 0
230
+
231
+
232
+ @dataclass
233
+ class StopStatement(Statement):
234
+ kind: str # 'all' | 'this script' | 'other scripts in sprite' | 'other scripts in stage'
235
+ line: int = 0
236
+
237
+
238
+ @dataclass
239
+ class BroadcastStatement(Statement):
240
+ msg: Any
241
+ wait: bool = False
242
+ line: int = 0
243
+
244
+
245
+ @dataclass
246
+ class CustomBlockDef(Statement):
247
+ """`def MyBlock(name, age) { ... }` - defines a custom block."""
248
+ name: str
249
+ params: List[Dict[str, str]] # [{name, type}] type in {string|bool|number}
250
+ body: List[Statement]
251
+ warp: bool = False # run without screen refresh
252
+ line: int = 0
253
+
254
+
255
+ @dataclass
256
+ class CustomBlockCall(Statement):
257
+ """A call to a user-defined custom block."""
258
+ name: str
259
+ args: List[Any]
260
+ line: int = 0
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Expressions
265
+ # ---------------------------------------------------------------------------
266
+
267
+ @dataclass
268
+ class NumberLit:
269
+ value: float
270
+ line: int = 0
271
+
272
+
273
+ @dataclass
274
+ class StringLit:
275
+ value: str
276
+ line: int = 0
277
+
278
+
279
+ @dataclass
280
+ class ColorLit:
281
+ value: str # like "#ff0000"
282
+ line: int = 0
283
+
284
+
285
+ @dataclass
286
+ class VarRef:
287
+ name: str
288
+ line: int = 0
289
+
290
+
291
+ @dataclass
292
+ class ListRef:
293
+ name: str
294
+ index: Any
295
+ line: int = 0
296
+
297
+
298
+ @dataclass
299
+ class BinOp:
300
+ op: str # '+','-','*','/','mod','>','<','=','and','or'
301
+ left: Any
302
+ right: Any
303
+ line: int = 0
304
+
305
+
306
+ @dataclass
307
+ class UnaryOp:
308
+ op: str # 'not' or '-'
309
+ operand: Any
310
+ line: int = 0
311
+
312
+
313
+ @dataclass
314
+ class FuncCall:
315
+ """Operator-like reporter: pickRandom(1,10), join(a,b), abs(x), ..."""
316
+ name: str
317
+ args: List[Any]
318
+ line: int = 0
319
+
320
+
321
+ @dataclass
322
+ class BuiltinRef:
323
+ """Direct references to Scratch reporters that have no arguments:
324
+ answer, mouseX, mouseY, timer, loudness, daysSince2000, username,
325
+ x, y, direction, size, costumeName, backdropName, volume, etc.
326
+ """
327
+ name: str
328
+ line: int = 0
329
+
330
+
331
+ @dataclass
332
+ class BareRef:
333
+ """A bare identifier used in expression context.
334
+
335
+ Resolved at codegen time:
336
+ * if the name matches a declared variable in the current target -> VarRef
337
+ * if the name matches a builtin reporter (e.g. "answer", "mouseX") -> BuiltinRef
338
+ * if the name matches a current custom-block parameter -> ArgumentRef
339
+ * otherwise -> StringLit (literal text)
340
+ """
341
+ name: str
342
+ line: int = 0
343
+
344
+
345
+ @dataclass
346
+ class ArgumentRef:
347
+ """Reference to a custom block parameter (an argument_reporter block)."""
348
+ name: str
349
+ arg_id: str = "" # filled in by codegen
350
+ line: int = 0
351
+
352
+
353
+ @dataclass
354
+ class ListReporterRef:
355
+ """A list reporter reference like `inventory.item(1)`, `inventory.length`,
356
+ `inventory.contains(thing)`, `inventory.itemNum(thing)`, `inventory.contents`.
357
+
358
+ Resolved at codegen time into the appropriate data_* block.
359
+ """
360
+ list_name: str
361
+ method: str # 'item' | 'length' | 'contains' | 'itemNum' | 'contents'
362
+ args: List[Any]
363
+ line: int = 0