Skip to content

Commit 86b45bd

Browse files
authored
Scratch text representation (#435)
* replace parent imports with first party imports * incomplete text representation of scratch code --------- Signed-off-by: TheCommCraft <[email protected]>
1 parent e932ede commit 86b45bd

File tree

6 files changed

+243
-0
lines changed

6 files changed

+243
-0
lines changed

scratchattach/editor/code_translation/__init__.py

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## For testing. The file extension is temporary and might change in the future. ##
2+
3+
WHEN (green_flag_clicked) {
4+
until {
5+
say()say()
6+
}
7+
sa()
8+
}
9+
custom_block hello_%s_ (<a>, awd) {
10+
until (1) {
11+
say((8 + 2) * (6 - 3))
12+
}
13+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
start: top_level_block*
2+
3+
top_level_block: hat "{" block* "}"
4+
| PREPROC_INSTR
5+
| COMMMENT
6+
7+
PREPROC_INSTR: "%%" _PREPROC_INSTR_CONTENT "%%"
8+
9+
_PREPROC_INSTR_CONTENT: /[\s\S]+?/
10+
11+
COMMMENT: "##" _COMMMENT_CONTENT "##"
12+
13+
_COMMMENT_CONTENT: /[\s\S]+?/
14+
15+
hat: [event_hat | block_hat]
16+
17+
event_hat: "when"i "(" EVENT ")"
18+
block_hat: "custom_block"i BLOCK_NAME "(" [ param ("," param)* ] ")"
19+
20+
param: value_param
21+
| bool_param
22+
value_param: PARAM_NAME
23+
bool_param: "<" PARAM_NAME ">"
24+
25+
EVENT: "green_flag_clicked"i
26+
27+
block: CONTROL_BLOCK_NAME ["(" block_params ")"] "{" block_content "}"
28+
| BLOCK_NAME ["(" block_params ")"] [";" | "\n" | " "]
29+
30+
block_params: expr*
31+
block_content: block*
32+
33+
expr: addition | subtraction | multiplication | division | LITERAL_NUMBER | "(" expr ")"
34+
35+
low_expr1: ("(" expr ")") | LITERAL_NUMBER
36+
low_expr2: low_expr1 | multiplication | division
37+
38+
addition: expr "+" expr
39+
subtraction: expr "-" low_expr2
40+
multiplication: low_expr2 "*" low_expr2
41+
division: low_expr2 "/" low_expr1
42+
43+
CONTROL_BLOCK_NAME: "repeat"i
44+
| "until"i
45+
| "forever"i
46+
47+
PARAM_NAME: ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+
48+
BLOCK_NAME: [MODULE_NAME "."] ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+
49+
50+
MODULE_NAME: "params"i
51+
| "vars"i
52+
| "lists"i
53+
54+
WS: /\s+/
55+
%ignore WS
56+
%import common.SIGNED_NUMBER -> LITERAL_NUMBER
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from __future__ import annotations
2+
from pathlib import Path
3+
from typing import Union, Generic, TypeVar
4+
from abc import ABC, abstractmethod
5+
from collections.abc import Sequence
6+
7+
from lark import Lark, Transformer, Tree, Token, v_args
8+
from lark.reconstruct import Reconstructor
9+
10+
R = TypeVar("R")
11+
class SupportsRead(ABC, Generic[R]):
12+
@abstractmethod
13+
def read(self, size: int | None = -1) -> R:
14+
pass
15+
16+
LANG_PATH = Path(__file__).parent / "language.lark"
17+
18+
lang = Lark(LANG_PATH.read_text(), maybe_placeholders=False)
19+
reconstructor = Reconstructor(lang)
20+
21+
def parse(script: Union[str, bytes, SupportsRead[str], Path]) -> Tree:
22+
if isinstance(script, Path):
23+
script = script.read_text()
24+
if isinstance(script, SupportsRead):
25+
read_data = script.read()
26+
assert isinstance(read_data, str)
27+
script = read_data
28+
if isinstance(script, bytes):
29+
script = script.decode("utf-8")
30+
return lang.parse(script)
31+
32+
def unparse(tree: Tree) -> str:
33+
return reconstructor.reconstruct(tree)
34+
35+
class PrettyUnparser(Transformer):
36+
INDENT_STRING = " "
37+
38+
@classmethod
39+
def _indent(cls, text):
40+
if not text:
41+
return ""
42+
return "\n".join(cls.INDENT_STRING + line for line in text.splitlines())
43+
44+
def PARAM_NAME(self, token):
45+
return token.value
46+
47+
def BLOCK_NAME(self, token):
48+
return token.value
49+
50+
def EVENT(self, token):
51+
return token.value
52+
53+
def CONTROL_BLOCK_NAME(self, token):
54+
return token.value
55+
56+
def _PREPROC_INSTR_CONTENT(self, token):
57+
return token.value
58+
59+
def _COMMMENT_CONTENT(self, token):
60+
return token.value
61+
62+
@v_args(inline=True)
63+
def hat(self, child):
64+
return child
65+
66+
@v_args(inline=True)
67+
def param(self, child):
68+
return child
69+
70+
@v_args(inline=True)
71+
def value_param(self, name):
72+
return name
73+
74+
@v_args(inline=True)
75+
def bool_param(self, name):
76+
return f"<{name}>"
77+
78+
@v_args(inline=True)
79+
def event_hat(self, event_name):
80+
return f"when ({event_name})"
81+
82+
def block_hat(self, items):
83+
name, *params = items
84+
params_str = ", ".join(params)
85+
return f"custom_block {name} ({params_str})"
86+
87+
@v_args(inline=True)
88+
def PREPROC_INSTR(self, content):
89+
return f"%%{content}%%"
90+
91+
@v_args(inline=True)
92+
def COMMMENT(self, content):
93+
return f"##{content}##"
94+
95+
def block(self, items):
96+
params = []
97+
inner_blocks = []
98+
for i in items[1:]:
99+
if not isinstance(i, Tree):
100+
continue
101+
if str(i.data) == "block_content":
102+
inner_blocks.extend(i.children)
103+
if str(i.data) == "block_params":
104+
params.extend(i.children)
105+
block_name = items[0]
106+
block_text = f"{block_name}({', '.join(params)})" if params or not inner_blocks else f"{block_name}"
107+
if len(inner_blocks) > 0:
108+
blocks_content = "\n".join(inner_blocks)
109+
indented_content = self._indent(blocks_content)
110+
block_text += f" {{\n{indented_content}\n}}"
111+
return block_text
112+
113+
def LITERAL_NUMBER(self, number: str):
114+
return number
115+
116+
@v_args(inline=True)
117+
def expr(self, item):
118+
return item
119+
120+
@v_args(inline=True)
121+
def low_expr1(self, item):
122+
if " " in item:
123+
return f"({item})"
124+
return item
125+
126+
@v_args(inline=True)
127+
def low_expr2(self, item):
128+
return item
129+
130+
def addition(self, items):
131+
return items[0] + " + " + items[1]
132+
133+
def subtraction(self, items):
134+
return items[0] + " - " + items[1]
135+
136+
def multiplication(self, items):
137+
return items[0] + " * " + items[1]
138+
139+
def division(self, items):
140+
return items[0] + " / " + items[1]
141+
142+
def top_level_block(self, items):
143+
first_item = items[0]
144+
if first_item.startswith("%%") or first_item.startswith("##"):
145+
return first_item
146+
147+
hat, *blocks = items
148+
blocks_content = "\n".join(blocks)
149+
indented_content = self._indent(blocks_content)
150+
return f"{hat} {{\n{indented_content}\n}}"
151+
152+
def start(self, items):
153+
return "\n\n".join(items)
154+
155+
def pretty_unparse(tree: Tree):
156+
return PrettyUnparser().transform(tree)
157+
158+
if __name__ == "__main__":
159+
EXAMPLE_FILE = Path(__file__).parent / "example.txt"
160+
tree = parse(EXAMPLE_FILE)
161+
print(tree.pretty())
162+
print()
163+
print()
164+
print(tree)
165+
print()
166+
print()
167+
print(unparse(tree))
168+
print()
169+
print()
170+
print(pretty_unparse(tree))

scratchattach/site/classroom.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from . import user, activity
1717
from ._base import BaseSiteComponent
1818
from scratchattach.utils import exceptions, commons
19+
from scratchattach.utils.commons import headers
1920

2021

2122
@dataclass

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
long_description=LONG_DESCRIPTION,
2222
packages=find_packages(),
2323
install_requires=requirements,
24+
extras_require={
25+
"lark": ["lark"]
26+
},
2427
keywords=['scratch api', 'scratchattach', 'scratch api python', 'scratch python', 'scratch for python', 'scratch', 'scratch cloud', 'scratch cloud variables', 'scratch bot'],
2528
url='https://scratchattach.tim1de.net',
2629
classifiers=[

0 commit comments

Comments
 (0)