diff --git a/scratchattach/editor/code_translation/__init__.py b/scratchattach/editor/code_translation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scratchattach/editor/code_translation/example.txt b/scratchattach/editor/code_translation/example.txt new file mode 100644 index 00000000..165500cd --- /dev/null +++ b/scratchattach/editor/code_translation/example.txt @@ -0,0 +1,13 @@ +## For testing. The file extension is temporary and might change in the future. ## + +WHEN (green_flag_clicked) { + until { + say()say() + } + sa() +} +custom_block hello_%s_ (, awd) { + until (1) { + say((8 + 2) * (6 - 3)) + } +} \ No newline at end of file diff --git a/scratchattach/editor/code_translation/language.lark b/scratchattach/editor/code_translation/language.lark new file mode 100644 index 00000000..f9ba82fe --- /dev/null +++ b/scratchattach/editor/code_translation/language.lark @@ -0,0 +1,56 @@ +start: top_level_block* + +top_level_block: hat "{" block* "}" + | PREPROC_INSTR + | COMMMENT + +PREPROC_INSTR: "%%" _PREPROC_INSTR_CONTENT "%%" + +_PREPROC_INSTR_CONTENT: /[\s\S]+?/ + +COMMMENT: "##" _COMMMENT_CONTENT "##" + +_COMMMENT_CONTENT: /[\s\S]+?/ + +hat: [event_hat | block_hat] + +event_hat: "when"i "(" EVENT ")" +block_hat: "custom_block"i BLOCK_NAME "(" [ param ("," param)* ] ")" + +param: value_param + | bool_param +value_param: PARAM_NAME +bool_param: "<" PARAM_NAME ">" + +EVENT: "green_flag_clicked"i + +block: CONTROL_BLOCK_NAME ["(" block_params ")"] "{" block_content "}" + | BLOCK_NAME ["(" block_params ")"] [";" | "\n" | " "] + +block_params: expr* +block_content: block* + +expr: addition | subtraction | multiplication | division | LITERAL_NUMBER | "(" expr ")" + +low_expr1: ("(" expr ")") | LITERAL_NUMBER +low_expr2: low_expr1 | multiplication | division + +addition: expr "+" expr +subtraction: expr "-" low_expr2 +multiplication: low_expr2 "*" low_expr2 +division: low_expr2 "/" low_expr1 + +CONTROL_BLOCK_NAME: "repeat"i + | "until"i + | "forever"i + +PARAM_NAME: ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+ +BLOCK_NAME: [MODULE_NAME "."] ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+ + +MODULE_NAME: "params"i + | "vars"i + | "lists"i + +WS: /\s+/ +%ignore WS +%import common.SIGNED_NUMBER -> LITERAL_NUMBER \ No newline at end of file diff --git a/scratchattach/editor/code_translation/parse.py b/scratchattach/editor/code_translation/parse.py new file mode 100644 index 00000000..e3347f13 --- /dev/null +++ b/scratchattach/editor/code_translation/parse.py @@ -0,0 +1,170 @@ +from __future__ import annotations +from pathlib import Path +from typing import Union, Generic, TypeVar +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from lark import Lark, Transformer, Tree, Token, v_args +from lark.reconstruct import Reconstructor + +R = TypeVar("R") +class SupportsRead(ABC, Generic[R]): + @abstractmethod + def read(self, size: int | None = -1) -> R: + pass + +LANG_PATH = Path(__file__).parent / "language.lark" + +lang = Lark(LANG_PATH.read_text(), maybe_placeholders=False) +reconstructor = Reconstructor(lang) + +def parse(script: Union[str, bytes, SupportsRead[str], Path]) -> Tree: + if isinstance(script, Path): + script = script.read_text() + if isinstance(script, SupportsRead): + read_data = script.read() + assert isinstance(read_data, str) + script = read_data + if isinstance(script, bytes): + script = script.decode("utf-8") + return lang.parse(script) + +def unparse(tree: Tree) -> str: + return reconstructor.reconstruct(tree) + +class PrettyUnparser(Transformer): + INDENT_STRING = " " + + @classmethod + def _indent(cls, text): + if not text: + return "" + return "\n".join(cls.INDENT_STRING + line for line in text.splitlines()) + + def PARAM_NAME(self, token): + return token.value + + def BLOCK_NAME(self, token): + return token.value + + def EVENT(self, token): + return token.value + + def CONTROL_BLOCK_NAME(self, token): + return token.value + + def _PREPROC_INSTR_CONTENT(self, token): + return token.value + + def _COMMMENT_CONTENT(self, token): + return token.value + + @v_args(inline=True) + def hat(self, child): + return child + + @v_args(inline=True) + def param(self, child): + return child + + @v_args(inline=True) + def value_param(self, name): + return name + + @v_args(inline=True) + def bool_param(self, name): + return f"<{name}>" + + @v_args(inline=True) + def event_hat(self, event_name): + return f"when ({event_name})" + + def block_hat(self, items): + name, *params = items + params_str = ", ".join(params) + return f"custom_block {name} ({params_str})" + + @v_args(inline=True) + def PREPROC_INSTR(self, content): + return f"%%{content}%%" + + @v_args(inline=True) + def COMMMENT(self, content): + return f"##{content}##" + + def block(self, items): + params = [] + inner_blocks = [] + for i in items[1:]: + if not isinstance(i, Tree): + continue + if str(i.data) == "block_content": + inner_blocks.extend(i.children) + if str(i.data) == "block_params": + params.extend(i.children) + block_name = items[0] + block_text = f"{block_name}({', '.join(params)})" if params or not inner_blocks else f"{block_name}" + if len(inner_blocks) > 0: + blocks_content = "\n".join(inner_blocks) + indented_content = self._indent(blocks_content) + block_text += f" {{\n{indented_content}\n}}" + return block_text + + def LITERAL_NUMBER(self, number: str): + return number + + @v_args(inline=True) + def expr(self, item): + return item + + @v_args(inline=True) + def low_expr1(self, item): + if " " in item: + return f"({item})" + return item + + @v_args(inline=True) + def low_expr2(self, item): + return item + + def addition(self, items): + return items[0] + " + " + items[1] + + def subtraction(self, items): + return items[0] + " - " + items[1] + + def multiplication(self, items): + return items[0] + " * " + items[1] + + def division(self, items): + return items[0] + " / " + items[1] + + def top_level_block(self, items): + first_item = items[0] + if first_item.startswith("%%") or first_item.startswith("##"): + return first_item + + hat, *blocks = items + blocks_content = "\n".join(blocks) + indented_content = self._indent(blocks_content) + return f"{hat} {{\n{indented_content}\n}}" + + def start(self, items): + return "\n\n".join(items) + +def pretty_unparse(tree: Tree): + return PrettyUnparser().transform(tree) + +if __name__ == "__main__": + EXAMPLE_FILE = Path(__file__).parent / "example.txt" + tree = parse(EXAMPLE_FILE) + print(tree.pretty()) + print() + print() + print(tree) + print() + print() + print(unparse(tree)) + print() + print() + print(pretty_unparse(tree)) \ No newline at end of file diff --git a/scratchattach/site/classroom.py b/scratchattach/site/classroom.py index 82fec6f3..3d8149ef 100644 --- a/scratchattach/site/classroom.py +++ b/scratchattach/site/classroom.py @@ -16,6 +16,7 @@ from . import user, activity from ._base import BaseSiteComponent from scratchattach.utils import exceptions, commons +from scratchattach.utils.commons import headers @dataclass diff --git a/setup.py b/setup.py index 32993d68..3336dddd 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,9 @@ long_description=LONG_DESCRIPTION, packages=find_packages(), install_requires=requirements, + extras_require={ + "lark": ["lark"] + }, keywords=['scratch api', 'scratchattach', 'scratch api python', 'scratch python', 'scratch for python', 'scratch', 'scratch cloud', 'scratch cloud variables', 'scratch bot'], url='https://scratchattach.tim1de.net', classifiers=[