From e5b390c0b63b2b2ba534fa0ab1ce8995132aa4bf Mon Sep 17 00:00:00 2001 From: DeleMike Date: Sun, 2 Jul 2023 14:07:59 +0100 Subject: [PATCH] update files update doc string fix build issue --- .gitignore | 3 + build.sh | 6 +- starfyre/__init__.py | 76 ++--- starfyre/__main__.py | 137 +++++---- starfyre/compiler.py | 355 +++++++++++----------- starfyre/component.py | 54 ++-- starfyre/dom_methods.py | 238 +++++++-------- starfyre/global_components.py | 14 +- starfyre/js/store.js | 110 +++---- starfyre/parser.py | 540 +++++++++++++++++----------------- starfyre/transpiler.py | 232 +++++++-------- 11 files changed, 891 insertions(+), 874 deletions(-) diff --git a/.gitignore b/.gitignore index e56f842..d937fde 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ dist test-application/__pycache__/ test-application/dist/* test-application/starfyre-dist/* + +# dev file +starfyre/run_test.py diff --git a/build.sh b/build.sh index a82a245..efe9cb2 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,7 @@ #!/bin/sh -python3 starfyre --dev=True --path="test-application/" && python -m starfyre --build=True --path="test-application/" +<<<<<<< HEAD +python3 starfyre --path="test-application" +======= +python3 starfyre --build --path="my-first-app" +>>>>>>> 97c7e17... fix build issue diff --git a/starfyre/__init__.py b/starfyre/__init__.py index 0792a51..c2b4e00 100644 --- a/starfyre/__init__.py +++ b/starfyre/__init__.py @@ -1,38 +1,38 @@ -import inspect -from starfyre.dom_methods import render, render_root -from .compiler import compile - -from starfyre.component import Component -from .transpiler import transpile - -from .parser import RootParser - - -def create_component(pyml="", css="", js="", client_side_python=""): - if client_side_python: - new_js = transpile(client_side_python) + js - js = new_js - - local_variables = inspect.currentframe().f_back.f_back.f_locals.copy() - global_variables = inspect.currentframe().f_back.f_back.f_globals.copy() - - parser = RootParser(local_variables, global_variables, css, js) - pyml = pyml.strip("\n").strip() - parser.feed(pyml) - parser.close() - pyml_root = parser.get_root() - - if pyml_root is None: - return Component("div", {}, [], {}, {}, uuid="store", js=js) - - return pyml_root - - -__all__ = [ - "create_component", - "render", - "render_root", - "compile", - "transpile", - "Component", -] +import inspect +from starfyre.dom_methods import render, render_root +from .compiler import compile + +from starfyre.component import Component +from .transpiler import transpile + +from .parser import RootParser + + +def create_component(pyml="", css="", js="", client_side_python=""): + if client_side_python: + new_js = transpile(client_side_python) + js + js = new_js + + local_variables = inspect.currentframe().f_back.f_back.f_locals.copy() + global_variables = inspect.currentframe().f_back.f_back.f_globals.copy() + + parser = RootParser(local_variables, global_variables, css, js) + pyml = pyml.strip("\n").strip() + parser.feed(pyml) + parser.close() + pyml_root = parser.get_root() + + if pyml_root is None: + return Component("div", {}, [], {}, {}, uuid="store", js=js) + + return pyml_root + + +__all__ = [ + "create_component", + "render", + "render_root", + "compile", + "transpile", + "Component", +] diff --git a/starfyre/__main__.py b/starfyre/__main__.py index c6fd0c8..5060386 100644 --- a/starfyre/__main__.py +++ b/starfyre/__main__.py @@ -1,64 +1,73 @@ -from starfyre import compile -from pathlib import Path -import sys -import os -import shutil -import subprocess -import click -import importlib.resources as pkg_resources - - -def write_js_file(path): - dist_path = Path(path) / "dist" - dist_path.mkdir(exist_ok=True) - js_store = pkg_resources.path("starfyre.js", "store.js") - shutil.copy(str(js_store), path + "/dist/store.js") - - -def create_main_file(path): - output_file_path = path + "/build/__main__.py" - write_js_file(path) - - with open(output_file_path, "w") as f: - f.write( - """ -from . import app -import os -from pathlib import Path - - -if __name__ == '__main__': - path_ = os.path.dirname(os.path.abspath(__file__)) - directory = Path(path_ ) / ".." / "dist" - if not directory.exists(): - directory.mkdir() - - with open(f"{directory}/index.html", "w") as f: - f.write("") - f.write(app) -""" - ) - - -@click.command() -@click.option("--path", default=".", help="Path to the project") -@click.option("--dev", default=False, help="Start the compilation and generate the build package.") -@click.option("--build", default=False, help="Start the build package") -def main(path, dev, build): - if dev: - path_ = path + "/__init__.py" - # get absolute path - path = os.path.abspath(path_) - compile(path) - create_main_file(os.path.dirname(path)) - if build: - subprocess.run( - [sys.executable, "-m", "build"], - cwd=path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - -if __name__ == "__main__": - main() +from starfyre import compile +from pathlib import Path +import sys +import os +import shutil +import subprocess +import click +import importlib.resources as pkg_resources + + +def write_js_file(path): + dist_path = Path(path) / "dist" + dist_path.mkdir(exist_ok=True) + js_store = pkg_resources.path("starfyre.js", "store.js") + shutil.copy(str(js_store), path + "/dist/store.js") + + +def create_main_file(path): + output_file_path = path + "/build/__main__.py" + write_js_file(path) + + with open(output_file_path, "w") as f: + f.write( + """ +from . import app +import os +from pathlib import Path + + +if __name__ == '__main__': + path_ = os.path.dirname(os.path.abspath(__file__)) + directory = Path(path_ ) / ".." / "dist" + if not directory.exists(): + directory.mkdir() + + with open(f"{directory}/index.html", "w") as f: + f.write("") + f.write(app) +""" + ) + + +@click.command() +@click.option("--path", default=".", help="Path to the project") +@click.option("--build", is_flag=True, help="Compile and build package") +def main(path, build): + """ + Command-line interface to compile and build a project. + + Args: + path (str): Path to the project directory. + build (bool): Whether to start the build package. + """ + # Convert path to absolute path + path = os.path.abspath(path) + + # Compile and build project + compile(os.path.join(path, "__init__.py")) + create_main_file(path) + + if build: + # Start/run project + build_dir = os.path.join(path, "build") + subprocess.run( + [sys.executable, "-m", "build"], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +if __name__ == "__main__": + main() diff --git a/starfyre/compiler.py b/starfyre/compiler.py index 1b9cd8d..4a30bf7 100644 --- a/starfyre/compiler.py +++ b/starfyre/compiler.py @@ -1,177 +1,178 @@ -import os -from pathlib import Path - - -def get_fyre_files(project_dir): - fyre_files = [] - for file in os.listdir(project_dir): - if file.endswith(".fyre"): - fyre_files.append(file) - return fyre_files - - -def parse(fyre_file_name): - def remove_empty_lines_from_end(lines): - while lines and lines[-1] == "\n": - lines.pop() - - while lines and lines[0] == "\n": - lines.pop(0) - - if lines == []: - return [""] - return lines - - current_line_type = "python" - python_lines = [] - css_lines = [] - pyml_lines = [] - js_lines = [] - client_side_python = [] - - with open(fyre_file_name, "r") as fyre_file: - for line in fyre_file.readlines(): - if line.startswith("" in line - or "" in line - or "" in line - or "--" in line - ): - current_line_type = "python" - continue - - if current_line_type == "python": - python_lines.append(line) - elif current_line_type == "css": - css_lines.append(line) - elif current_line_type == "pyml": - pyml_lines.append(line) - elif current_line_type == "js": - js_lines.append(line) - elif current_line_type == "client": - client_side_python.append(line) - - return ( - remove_empty_lines_from_end(python_lines), - remove_empty_lines_from_end(css_lines), - remove_empty_lines_from_end(pyml_lines), - remove_empty_lines_from_end(js_lines), - remove_empty_lines_from_end(client_side_python), - ) - - -def python_transpiled_string( - pyml_lines, css_lines, js_lines, client_side_python, file_name -): - file_name = file_name.replace(".py", "").split("/")[-1] - pyml_lines = "".join(pyml_lines) - css_lines = "".join(css_lines) - js_lines = "".join(js_lines) - client_side_python = "".join(client_side_python) - - root_name = None - - if "__init__" in file_name: - root_name = "app" - else: - root_name = file_name - - if root_name == "app": - return f''' -from starfyre import create_component, render_root - -def fx_{root_name}(): - # not nesting the code to preserve the frames - component = create_component(""" -{pyml_lines} -""", css=""" -{css_lines} -""", js=""" -{js_lines} -""", client_side_python=""" -{client_side_python} -""" -) - return render_root(component) - -{root_name}=fx_{root_name}() -''' - else: - return f''' -from starfyre import create_component - -def fx_{root_name}(): - component = create_component(""" -{pyml_lines} -""", css=""" -{css_lines} -""", js=""" -{js_lines} -""", client_side_python=""" -{client_side_python} -""" -) - return component - -{root_name}=fx_{root_name}() -''' - - -def transpile_to_python( - python_lines, - css_lines, - pyml_lines, - js_lines, - client_side_python, - output_file_name, - project_dir, -): - final_python_lines = ["".join(python_lines)] - - main_content = python_transpiled_string( - pyml_lines, css_lines, js_lines, client_side_python, output_file_name - ) - - final_python_lines.append(main_content) - - file_name = output_file_name.split("/")[-1] - output_file_name = project_dir / "build" / file_name - - with open(output_file_name, "w") as output_file: - output_file.write("".join(final_python_lines)) - - -def compile(entry_file_name): - project_dir = Path(os.path.dirname(entry_file_name)) - - build_dir = project_dir / "build" - build_dir.mkdir(exist_ok=True) - - fyre_files = get_fyre_files(project_dir) - - for fyre_file in fyre_files: - python_file_name = fyre_file.replace(".fyre", ".py") - python_lines, css_lines, pyml_lines, js_lines, client_side_python = parse( - project_dir / fyre_file - ) - transpile_to_python( - python_lines, - css_lines, - pyml_lines, - js_lines, - client_side_python, - python_file_name, - project_dir, - ) +import os +from pathlib import Path + + +def get_fyre_files(project_dir): + fyre_files = [] + for file in os.listdir(project_dir): + if file.endswith(".fyre"): + fyre_files.append(file) + return fyre_files + + +def parse(fyre_file_name): + def remove_empty_lines_from_end(lines): + while lines and lines[-1] == "\n": + lines.pop() + + while lines and lines[0] == "\n": + lines.pop(0) + + if lines == []: + return [""] + return lines + + current_line_type = "python" + python_lines = [] + css_lines = [] + pyml_lines = [] + js_lines = [] + client_side_python = [] + + with open(fyre_file_name, "r") as fyre_file: + for line in fyre_file.readlines(): + if line.startswith("" in line + or "" in line + or "" in line + or "--" in line + ): + current_line_type = "python" + continue + + if current_line_type == "python": + python_lines.append(line) + elif current_line_type == "css": + css_lines.append(line) + elif current_line_type == "pyml": + pyml_lines.append(line) + elif current_line_type == "js": + js_lines.append(line) + elif current_line_type == "client": + client_side_python.append(line) + + return ( + remove_empty_lines_from_end(python_lines), + remove_empty_lines_from_end(css_lines), + remove_empty_lines_from_end(pyml_lines), + remove_empty_lines_from_end(js_lines), + remove_empty_lines_from_end(client_side_python), + ) + + +def python_transpiled_string( + pyml_lines, css_lines, js_lines, client_side_python, file_name +): + file_name = file_name.replace(".py", "").split("/")[-1] + pyml_lines = "".join(pyml_lines) + css_lines = "".join(css_lines) + js_lines = "".join(js_lines) + client_side_python = "".join(client_side_python) + + root_name = None + + if "__init__" in file_name: + root_name = "app" + else: + root_name = file_name + + if root_name == "app": + return f''' +from starfyre import create_component, render_root + +def fx_{root_name}(): + # not nesting the code to preserve the frames + component = create_component(""" +{pyml_lines} +""", css=""" +{css_lines} +""", js=""" +{js_lines} +""", client_side_python=""" +{client_side_python} +""" +) + return render_root(component) + +{root_name}=fx_{root_name}() +''' + else: + return f''' +from starfyre import create_component + +def fx_{root_name}(): + component = create_component(""" +{pyml_lines} +""", css=""" +{css_lines} +""", js=""" +{js_lines} +""", client_side_python=""" +{client_side_python} +""" +) + return component + +{root_name}=fx_{root_name}() +''' + + +def transpile_to_python( + python_lines, + css_lines, + pyml_lines, + js_lines, + client_side_python, + output_file_name, + project_dir, +): + final_python_lines = ["".join(python_lines)] + + main_content = python_transpiled_string( + pyml_lines, css_lines, js_lines, client_side_python, output_file_name + ) + + final_python_lines.append(main_content) + + file_name = output_file_name.split("/")[-1] + output_file_name = project_dir / "build" / file_name + + with open(output_file_name, "w") as output_file: + output_file.write("".join(final_python_lines)) + + +def compile(entry_file_name): + project_dir = Path(os.path.dirname(entry_file_name)) + + build_dir = project_dir / "build" + build_dir.parent.mkdir(parents=True, exist_ok=True) # Create parent directory if it doesn't exist + build_dir.mkdir(exist_ok=True) + + fyre_files = get_fyre_files(project_dir) + + for fyre_file in fyre_files: + python_file_name = fyre_file.replace(".fyre", ".py") + python_lines, css_lines, pyml_lines, js_lines, client_side_python = parse( + project_dir / fyre_file + ) + transpile_to_python( + python_lines, + css_lines, + pyml_lines, + js_lines, + client_side_python, + python_file_name, + project_dir, + ) diff --git a/starfyre/component.py b/starfyre/component.py index af3993b..ea76f26 100644 --- a/starfyre/component.py +++ b/starfyre/component.py @@ -1,27 +1,27 @@ -from dataclasses import dataclass -from typing import Any, Optional - - -@dataclass -class Component: - tag: str - props: dict - children: list - event_listeners: dict - state: dict - uuid: Any - signal: str = "" - original_data: str = "" - data: str = "" - parentComponent: Optional[Any] = None - html: str = "" - css: str = "" - js: str = "" - # on any property change, rebuild the tree - - def render(self): - pass - - @property - def is_text_component(self): - return self.tag == "TEXT_NODE" +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class Component: + tag: str + props: dict + children: list + event_listeners: dict + state: dict + uuid: Any + signal: str = "" + original_data: str = "" + data: str = "" + parentComponent: Optional[Any] = None + html: str = "" + css: str = "" + js: str = "" + # on any property change, rebuild the tree + + def render(self): + pass + + @property + def is_text_component(self): + return self.tag == "TEXT_NODE" diff --git a/starfyre/dom_methods.py b/starfyre/dom_methods.py index a6ea3ea..4a66586 100644 --- a/starfyre/dom_methods.py +++ b/starfyre/dom_methods.py @@ -1,119 +1,119 @@ -import re -from functools import partial -from uuid import uuid4 - -from .transpiler import transpile_to_js - - -from .component import Component - - -def assign_event_listeners(event_listener_name, event_listener): - # component.dom.addEventListener(event_type, create_proxy(event_listener)) - js_event_listener = transpile_to_js(event_listener) - html = f" {event_listener_name}='{event_listener.__name__}()' " - - return html, js_event_listener - - -def render_helper(component: Component) -> tuple[str, str, str]: - # Add event listeners - def is_listener(name): - return name.startswith("on") - - def is_attribute(name): - return not is_listener(name) and name != "children" - - parentElement = component.parentComponent - html = "\n" - css = "\n" - js = "\n" - if parentElement is None: - parentElement = Component("div", {"id": "root"}, [], {}, {}, uuid=uuid4()) - component.parentComponent = parentElement - - tag = component.tag - props = component.props - state = component.state - data = component.data - event_listeners = component.event_listeners - - # Create DOM element - if component.is_text_component: - # find all the names in "{}" and print them - matches = re.findall(r"{(.*?)}", data) - for match in matches: - if match in state: - function = state[match] - function = partial(function, component) - data = component.data.replace(f"{{{ match }}}", str(function())) - else: - print("No match found for", match) - - component.parentComponent.uuid = component.uuid - html += f"{data}\n" - component.html = html - - if component.signal: - js += f""" - component = document.getElementById('{component.uuid}'); - addDomIdToMap('{component.uuid}', "{ component.signal }"); - if (component) {{ - component.innerText = `${{{ component.signal }}}`; - }} - """ - - return html, css, js - - if component.css: - css += f"{ component.css }\n" - - if component.js: - js += f"{component.js}\n" - - html += f"<{tag} id='{component.uuid}' " - - # this is not when the component is a text component - prop_string = "" - for name in props: - if is_attribute(name): - prop_string += f" {name}='{props[name]}' " - - for name, function in event_listeners.items(): - if is_listener(name): - new_html, new_js = assign_event_listeners(name, function) - js += new_js - html += new_html - - if html.endswith(">"): - html.removesuffix(">") - - if not component.is_text_component: - html += f"{prop_string} >" - - # Render children - children = component.children - # childElements.forEach(childElement => render(childElement, dom)); - for childElement in children: - childElement.parentElement = component - new_html, new_css, new_js = render_helper(childElement) - html += new_html - css += new_css - js += new_js - - html += f"\n" - - component.html = html - return html, css, js - - -def render(component: Component) -> str: - html, css, js = render_helper(component) - final_html = f"{html}" - return final_html - - -def render_root(component: Component) -> str: - html, css, js = render_helper(component) - final_html = f"
{html}
" - return final_html +import re +from functools import partial +from uuid import uuid4 + +from .transpiler import transpile_to_js + + +from .component import Component + + +def assign_event_listeners(event_listener_name, event_listener): + # component.dom.addEventListener(event_type, create_proxy(event_listener)) + js_event_listener = transpile_to_js(event_listener) + html = f" {event_listener_name}='{event_listener.__name__}()' " + + return html, js_event_listener + + +def render_helper(component: Component) -> tuple[str, str, str]: + # Add event listeners + def is_listener(name): + return name.startswith("on") + + def is_attribute(name): + return not is_listener(name) and name != "children" + + parentElement = component.parentComponent + html = "\n" + css = "\n" + js = "\n" + if parentElement is None: + parentElement = Component("div", {"id": "root"}, [], {}, {}, uuid=uuid4()) + component.parentComponent = parentElement + + tag = component.tag + props = component.props + state = component.state + data = component.data + event_listeners = component.event_listeners + + # Create DOM element + if component.is_text_component: + # find all the names in "{}" and print them + matches = re.findall(r"{(.*?)}", data) + for match in matches: + if match in state: + function = state[match] + function = partial(function, component) + data = component.data.replace(f"{{{ match }}}", str(function())) + else: + print("No match found for", match) + + component.parentComponent.uuid = component.uuid + html += f"{data}\n" + component.html = html + + if component.signal: + js += f""" + component = document.getElementById('{component.uuid}'); + addDomIdToMap('{component.uuid}', "{ component.signal }"); + if (component) {{ + component.innerText = `${{{ component.signal }}}`; + }} + """ + + return html, css, js + + if component.css: + css += f"{ component.css }\n" + + if component.js: + js += f"{component.js}\n" + + html += f"<{tag} id='{component.uuid}' " + + # this is not when the component is a text component + prop_string = "" + for name in props: + if is_attribute(name): + prop_string += f" {name}='{props[name]}' " + + for name, function in event_listeners.items(): + if is_listener(name): + new_html, new_js = assign_event_listeners(name, function) + js += new_js + html += new_html + + if html.endswith(">"): + html.removesuffix(">") + + if not component.is_text_component: + html += f"{prop_string} >" + + # Render children + children = component.children + # childElements.forEach(childElement => render(childElement, dom)); + for childElement in children: + childElement.parentElement = component + new_html, new_css, new_js = render_helper(childElement) + html += new_html + css += new_css + js += new_js + + html += f"\n" + + component.html = html + return html, css, js + + +def render(component: Component) -> str: + html, css, js = render_helper(component) + final_html = f"{html}" + return final_html + + +def render_root(component: Component) -> str: + html, css, js = render_helper(component) + final_html = f"
{html}
" + return final_html diff --git a/starfyre/global_components.py b/starfyre/global_components.py index 775375b..1b47f20 100644 --- a/starfyre/global_components.py +++ b/starfyre/global_components.py @@ -1,7 +1,7 @@ -from collections import defaultdict - -components = defaultdict(list) -new_global_components = dict() - - -__all__ = ["components"] +from collections import defaultdict + +components = defaultdict(list) +new_global_components = dict() + + +__all__ = ["components"] diff --git a/starfyre/js/store.js b/starfyre/js/store.js index d52e45d..212bc1c 100644 --- a/starfyre/js/store.js +++ b/starfyre/js/store.js @@ -1,55 +1,55 @@ -observers = {}; // uuid , list[observers] - -const domIdMap = {}; - -function addDomIdToMap(domId, pyml) { - domIdMap[domId] = pyml; -} - -function getPymlFromDomId(domId) { - return domIdMap[domId]; -} - -function render(domId) { - const element = document.getElementById(domId); - const pyml = getPymlFromDomId(domId); - element.innerHTML = `${eval(pyml)}`; -} - -function create_signal(initial_state) { - let id = Math.random() * 1000000000000000; // simulating uuid - let state = initial_state; - - function use_signal(domId) { - if (!domId) { - // when the domId is not provided, it means that the signal is used in a function component - return store[id] || initial_state; - } - - if (observers[id]) { - observers[id].push(domId); - } else { - observers[id] = [domId]; - } - - return state; - } - - function set_signal(newState) { - state = newState; - - if (!observers[id]) { - return; - } - - observers[id].forEach((element) => { - render(element); - }); - } - - function get_signal() { - return state; - } - - return [use_signal, set_signal, get_signal]; -} +observers = {}; // uuid , list[observers] + +const domIdMap = {}; + +function addDomIdToMap(domId, pyml) { + domIdMap[domId] = pyml; +} + +function getPymlFromDomId(domId) { + return domIdMap[domId]; +} + +function render(domId) { + const element = document.getElementById(domId); + const pyml = getPymlFromDomId(domId); + element.innerHTML = `${eval(pyml)}`; +} + +function create_signal(initial_state) { + let id = Math.random() * 1000000000000000; // simulating uuid + let state = initial_state; + + function use_signal(domId) { + if (!domId) { + // when the domId is not provided, it means that the signal is used in a function component + return store[id] || initial_state; + } + + if (observers[id]) { + observers[id].push(domId); + } else { + observers[id] = [domId]; + } + + return state; + } + + function set_signal(newState) { + state = newState; + + if (!observers[id]) { + return; + } + + observers[id].forEach((element) => { + render(element); + }); + } + + function get_signal() { + return state; + } + + return [use_signal, set_signal, get_signal]; +} diff --git a/starfyre/parser.py b/starfyre/parser.py index 95c8b7d..fd64287 100644 --- a/starfyre/parser.py +++ b/starfyre/parser.py @@ -1,270 +1,270 @@ -import re -from html.parser import HTMLParser -from uuid import uuid4 - -from starfyre.transpiler import transpile - -from .component import Component - - -def extract_functions(obj): - functions = {} - for key, value in obj.items(): - if callable(value): - functions[key] = value - - return functions - - -class RootParser(HTMLParser): - # this is the grammar for the parser - # we need cover all the grammar rules - - generic_tags = ["div", "p", "b", "span", "i", "button"] - - def __init__(self, component_local_variables, component_global_variables, css, js): - super().__init__() - self.stack: list[tuple[Component, int]] = [] - self.children = [] - self.current_depth = 0 - self.css = css - self.js = js - - # these are the event handlers and the props - self.local_variables = component_local_variables - self.global_variables = component_global_variables - - self.components = self.extract_components( - {**self.local_variables, **self.global_variables} - ) - # populate the dict with the components - - def extract_components(self, local_functions): - components = {} - for key, value in local_functions.items(): - if isinstance(value, Component): - components[key] = value - - return components - - def is_event_listener(self, name): - return name.startswith("on") - - def handle_starttag(self, tag, attrs): - # logic should be to just create an empty component on start - # and fill the contents on the end tag - props = {} - state = {} - event_listeners = {} - self.current_depth += 1 - - for attr in attrs: - if attr[1].startswith("{") and attr[1].endswith("}"): - attr_value = attr[1].strip("{").strip("}").strip(" ") - if self.is_event_listener(attr[0]): - event_handler = None - if attr_value in self.global_variables: - event_handler = self.global_variables[attr_value] - - # we are giving the priority to local functions - if attr_value in self.local_variables: - event_handler = self.local_variables[attr_value] - - if event_handler is None: - print("Event handler not found") - - event_listeners[attr[0]] = event_handler - # these are functions, so we will replace them with the actual function - else: - # here we need to check if these are functions - # or state objects or just regular text - if attr_value in self.local_variables and self.is_state( - self.local_variables[attr_value] - ): - state[attr[0]] = self.local_variables[attr_value] - props[attr[0]] = self.local_variables[attr_value]() - else: - props[attr[0]] = attr[1] - - if tag not in self.generic_tags and tag in self.components: - component = self.components[tag] - tag = component.tag - component.props = {**component.props, **props} - component.state = {**component.state, **state} - component.event_listeners = {**component.event_listeners, **event_listeners} - self.stack.append((component, self.current_depth)) - - return - - component = Component( - tag, - props, - [], - event_listeners, - state, - js=self.js, - css=self.css, - uuid=uuid4(), - ) - - # instead of assiging tags we assign uuids - self.stack.append((component, self.current_depth)) - [(element[0].tag, element[1]) for element in self.stack] - - def handle_endtag(self, tag): - # we need to check if the tag is a default component or a custom component - # if it is a custom component, we get the element from the custom components dict - if tag not in self.generic_tags and tag in self.components: - component = self.components[tag] - tag = component.tag - - # need to check the if this is always true - parent_node, parent_depth = self.stack[ - -1 - ] # based on the assumption that the stack is not empty - - while len(self.children) > 0: - child, child_depth = self.children[0] - if child_depth == parent_depth + 1: - self.children.pop(0) - self.stack[-1][0].children.insert(0, child) - else: - break # we have reached the end of the children - - self.stack.pop() - self.current_depth -= 1 - - if parent_node.tag != "style" and parent_node.tag != "script": - self.children.insert(0, (parent_node, parent_depth)) - - def is_signal(self, str): - if not str: - return False - return "signal" in str - - def inject_uuid(self, signal, uuid): - return signal.replace("()", f"('{uuid}')") - - def handle_data(self, data): - # this is doing too much - # lexing - # parsing - - - # this is a very minimal version of lexing - # we should ideally be writing a separate layer for lexing - data = data.strip().strip("\n").strip(" ") - # regex to find all the elements that are wrapped in {} - - matches = re.findall(r"{(.*?)}", data) - - - # parsing starts here - state = {} - - parent_node, parent_depth = self.stack[-1] - uuid = uuid4() - component_signal = "" - - for match in matches: - # match can be a sentece so we will split it - current_data = None - if match in self.local_variables: - current_data = self.local_variables[match] - elif match in self.global_variables: - current_data = self.global_variables[match] - else: - # we need to handle a case where the eval result is a signal object - if self.is_signal(match): - new_js = transpile(match) - new_js = self.inject_uuid(new_js, uuid) - component_signal = new_js.strip("{").strip("}").strip(";") - print("new js", new_js) - # inject uuid in the signal function call - - current_data = new_js - - else: - eval_result = eval( - match, self.local_variables, self.global_variables - ) - if isinstance(eval_result, Component): - self.stack[-1][0].children.append(eval_result) - return - elif isinstance(eval_result, str): - current_data = eval_result - elif isinstance(eval_result, list): - current_data = " ".join([str(i) for i in eval_result]) - else: - # we need to handle a case where the eval result is a state object - - raise Exception("Variable not found") - - if not self.is_signal(current_data) and not callable(current_data): - print("current data", current_data) - if matches: - data = data.replace("{", "").replace("}", "") - data = data.replace(match, str(current_data)) - - if data == "": - return - - # matches can be of 4 types - # 1. {{variable}} 2. {{function()}} 3. Props = these are all just local and global variables - # 4. State - - # TODO handle state in text node - - # this should never be in the parent stack - # a text node is a child node as soon as it is created - - # add a parent component - # on the wrapper div component - - wrapper_div_component = Component( - "div", - {}, - [], - {}, - state=state, - data=data, - css=self.css, - js=self.js, - signal="", - uuid=uuid, - ) - - wrapper_div_component.children.append( - Component( - "TEXT_NODE", - {}, - [], - {}, - state=state, - data=data, - css=self.css, - js=self.js, - signal=component_signal, - uuid=uuid, - ) - ) - - parent_node.children.insert(0, wrapper_div_component) - - print( - "parent node", - parent_node.tag, - parent_node.children, - "for the text node ", - data, - ) - - def get_stack(self): - return self.stack - - def get_root(self): - if len(self.children) != 0: - return self.children[0][0] - return Component( - "div", {}, [], {}, state={}, data="", css=self.css, js=self.js, uuid=uuid4() - ) +import re +from html.parser import HTMLParser +from uuid import uuid4 + +from starfyre.transpiler import transpile + +from .component import Component + + +def extract_functions(obj): + functions = {} + for key, value in obj.items(): + if callable(value): + functions[key] = value + + return functions + + +class RootParser(HTMLParser): + # this is the grammar for the parser + # we need cover all the grammar rules + + generic_tags = ["div", "p", "b", "span", "i", "button"] + + def __init__(self, component_local_variables, component_global_variables, css, js): + super().__init__() + self.stack: list[tuple[Component, int]] = [] + self.children = [] + self.current_depth = 0 + self.css = css + self.js = js + + # these are the event handlers and the props + self.local_variables = component_local_variables + self.global_variables = component_global_variables + + self.components = self.extract_components( + {**self.local_variables, **self.global_variables} + ) + # populate the dict with the components + + def extract_components(self, local_functions): + components = {} + for key, value in local_functions.items(): + if isinstance(value, Component): + components[key] = value + + return components + + def is_event_listener(self, name): + return name.startswith("on") + + def handle_starttag(self, tag, attrs): + # logic should be to just create an empty component on start + # and fill the contents on the end tag + props = {} + state = {} + event_listeners = {} + self.current_depth += 1 + + for attr in attrs: + if attr[1].startswith("{") and attr[1].endswith("}"): + attr_value = attr[1].strip("{").strip("}").strip(" ") + if self.is_event_listener(attr[0]): + event_handler = None + if attr_value in self.global_variables: + event_handler = self.global_variables[attr_value] + + # we are giving the priority to local functions + if attr_value in self.local_variables: + event_handler = self.local_variables[attr_value] + + if event_handler is None: + print("Event handler not found") + + event_listeners[attr[0]] = event_handler + # these are functions, so we will replace them with the actual function + else: + # here we need to check if these are functions + # or state objects or just regular text + if attr_value in self.local_variables and self.is_state( + self.local_variables[attr_value] + ): + state[attr[0]] = self.local_variables[attr_value] + props[attr[0]] = self.local_variables[attr_value]() + else: + props[attr[0]] = attr[1] + + if tag not in self.generic_tags and tag in self.components: + component = self.components[tag] + tag = component.tag + component.props = {**component.props, **props} + component.state = {**component.state, **state} + component.event_listeners = {**component.event_listeners, **event_listeners} + self.stack.append((component, self.current_depth)) + + return + + component = Component( + tag, + props, + [], + event_listeners, + state, + js=self.js, + css=self.css, + uuid=uuid4(), + ) + + # instead of assiging tags we assign uuids + self.stack.append((component, self.current_depth)) + [(element[0].tag, element[1]) for element in self.stack] + + def handle_endtag(self, tag): + # we need to check if the tag is a default component or a custom component + # if it is a custom component, we get the element from the custom components dict + if tag not in self.generic_tags and tag in self.components: + component = self.components[tag] + tag = component.tag + + # need to check the if this is always true + parent_node, parent_depth = self.stack[ + -1 + ] # based on the assumption that the stack is not empty + + while len(self.children) > 0: + child, child_depth = self.children[0] + if child_depth == parent_depth + 1: + self.children.pop(0) + self.stack[-1][0].children.insert(0, child) + else: + break # we have reached the end of the children + + self.stack.pop() + self.current_depth -= 1 + + if parent_node.tag != "style" and parent_node.tag != "script": + self.children.insert(0, (parent_node, parent_depth)) + + def is_signal(self, str): + if not str: + return False + return "signal" in str + + def inject_uuid(self, signal, uuid): + return signal.replace("()", f"('{uuid}')") + + def handle_data(self, data): + # this is doing too much + # lexing + # parsing + + + # this is a very minimal version of lexing + # we should ideally be writing a separate layer for lexing + data = data.strip().strip("\n").strip(" ") + # regex to find all the elements that are wrapped in {} + + matches = re.findall(r"{(.*?)}", data) + + + # parsing starts here + state = {} + + parent_node, parent_depth = self.stack[-1] + uuid = uuid4() + component_signal = "" + + for match in matches: + # match can be a sentece so we will split it + current_data = None + if match in self.local_variables: + current_data = self.local_variables[match] + elif match in self.global_variables: + current_data = self.global_variables[match] + else: + # we need to handle a case where the eval result is a signal object + if self.is_signal(match): + new_js = transpile(match) + new_js = self.inject_uuid(new_js, uuid) + component_signal = new_js.strip("{").strip("}").strip(";") + print("new js", new_js) + # inject uuid in the signal function call + + current_data = new_js + + else: + eval_result = eval( + match, self.local_variables, self.global_variables + ) + if isinstance(eval_result, Component): + self.stack[-1][0].children.append(eval_result) + return + elif isinstance(eval_result, str): + current_data = eval_result + elif isinstance(eval_result, list): + current_data = " ".join([str(i) for i in eval_result]) + else: + # we need to handle a case where the eval result is a state object + + raise Exception("Variable not found") + + if not self.is_signal(current_data) and not callable(current_data): + print("current data", current_data) + if matches: + data = data.replace("{", "").replace("}", "") + data = data.replace(match, str(current_data)) + + if data == "": + return + + # matches can be of 4 types + # 1. {{variable}} 2. {{function()}} 3. Props = these are all just local and global variables + # 4. State + + # TODO handle state in text node + + # this should never be in the parent stack + # a text node is a child node as soon as it is created + + # add a parent component + # on the wrapper div component + + wrapper_div_component = Component( + "div", + {}, + [], + {}, + state=state, + data=data, + css=self.css, + js=self.js, + signal="", + uuid=uuid, + ) + + wrapper_div_component.children.append( + Component( + "TEXT_NODE", + {}, + [], + {}, + state=state, + data=data, + css=self.css, + js=self.js, + signal=component_signal, + uuid=uuid, + ) + ) + + parent_node.children.insert(0, wrapper_div_component) + + print( + "parent node", + parent_node.tag, + parent_node.children, + "for the text node ", + data, + ) + + def get_stack(self): + return self.stack + + def get_root(self): + if len(self.children) != 0: + return self.children[0][0] + return Component( + "div", {}, [], {}, state={}, data="", css=self.css, js=self.js, uuid=uuid4() + ) diff --git a/starfyre/transpiler.py b/starfyre/transpiler.py index 68274fe..07f4c58 100644 --- a/starfyre/transpiler.py +++ b/starfyre/transpiler.py @@ -1,116 +1,116 @@ -import ast -import inspect - - -class PythonToJsTranspiler(ast.NodeVisitor): - JS_RESERVED_KEYWORDS = {"create_signal", "use_signal", "set_signal", "console.log"} - - def __init__(self): - self.js_code = [] - - def visit_FunctionDef(self, node): - function_name = node.name - parameters = [arg.arg for arg in node.args.args] - param_list = ", ".join(parameters) - - self.js_code.append(f"function {function_name}({param_list}) {{") - self.generic_visit(node) - self.js_code.append("}\n") - - def visit_AsyncFunctionDef(self, node): - function_name = node.name - parameters = [arg.arg for arg in node.args.args] - param_list = ", ".join(parameters) - - self.js_code.append(f"async function {function_name}({param_list}) {{") - self.generic_visit(node) - self.js_code.append("}\n") - - def visit_Assign(self, node): - targets_code = " = ".join([ast.unparse(t) for t in node.targets]) - value_code = ast.unparse(node.value) - - if targets_code.startswith("(") and targets_code.endswith(")"): - targets_code = targets_code.replace("(", "[").replace(")", "]") - - self.js_code.append(f" {targets_code} = {value_code};") - - def visit_Return(self, node): - value_code = ast.unparse(node.value) if node.value else "" - self.js_code.append(f" return {value_code};") - - def visit_Expr(self, node): - if isinstance(node.value, ast.Call): - self.visit_Call(node.value) - else: - value_code = ast.unparse(node.value) if node.value else "" - value_code = value_code.replace("print", "console.log") - self.js_code.append(f" {value_code};") - - def visit_If(self, node): - test_code = ast.unparse(node.test) - self.js_code.append(f" if ({test_code}) {{") - self.generic_visit(node) - self.js_code.append(" }\n") - - def visit_For(self, node): - target_code = ast.unparse(node.target) - iter_code = ast.unparse(node.iter) - self.js_code.append(f" for ({target_code} of {iter_code}) {{") - self.generic_visit(node) - self.js_code.append(" }\n") - - def visit_While(self, node): - test_code = ast.unparse(node.test) - self.js_code.append(f" while ({test_code}) {{") - self.generic_visit(node) - self.js_code.append(" }\n") - - def visit_Call(self, node): - """This is a call node - Call node is e.g. print("Hello, World") - No assignment takes place here - """ - - if isinstance(node.func, ast.Name) and node.func.id == "print": - args_code = ", ".join([ast.unparse(arg) for arg in node.args]) - self.js_code.append(f" console.log({args_code});") - else: - func_code = ast.unparse(node.func) - args_code = ", ".join([ast.unparse(arg) for arg in node.args]) - self.js_code.append(f" {func_code}({args_code});") - - -def transpile(python_code: str) -> str: - tree = ast.parse(python_code) - transpiler = PythonToJsTranspiler() - transpiler.visit(tree) - return "".join(transpiler.js_code) - - -def transpile_to_js(python_code): - code = inspect.getsource(python_code) - return transpile(code) - - -def main(): - python_code = """ -def greet(name): - print("Hello, " + name) - -def add(a, b): - result = greet("World") - return result - -def subtract(a, b): - return add(a, -b) -""" - - js_code = transpile(python_code) - - # get ast of code - print(js_code) - - -if __name__ == "__main__": - main() +import ast +import inspect + + +class PythonToJsTranspiler(ast.NodeVisitor): + JS_RESERVED_KEYWORDS = {"create_signal", "use_signal", "set_signal", "console.log"} + + def __init__(self): + self.js_code = [] + + def visit_FunctionDef(self, node): + function_name = node.name + parameters = [arg.arg for arg in node.args.args] + param_list = ", ".join(parameters) + + self.js_code.append(f"function {function_name}({param_list}) {{") + self.generic_visit(node) + self.js_code.append("}\n") + + def visit_AsyncFunctionDef(self, node): + function_name = node.name + parameters = [arg.arg for arg in node.args.args] + param_list = ", ".join(parameters) + + self.js_code.append(f"async function {function_name}({param_list}) {{") + self.generic_visit(node) + self.js_code.append("}\n") + + def visit_Assign(self, node): + targets_code = " = ".join([ast.unparse(t) for t in node.targets]) + value_code = ast.unparse(node.value) + + if targets_code.startswith("(") and targets_code.endswith(")"): + targets_code = targets_code.replace("(", "[").replace(")", "]") + + self.js_code.append(f" {targets_code} = {value_code};") + + def visit_Return(self, node): + value_code = ast.unparse(node.value) if node.value else "" + self.js_code.append(f" return {value_code};") + + def visit_Expr(self, node): + if isinstance(node.value, ast.Call): + self.visit_Call(node.value) + else: + value_code = ast.unparse(node.value) if node.value else "" + value_code = value_code.replace("print", "console.log") + self.js_code.append(f" {value_code};") + + def visit_If(self, node): + test_code = ast.unparse(node.test) + self.js_code.append(f" if ({test_code}) {{") + self.generic_visit(node) + self.js_code.append(" }\n") + + def visit_For(self, node): + target_code = ast.unparse(node.target) + iter_code = ast.unparse(node.iter) + self.js_code.append(f" for ({target_code} of {iter_code}) {{") + self.generic_visit(node) + self.js_code.append(" }\n") + + def visit_While(self, node): + test_code = ast.unparse(node.test) + self.js_code.append(f" while ({test_code}) {{") + self.generic_visit(node) + self.js_code.append(" }\n") + + def visit_Call(self, node): + """This is a call node + Call node is e.g. print("Hello, World") + No assignment takes place here + """ + + if isinstance(node.func, ast.Name) and node.func.id == "print": + args_code = ", ".join([ast.unparse(arg) for arg in node.args]) + self.js_code.append(f" console.log({args_code});") + else: + func_code = ast.unparse(node.func) + args_code = ", ".join([ast.unparse(arg) for arg in node.args]) + self.js_code.append(f" {func_code}({args_code});") + + +def transpile(python_code: str) -> str: + tree = ast.parse(python_code) + transpiler = PythonToJsTranspiler() + transpiler.visit(tree) + return "".join(transpiler.js_code) + + +def transpile_to_js(python_code): + code = inspect.getsource(python_code) + return transpile(code) + + +def main(): + python_code = """ +def greet(name): + print("Hello, " + name) + +def add(a, b): + result = greet("World") + return result + +def subtract(a, b): + return add(a, -b) +""" + + js_code = transpile(python_code) + + # get ast of code + print(js_code) + + +if __name__ == "__main__": + main()