├── .env.development
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── build.yml
│ ├── lint.yml
│ └── python_tests.yml
├── .gitignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── SplootCode-screenshot-readme.png
├── babel.config.json
├── index.html
├── jest.config.ts
├── package.json
├── packages
├── components
│ ├── .gitignore
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── export_text_modal.tsx
│ │ ├── index.ts
│ │ ├── menu_bar.css
│ │ ├── menu_bar.tsx
│ │ ├── run_type_icon.tsx
│ │ └── save_project_modal.tsx
│ └── tsconfig.json
├── core
│ ├── .gitignore
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── code_io
│ │ │ ├── filesystem.ts
│ │ │ ├── filesystem_file_loader.ts
│ │ │ ├── local_storage_file_loader.ts
│ │ │ ├── local_storage_project_loader.ts
│ │ │ ├── starting_files.ts
│ │ │ └── static_file_loader.ts
│ │ ├── colors.ts
│ │ ├── feature_flags
│ │ │ └── feature_flags.ts
│ │ ├── http_types.ts
│ │ ├── index.ts
│ │ └── language
│ │ │ ├── annotations
│ │ │ └── annotations.ts
│ │ │ ├── autocomplete
│ │ │ ├── autocompleter.ts
│ │ │ ├── registry.ts
│ │ │ └── suggested_node.ts
│ │ │ ├── capture
│ │ │ └── runtime_capture.ts
│ │ │ ├── childset.ts
│ │ │ ├── fragment.ts
│ │ │ ├── fragment_adapter.ts
│ │ │ ├── invariants.ts
│ │ │ ├── mutations
│ │ │ ├── child_set_mutations.ts
│ │ │ ├── mutation_dispatcher.ts
│ │ │ ├── node_mutations.ts
│ │ │ ├── project_mutations.ts
│ │ │ └── scope_mutations.ts
│ │ │ ├── node.ts
│ │ │ ├── node_category_registry.ts
│ │ │ ├── observers.ts
│ │ │ ├── projects
│ │ │ ├── file.ts
│ │ │ ├── file_loader.ts
│ │ │ ├── package.ts
│ │ │ ├── project.ts
│ │ │ └── run_settings.ts
│ │ │ ├── tray
│ │ │ └── tray.ts
│ │ │ ├── type_registry.ts
│ │ │ └── validation
│ │ │ └── validation_watcher.ts
│ └── tsconfig.json
├── editor
│ ├── .gitignore
│ ├── jest.config.ts
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── components
│ │ │ ├── attached_child.css
│ │ │ ├── attached_child.tsx
│ │ │ ├── autosave_info.tsx
│ │ │ ├── config_panel.tsx
│ │ │ ├── cursor.css
│ │ │ ├── cursor.tsx
│ │ │ ├── drag_overlay.css
│ │ │ ├── drag_overlay.tsx
│ │ │ ├── edit_box.css
│ │ │ ├── edit_box.tsx
│ │ │ ├── editor.css
│ │ │ ├── editor.tsx
│ │ │ ├── editor_banner.tsx
│ │ │ ├── editor_side_menu.css
│ │ │ ├── editor_side_menu.tsx
│ │ │ ├── fragment.tsx
│ │ │ ├── insert_box.css
│ │ │ ├── insert_box.tsx
│ │ │ ├── list_block.css
│ │ │ ├── list_block.tsx
│ │ │ ├── node_block.css
│ │ │ ├── node_block.test.tsx
│ │ │ ├── node_block.tsx
│ │ │ ├── placeholder_label.css
│ │ │ ├── placeholder_label.tsx
│ │ │ ├── property.css
│ │ │ ├── property.tsx
│ │ │ ├── runtime_annotations.css
│ │ │ ├── runtime_annotations.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── string_literal.css
│ │ │ ├── string_literal.tsx
│ │ │ ├── string_node.css
│ │ │ ├── string_node.tsx
│ │ │ ├── test_request_panel.tsx
│ │ │ ├── token_list_block.tsx
│ │ │ ├── tray
│ │ │ │ ├── add_import_modal.tsx
│ │ │ │ ├── category.tsx
│ │ │ │ ├── entry.tsx
│ │ │ │ ├── imports_tray.tsx
│ │ │ │ ├── scope_tray.css
│ │ │ │ ├── scope_tray.tsx
│ │ │ │ ├── tray.css
│ │ │ │ └── tray.tsx
│ │ │ ├── tree_list_block.css
│ │ │ └── tree_list_block.tsx
│ │ ├── context
│ │ │ ├── autosave_watcher.ts
│ │ │ ├── cursor_map.ts
│ │ │ ├── edit_box.ts
│ │ │ ├── editor_context.ts
│ │ │ ├── insert_box.ts
│ │ │ ├── multiselect_deleter.ts
│ │ │ ├── multiselect_fragment_creator.ts
│ │ │ ├── multiselect_tree_walker.ts
│ │ │ ├── rendered_tree_iterator.ts
│ │ │ ├── runtime_context_manager.ts
│ │ │ ├── selection.ts
│ │ │ └── undoWatcher.ts
│ │ ├── editor_hosting_config.ts
│ │ ├── index.ts
│ │ ├── layout
│ │ │ ├── attach_right_layout_handler.ts
│ │ │ ├── before_stack_layout_handler.ts
│ │ │ ├── block_layout_handler.ts
│ │ │ ├── breadcrumbs_layout_handler.ts
│ │ │ ├── childset_layout_handler.ts
│ │ │ ├── layout_constants.ts
│ │ │ ├── rendered_childset_block.ts
│ │ │ ├── rendered_fragment.ts
│ │ │ ├── rendered_node.ts
│ │ │ ├── stack_layout_handler.ts
│ │ │ ├── token_layout_handler.ts
│ │ │ └── tree_layout_handler.ts
│ │ ├── runtime
│ │ │ ├── file_change_watcher.ts
│ │ │ ├── frame_state_manager.ts
│ │ │ ├── project_file_change_watcher.ts
│ │ │ ├── python_frame.css
│ │ │ ├── python_frame.tsx
│ │ │ ├── python_runtime_panel.tsx
│ │ │ ├── response_viewer.css
│ │ │ ├── response_viewer.tsx
│ │ │ ├── terminal.css
│ │ │ ├── wasm-tty
│ │ │ │ └── wasm-tty.ts
│ │ │ ├── web_runtime.css
│ │ │ └── web_runtime.tsx
│ │ └── tests
│ │ │ ├── check_observer_mapping.ts
│ │ │ ├── dict_multiselect.test.ts
│ │ │ ├── edit_actions.test.ts
│ │ │ ├── fragment_insert.test.ts
│ │ │ └── multiselect.test.ts
│ └── tsconfig.json
├── language-python
│ ├── .gitignore
│ ├── jest.config.ts
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── analyzer
│ │ │ └── python_analyzer.ts
│ │ ├── generated
│ │ │ └── .gitignore
│ │ ├── index.ts
│ │ ├── nodes
│ │ │ ├── declared_identifier.ts
│ │ │ ├── literals.ts
│ │ │ ├── python_argument.ts
│ │ │ ├── python_assignment.ts
│ │ │ ├── python_binary_operator.ts
│ │ │ ├── python_brackets.ts
│ │ │ ├── python_break.ts
│ │ │ ├── python_call_member.ts
│ │ │ ├── python_call_node.ts
│ │ │ ├── python_call_variable.ts
│ │ │ ├── python_comment.ts
│ │ │ ├── python_continue.ts
│ │ │ ├── python_decorator.ts
│ │ │ ├── python_dictionary.ts
│ │ │ ├── python_elif.ts
│ │ │ ├── python_else.ts
│ │ │ ├── python_expression.ts
│ │ │ ├── python_file.ts
│ │ │ ├── python_for.ts
│ │ │ ├── python_from_import.ts
│ │ │ ├── python_function.ts
│ │ │ ├── python_identifier.ts
│ │ │ ├── python_if.ts
│ │ │ ├── python_import.ts
│ │ │ ├── python_keyvalue.ts
│ │ │ ├── python_keyword_argument.ts
│ │ │ ├── python_list.ts
│ │ │ ├── python_member.ts
│ │ │ ├── python_module_identifier.ts
│ │ │ ├── python_node.ts
│ │ │ ├── python_return.ts
│ │ │ ├── python_set.ts
│ │ │ ├── python_slice.ts
│ │ │ ├── python_slice_range.ts
│ │ │ ├── python_statement.ts
│ │ │ ├── python_string.ts
│ │ │ ├── python_subscript.ts
│ │ │ ├── python_tuple.ts
│ │ │ ├── python_while.ts
│ │ │ ├── scope_argument_autocompleter.ts
│ │ │ ├── scope_autocompleter.ts
│ │ │ ├── scope_member_autocompleter.ts
│ │ │ ├── tests
│ │ │ │ ├── parsetree.test.ts
│ │ │ │ ├── serialization.test.ts
│ │ │ │ ├── single_examples.ts
│ │ │ │ ├── test_utils.ts
│ │ │ │ └── tree_walker.ts
│ │ │ ├── utils.ts
│ │ │ └── variable_reference.ts
│ │ ├── scope
│ │ │ ├── python.ts
│ │ │ ├── python_scope.ts
│ │ │ └── types.ts
│ │ ├── standard_lib_modules.json
│ │ ├── tests
│ │ │ ├── load_projects.test.ts
│ │ │ ├── test_data
│ │ │ │ ├── blank_main.json
│ │ │ │ ├── helloname_main.json
│ │ │ │ ├── missing_expressions_main.json
│ │ │ │ ├── missing_values_main.json
│ │ │ │ ├── secret_password_main.json
│ │ │ │ ├── temperature_conversion_main.json
│ │ │ │ └── test_collections_main.json
│ │ │ └── test_file_loader.ts
│ │ ├── tray
│ │ │ ├── language.test.ts
│ │ │ └── language.ts
│ │ └── type_loader.ts
│ ├── tray
│ │ ├── abc.json
│ │ ├── aifc.json
│ │ ├── argparse.json
│ │ ├── array.json
│ │ ├── ast.json
│ │ ├── asyncio.json
│ │ ├── atexit.json
│ │ ├── base64.json
│ │ ├── bdb.json
│ │ ├── binascii.json
│ │ ├── bisect.json
│ │ ├── builtins.json
│ │ ├── bz2.json
│ │ ├── cProfile.json
│ │ ├── calendar.json
│ │ ├── cgi.json
│ │ ├── cgitb.json
│ │ ├── chunk.json
│ │ ├── cmath.json
│ │ ├── cmd.json
│ │ ├── code.json
│ │ ├── codecs.json
│ │ ├── codeop.json
│ │ ├── collections.json
│ │ ├── colorsys.json
│ │ ├── compileall.json
│ │ ├── configparser.json
│ │ ├── contextlib.json
│ │ ├── contextvars.json
│ │ ├── copy.json
│ │ ├── copyreg.json
│ │ ├── crypt.json
│ │ ├── csv.json
│ │ ├── ctypes.json
│ │ ├── dataclasses.json
│ │ ├── datetime.json
│ │ ├── decimal.json
│ │ ├── difflib.json
│ │ ├── dis.json
│ │ ├── doctest.json
│ │ ├── email.json
│ │ ├── encodings.json
│ │ ├── enum.json
│ │ ├── errno.json
│ │ ├── faulthandler.json
│ │ ├── filecmp.json
│ │ ├── fileinput.json
│ │ ├── flask.json
│ │ ├── fnmatch.json
│ │ ├── fractions.json
│ │ ├── ftplib.json
│ │ ├── functools.json
│ │ ├── gc.json
│ │ ├── genericpath.json
│ │ ├── getopt.json
│ │ ├── getpass.json
│ │ ├── gettext.json
│ │ ├── glob.json
│ │ ├── graphlib.json
│ │ ├── gzip.json
│ │ ├── hashlib.json
│ │ ├── heapq.json
│ │ ├── hmac.json
│ │ ├── html.json
│ │ ├── http.json
│ │ ├── imaplib.json
│ │ ├── imghdr.json
│ │ ├── importlib.json
│ │ ├── inspect.json
│ │ ├── io.json
│ │ ├── ipaddress.json
│ │ ├── itertools.json
│ │ ├── json.json
│ │ ├── keyword.json
│ │ ├── linecache.json
│ │ ├── locale.json
│ │ ├── logging.json
│ │ ├── lzma.json
│ │ ├── mailbox.json
│ │ ├── mailcap.json
│ │ ├── marshal.json
│ │ ├── math.json
│ │ ├── mimetypes.json
│ │ ├── mmap.json
│ │ ├── modulefinder.json
│ │ ├── multiprocessing.json
│ │ ├── netrc.json
│ │ ├── nntplib.json
│ │ ├── ntpath.json
│ │ ├── nturl2path.json
│ │ ├── numbers.json
│ │ ├── numpy.json
│ │ ├── opcode.json
│ │ ├── operator.json
│ │ ├── optparse.json
│ │ ├── os.json
│ │ ├── pandas.json
│ │ ├── pathlib.json
│ │ ├── pdb.json
│ │ ├── pickle.json
│ │ ├── pickletools.json
│ │ ├── pipes.json
│ │ ├── pkgutil.json
│ │ ├── platform.json
│ │ ├── plistlib.json
│ │ ├── poplib.json
│ │ ├── posix.json
│ │ ├── posixpath.json
│ │ ├── pprint.json
│ │ ├── profile.json
│ │ ├── pstats.json
│ │ ├── py_compile.json
│ │ ├── pyclbr.json
│ │ ├── pydoc.json
│ │ ├── pydoc_data.json
│ │ ├── pyexpat.json
│ │ ├── queue.json
│ │ ├── quopri.json
│ │ ├── random.json
│ │ ├── re.json
│ │ ├── reprlib.json
│ │ ├── requests.json
│ │ ├── rlcompleter.json
│ │ ├── runpy.json
│ │ ├── sched.json
│ │ ├── secrets.json
│ │ ├── select.json
│ │ ├── selectors.json
│ │ ├── shelve.json
│ │ ├── shlex.json
│ │ ├── shutil.json
│ │ ├── signal.json
│ │ ├── site.json
│ │ ├── smtplib.json
│ │ ├── sndhdr.json
│ │ ├── socket.json
│ │ ├── socketserver.json
│ │ ├── sqlite3.json
│ │ ├── sre_compile.json
│ │ ├── sre_constants.json
│ │ ├── sre_parse.json
│ │ ├── ssl.json
│ │ ├── stat.json
│ │ ├── statistics.json
│ │ ├── streamlit.json
│ │ ├── string.json
│ │ ├── stringprep.json
│ │ ├── struct.json
│ │ ├── subprocess.json
│ │ ├── sunau.json
│ │ ├── symtable.json
│ │ ├── sys.json
│ │ ├── sysconfig.json
│ │ ├── tabnanny.json
│ │ ├── tarfile.json
│ │ ├── telnetlib.json
│ │ ├── tempfile.json
│ │ ├── textwrap.json
│ │ ├── this.json
│ │ ├── threading.json
│ │ ├── time.json
│ │ ├── timeit.json
│ │ ├── token.json
│ │ ├── tokenize.json
│ │ ├── trace.json
│ │ ├── traceback.json
│ │ ├── tracemalloc.json
│ │ ├── types.json
│ │ ├── typing.json
│ │ ├── unicodedata.json
│ │ ├── unittest.json
│ │ ├── urllib.json
│ │ ├── uu.json
│ │ ├── uuid.json
│ │ ├── warnings.json
│ │ ├── wave.json
│ │ ├── weakref.json
│ │ ├── webbrowser.json
│ │ ├── wsgiref.json
│ │ ├── xdrlib.json
│ │ ├── xml.json
│ │ ├── xmlrpc.json
│ │ ├── zipapp.json
│ │ ├── zipfile.json
│ │ ├── zipimport.json
│ │ ├── zlib.json
│ │ └── zoneinfo.json
│ └── tsconfig.json
├── language-web
│ ├── .gitignore
│ ├── code_io
│ │ ├── import_html.ts
│ │ └── import_js.ts
│ ├── components
│ │ └── datasheet
│ │ │ └── datasheet.tsx
│ ├── definitions
│ │ └── loader.ts
│ ├── index.ts
│ ├── javascript_node.ts
│ ├── package.json
│ ├── rollup.config.js
│ ├── tsconfig.json
│ ├── type_loader.ts
│ └── types
│ │ ├── component
│ │ ├── component_declaration.ts
│ │ ├── component_invocation.ts
│ │ ├── component_property.ts
│ │ ├── declared_property.ts
│ │ ├── for_each_expression.ts
│ │ ├── property_reference.ts
│ │ └── react_element.ts
│ │ ├── css
│ │ └── css_properties.ts
│ │ ├── dataset
│ │ ├── datasheet.ts
│ │ ├── field_declaration.ts
│ │ ├── row.ts
│ │ └── string_entry.ts
│ │ ├── html
│ │ ├── html_attribute.ts
│ │ ├── html_document.ts
│ │ ├── html_element.ts
│ │ ├── html_script_element.ts
│ │ ├── html_style_element.ts
│ │ └── tags.ts
│ │ ├── js
│ │ ├── assignment.ts
│ │ ├── async_function.ts
│ │ ├── await_expression.ts
│ │ ├── binary_operator.ts
│ │ ├── call_member.ts
│ │ ├── call_variable.ts
│ │ ├── declared_identifier.ts
│ │ ├── expression.ts
│ │ ├── functions.ts
│ │ ├── if.ts
│ │ ├── import.ts
│ │ ├── import_default.ts
│ │ ├── inline_function.ts
│ │ ├── javascript_file.ts
│ │ ├── list.ts
│ │ ├── literals.ts
│ │ ├── logical_expression.ts
│ │ ├── lookup_expression.ts
│ │ ├── member_expression.ts
│ │ ├── object_expression.ts
│ │ ├── object_property.ts
│ │ ├── return.ts
│ │ ├── variable_declaration.ts
│ │ └── variable_reference.ts
│ │ ├── jss_styles
│ │ ├── jss_class_block.ts
│ │ ├── jss_class_reference.ts
│ │ ├── jss_hover_block.ts
│ │ ├── jss_style_block.ts
│ │ └── jss_style_property.ts
│ │ └── styles
│ │ ├── style_property.ts
│ │ ├── style_rule.ts
│ │ └── style_selector.ts
├── runtime-python
│ ├── .gitignore
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── autocomplete.ts
│ │ ├── autocomplete_webworker.ts
│ │ ├── index.ts
│ │ ├── message_types.ts
│ │ ├── pyodide.ts
│ │ ├── pyright.ts
│ │ ├── runtime
│ │ │ ├── autocomplete-worker-manager.ts
│ │ │ ├── common.ts
│ │ │ ├── index.ts
│ │ │ └── worker-manager.ts
│ │ ├── static_urls.ts
│ │ └── webworker.ts
│ └── tsconfig.json
└── runtime-web
│ ├── .gitignore
│ ├── index.ts
│ ├── serviceworker.ts
│ └── tsconfig.json
├── public
└── static
│ ├── generated
│ └── .gitignore
│ └── projects
│ ├── blank
│ ├── main
│ │ ├── index.html.sp
│ │ └── package.sp
│ └── project.sp
│ ├── blankpython
│ ├── main
│ │ ├── main.py.sp
│ │ └── package.sp
│ └── project.sp
│ ├── bouncing
│ ├── main
│ │ ├── app.js.sp
│ │ ├── index.html.sp
│ │ └── package.sp
│ └── project.sp
│ ├── flashcards
│ ├── main
│ │ ├── app.js.sp
│ │ ├── index.html.sp
│ │ ├── package.sp
│ │ └── words.sheet.sp
│ └── project.sp
│ ├── gallery
│ ├── main
│ │ ├── gallery.js.sp
│ │ ├── index.html.sp
│ │ ├── package.sp
│ │ └── setupjss.js.sp
│ └── project.sp
│ ├── helloname
│ ├── main
│ │ ├── main.py.sp
│ │ └── package.sp
│ └── project.sp
│ ├── secret_password
│ ├── main
│ │ ├── main.py.sp
│ │ └── package.sp
│ └── project.sp
│ └── temperature_conversion
│ ├── main
│ ├── main.py.sp
│ └── package.sp
│ └── project.sp
├── python
├── .gitignore
├── convert_ast.py
├── executor.py
├── generate.sh
├── generate_builtins.py
├── generate_tray.py
├── library
│ ├── overrides.yaml
│ ├── sploot-node-docs.yaml
│ └── tray_categories.yaml
├── module_loader.py
├── packages
│ ├── requests-2.28.1-py3-none-any.whl
│ ├── requests-2.28.2-py3-none-any.whl
│ ├── stlite_pyarrow-0.1.0-py3-none-any.whl
│ └── streamlit-1.19.0-py2.py3-none-any.whl
├── requirements.txt
├── tests
│ ├── __init__.py
│ ├── test_annotation_limits_executor.py
│ ├── test_convert_ast.py
│ ├── test_execute.py
│ ├── test_loop_annotations.py
│ ├── test_module_loader.py
│ ├── test_text_generator.py
│ └── test_tray_generation.py
└── text_generator.py
├── reactpreview.config.js
├── scripts
├── dummy.ts
├── generateLibs.ts
└── pyright_tray_generate.ts
├── splootframepythonclient.html
├── splootstreamlitpythonclient.html
├── src-runtime
├── autocomplete_webworker.ts
├── index.ts
├── python.d.ts
├── runtime_webworker.ts
└── static_urls.ts
├── src
├── app.css
├── app.tsx
├── code_io
│ └── static_projects.ts
├── index.d.ts
├── index.tsx
├── module_loader.ts
├── pages
│ ├── project_editor.css
│ ├── project_editor.tsx
│ ├── python_editor.css
│ ├── python_editor_panels.tsx
│ └── user_home.tsx
├── providers.tsx
└── template.html
├── tsconfig.json
├── vite-runtime.config.ts
├── vite.config.ts
└── yarn.lock
/.env.development:
--------------------------------------------------------------------------------
1 | SPLOOT_FRAME_VIEW_DOMAIN=localhost:3001
2 | SPLOOT_FRAME_VIEW_SCHEME=http
3 | SPLOOT_EDITOR_DOMAIN=http://localhost:3000
4 | SPLOOT_RUNTIME_PYTHON_STATIC_FOLDER=/runtime-python/static
5 | SPLOOT_TYPESHED_PATH=/static/typeshed/
6 | SPLOOT_GOOGLE_ANALYTICS_ID
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | dist-runtime
4 | frame-dist
5 | lib
6 | out
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser", // Specifies the ESLint parser
4 | "parserOptions": {
5 | "ecmaVersion": 2020,
6 | "sourceType": "module",
7 | "ecmaFeatures": {
8 | "jsx": true
9 | }
10 | },
11 | "settings": {
12 | "react": {
13 | "version": "detect"
14 | }
15 | },
16 | "extends": [
17 | "plugin:react/recommended",
18 | "plugin:@typescript-eslint/recommended",
19 | "plugin:prettier/recommended"
20 | ],
21 | "plugins": ["unused-imports", "sort-imports-es6-autofix", "import"],
22 | "rules": {
23 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
24 | "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }],
25 | "@typescript-eslint/no-empty-interface": ["off"],
26 | "@typescript-eslint/no-explicit-any": ["off"],
27 | "@typescript-eslint/ban-ts-comment": ["off"],
28 | "@typescript-eslint/no-empty-function": ["off"],
29 | "import/no-duplicates": ["error"],
30 | "unused-imports/no-unused-imports": "error",
31 | "sort-imports-es6-autofix/sort-imports-es6": ["error", {
32 | "ignoreCase": false,
33 | "ignoreMemberSort": false,
34 | "memberSyntaxSortOrder": ["none", "all", "single", "multiple"]
35 | }]
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build-and-test:
13 | name: Build prod
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Check out Git repository
18 | uses: actions/checkout@v2
19 | - name: Setup python
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: '3.10'
23 | - name: Yarn install
24 | run: yarn
25 | - name: Run Python generation
26 | run: cd python && ./generate.sh && cd ..
27 | - name: Build packages
28 | run: yarn build:packages
29 | - name: Check packages
30 | run: yarn check:packages
31 | - name: Check types
32 | run: yarn tsc
33 | - name: Test
34 | run: yarn test
35 | - name: Build app
36 | run: yarn build:app
37 | - name: Build runtime
38 | run: yarn build:runtime
39 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | run-linters:
13 | name: Run linters
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Check out Git repository
18 | uses: actions/checkout@v2
19 | - name: Yarn install
20 | run: yarn
21 | - name: Lint
22 | run: yarn lint
23 |
--------------------------------------------------------------------------------
/.github/workflows/python_tests.yml:
--------------------------------------------------------------------------------
1 | name: Python tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | run-python-tests:
13 | name: Run tests
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Check out Git repository
18 | uses: actions/checkout@v2
19 | - name: Set up Python
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: '3.10'
23 | - name: Run tests
24 | run: cd python && pip install -r requirements.txt && python -m unittest
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 |
4 | # non-dev env files
5 | production.env
6 |
7 | # Firebase files
8 | .firebase
9 |
10 | # Terraform
11 | .terraform
12 |
13 | lib
14 |
15 | # Mac files
16 | .DS_Store
17 |
18 | # Logs
19 | logs
20 | *.log
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | lerna-debug.log*
25 |
26 | # Diagnostic reports (https://nodejs.org/api/report.html)
27 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
28 |
29 | # Runtime data
30 | pids
31 | *.pid
32 | *.seed
33 | *.pid.lock
34 |
35 | # Directory for instrumented libs generated by jscoverage/JSCover
36 | lib-cov
37 |
38 | # Coverage directory used by tools like istanbul
39 | coverage
40 | *.lcov
41 |
42 | # nyc test coverage
43 | .nyc_output
44 |
45 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
46 | .grunt
47 |
48 | # Bower dependency directory (https://bower.io/)
49 | bower_components
50 |
51 | # node-waf configuration
52 | .lock-wscript
53 |
54 | # Compiled binary addons (https://nodejs.org/api/addons.html)
55 | build/Release
56 |
57 | # Dependency directories
58 | node_modules/
59 | jspm_packages/
60 |
61 | # TypeScript v1 declaration files
62 | typings/
63 |
64 | # TypeScript cache
65 | *.tsbuildinfo
66 |
67 | # Optional npm cache directory
68 | .npm
69 |
70 | # Optional eslint cache
71 | .eslintcache
72 |
73 | # Microbundle cache
74 | .rpt2_cache/
75 | .rts2_cache_cjs/
76 | .rts2_cache_es/
77 | .rts2_cache_umd/
78 |
79 | # Optional REPL history
80 | .node_repl_history
81 |
82 | # Output of 'npm pack'
83 | *.tgz
84 |
85 | # Yarn Integrity file
86 | .yarn-integrity
87 |
88 | # parcel-bundler cache (https://parceljs.org/)
89 | .cache
90 |
91 | # Next.js build output
92 | .next
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 | dist-runtime
98 | frame-dist
99 |
100 | # Gatsby files
101 | .cache/
102 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
103 | # https://nextjs.org/blog/next-9-1#public-directory-support
104 | # public
105 |
106 | # vuepress build output
107 | .vuepress/dist
108 |
109 | # Serverless directories
110 | .serverless/
111 |
112 | # FuseBox cache
113 | .fusebox/
114 |
115 | # DynamoDB Local files
116 | .dynamodb/
117 |
118 | # TernJS port file
119 | .tern-port
120 |
121 | .rollup.cache
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "es5",
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "tabWidth": 2
7 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.formatting.provider": "black",
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": "explicit"
5 | },
6 | }
--------------------------------------------------------------------------------
/SplootCode-screenshot-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SplootCode/splootcode/d621bcd5f30df97150d3e1388f60307636568780/SplootCode-screenshot-readme.png
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
4 | ["@babel/plugin-proposal-class-properties", { "loose": false }]
5 | ]
6 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | SplootCode
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types'
2 |
3 | const config: Config.InitialOptions = {
4 | preset: 'ts-jest',
5 | verbose: true,
6 | testPathIgnorePatterns: ['node_modules', 'lib', 'dist', 'out'],
7 | moduleNameMapper: {
8 | '\\.(css|less)$': 'identity-obj-proxy',
9 | },
10 | }
11 | export default config
12 |
--------------------------------------------------------------------------------
/packages/components/.gitignore:
--------------------------------------------------------------------------------
1 | # non-dev env files
2 | production.env
3 |
4 | # Firebase files
5 | .firebase
6 |
7 | lib
8 |
9 | # Terraform
10 | .terraform
11 |
12 | # Lambda zips
13 |
14 | # Mac files
15 | .DS_Store
16 |
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | lerna-debug.log*
24 |
25 | # Diagnostic reports (https://nodejs.org/api/report.html)
26 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
27 |
28 | # Runtime data
29 | pids
30 | *.pid
31 | *.seed
32 | *.pid.lock
33 |
34 | # Directory for instrumented libs generated by jscoverage/JSCover
35 | lib-cov
36 |
37 | # Coverage directory used by tools like istanbul
38 | coverage
39 | *.lcov
40 |
41 | # nyc test coverage
42 | .nyc_output
43 |
44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
45 | .grunt
46 |
47 | # Bower dependency directory (https://bower.io/)
48 | bower_components
49 |
50 | # node-waf configuration
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 | build/Release
55 |
56 | # Dependency directories
57 | node_modules/
58 | jspm_packages/
59 |
60 | # TypeScript v1 declaration files
61 | typings/
62 |
63 | # TypeScript cache
64 | *.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 | .npm
68 |
69 | # Optional eslint cache
70 | .eslintcache
71 |
72 | # Microbundle cache
73 | .rpt2_cache/
74 | .rts2_cache_cjs/
75 | .rts2_cache_es/
76 | .rts2_cache_umd/
77 |
78 | # Optional REPL history
79 | .node_repl_history
80 |
81 | # Output of 'npm pack'
82 | *.tgz
83 |
84 | # Yarn Integrity file
85 | .yarn-integrity
86 |
87 | # parcel-bundler cache (https://parceljs.org/)
88 | .cache
89 |
90 | # Next.js build output
91 | .next
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 | out
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # Serverless directories
108 | .serverless/
109 |
110 | # FuseBox cache
111 | .fusebox/
112 |
113 | # DynamoDB Local files
114 | .dynamodb/
115 |
116 | # TernJS port file
117 | .tern-port
118 |
--------------------------------------------------------------------------------
/packages/components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@splootcode/components",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "styles": "./dist/index.css",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": "./dist/index.mjs",
12 | "require": "./dist/index.js"
13 | },
14 | "./styles.css": {
15 | "import": "./dist/index.css"
16 | }
17 | },
18 | "files": [
19 | "dist/index.css"
20 | ],
21 | "scripts": {
22 | "test": "echo 'No tests for components package.'",
23 | "build": "yarn rollup -c"
24 | },
25 | "private": true,
26 | "peerDependencies": {
27 | "@chakra-ui/icons": "^1.0.4",
28 | "@chakra-ui/react": "^1.6.9",
29 | "@emotion/react": "^11.1.4",
30 | "@emotion/styled": "^11.0.0",
31 | "react": "17.0.2",
32 | "react-router-dom": "^5.1.2"
33 | },
34 | "devDependencies": {
35 | "@rollup/plugin-node-resolve": "^15.0.2",
36 | "@rollup/plugin-typescript": "^11.1.0",
37 | "@types/react": "^17.0.2",
38 | "@types/react-router-dom": "^5.3.3",
39 | "react-icons": "^4.4.0",
40 | "rollup": "3.20.2",
41 | "rollup-plugin-dts": "^5.3.0",
42 | "rollup-plugin-import-css": "^3.2.1",
43 | "tslib": "^1.10.0",
44 | "typescript": "^4.8.4"
45 | },
46 | "dependencies": {}
47 | }
48 |
--------------------------------------------------------------------------------
/packages/components/rollup.config.js:
--------------------------------------------------------------------------------
1 | import css from 'rollup-plugin-import-css'
2 | import dts from 'rollup-plugin-dts'
3 | import typescript from '@rollup/plugin-typescript'
4 | import { nodeResolve } from '@rollup/plugin-node-resolve'
5 |
6 | export default [
7 | {
8 | external: ['react', '@emotion/core', '@emotion/styled', '@chakra-ui/react', '@chakra-ui/icons'],
9 | plugins: [typescript(), css(), nodeResolve({ resolveOnly: ['react-icons'] })],
10 | input: 'src/index.ts',
11 | output: [
12 | {
13 | file: `dist/index.mjs`,
14 | format: 'es',
15 | sourcemap: true,
16 | },
17 | {
18 | file: `dist/index.js`,
19 | format: 'cjs',
20 | sourcemap: true,
21 | },
22 | ],
23 | },
24 | {
25 | input: 'src/index.ts',
26 | output: [{ file: `dist/index.d.ts`, format: 'es' }],
27 | plugins: [dts(), css()],
28 | },
29 | ]
30 |
--------------------------------------------------------------------------------
/packages/components/src/index.ts:
--------------------------------------------------------------------------------
1 | export { MenuBar, MenuBarItem } from './menu_bar'
2 | export { SaveProjectModal } from './save_project_modal'
3 | export type { MainMenuItem, MenuBarProps } from './menu_bar'
4 | export { RunTypeIcon } from './run_type_icon'
5 | export { ExportTextModal } from './export_text_modal'
6 |
--------------------------------------------------------------------------------
/packages/components/src/menu_bar.css:
--------------------------------------------------------------------------------
1 | #menubar {
2 | display: flex;
3 | justify-content: space-between;
4 | height: 36px;
5 | padding: 0px 4px;
6 | background-color: var(--chakra-colors-gray-800);
7 | box-sizing: border-box;
8 | border-bottom: 1px solid var(--chakra-colors-gray-900);
9 | }
10 |
11 | .menubar-left {
12 | display: inline-block;
13 | }
14 |
15 | .menubar-center {
16 | display: flex;
17 | align-items: center;
18 | gap: 8px;
19 | }
20 |
21 | .menubar-item {
22 | display: inline-block;
23 | min-width: 60px;
24 | }
25 |
26 | .menubar-right {
27 | display: flex;
28 | align-items: center;
29 | }
30 |
31 | .menubar-separator {
32 | background-color: var(--chakra-colors-green-500);
33 | height: 16px;
34 | width: 1px;
35 | display: inline-block;
36 | }
--------------------------------------------------------------------------------
/packages/components/src/menu_bar.tsx:
--------------------------------------------------------------------------------
1 | import './menu_bar.css'
2 | import React from 'react'
3 | import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react'
4 | import { HamburgerIcon } from '@chakra-ui/icons'
5 | import { Link } from 'react-router-dom'
6 |
7 | export interface MainMenuItem {
8 | name: string
9 | disabled?: boolean
10 | onClick: () => void
11 | }
12 |
13 | export interface MenuBarProps {
14 | children?: React.ReactNode
15 | menuItems: MainMenuItem[]
16 | }
17 |
18 | export const MenuBarItem = ({ children }: { children: React.ReactNode }): React.ReactElement => {
19 | return {children}
20 | }
21 |
22 | export const MenuBar = ({ children, menuItems }: MenuBarProps): React.ReactElement => {
23 | return (
24 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/packages/components/src/run_type_icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AiOutlineClockCircle } from 'react-icons/ai'
3 | import { BiGlobe } from 'react-icons/bi'
4 | import { FaStream, FaTerminal } from 'react-icons/fa'
5 | import { Icon } from '@chakra-ui/react'
6 | import { RunType } from '@splootcode/core'
7 |
8 | export function RunTypeIcon(props: { runType: RunType }) {
9 | switch (props.runType) {
10 | case RunType.COMMAND_LINE:
11 | return
12 | case RunType.HTTP_REQUEST:
13 | return
14 | case RunType.SCHEDULE:
15 | return
16 | case RunType.STREAMLIT:
17 | return
18 | default:
19 | return
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/components/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "lib": ["DOM", "ES2020"],
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "allowSyntheticDefaultImports": true,
11 | "experimentalDecorators": true,
12 | "resolveJsonModule": true,
13 | "skipDefaultLibCheck": true,
14 | "downlevelIteration": true,
15 | "noEmit": true,
16 | "baseUrl": ".",
17 | },
18 | "include": ["src"],
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | "out",
23 | "*.config.*"
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/.gitignore:
--------------------------------------------------------------------------------
1 | # non-dev env files
2 | production.env
3 |
4 | # Firebase files
5 | .firebase
6 |
7 | lib
8 |
9 | # Terraform
10 | .terraform
11 |
12 | # Lambda zips
13 |
14 | # Mac files
15 | .DS_Store
16 |
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | lerna-debug.log*
24 |
25 | # Diagnostic reports (https://nodejs.org/api/report.html)
26 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
27 |
28 | # Runtime data
29 | pids
30 | *.pid
31 | *.seed
32 | *.pid.lock
33 |
34 | # Directory for instrumented libs generated by jscoverage/JSCover
35 | lib-cov
36 |
37 | # Coverage directory used by tools like istanbul
38 | coverage
39 | *.lcov
40 |
41 | # nyc test coverage
42 | .nyc_output
43 |
44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
45 | .grunt
46 |
47 | # Bower dependency directory (https://bower.io/)
48 | bower_components
49 |
50 | # node-waf configuration
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 | build/Release
55 |
56 | # Dependency directories
57 | node_modules/
58 | jspm_packages/
59 |
60 | # TypeScript v1 declaration files
61 | typings/
62 |
63 | # TypeScript cache
64 | *.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 | .npm
68 |
69 | # Optional eslint cache
70 | .eslintcache
71 |
72 | # Microbundle cache
73 | .rpt2_cache/
74 | .rts2_cache_cjs/
75 | .rts2_cache_es/
76 | .rts2_cache_umd/
77 |
78 | # Optional REPL history
79 | .node_repl_history
80 |
81 | # Output of 'npm pack'
82 | *.tgz
83 |
84 | # Yarn Integrity file
85 | .yarn-integrity
86 |
87 | # parcel-bundler cache (https://parceljs.org/)
88 | .cache
89 |
90 | # Next.js build output
91 | .next
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 | out
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # Serverless directories
108 | .serverless/
109 |
110 | # FuseBox cache
111 | .fusebox/
112 |
113 | # DynamoDB Local files
114 | .dynamodb/
115 |
116 | # TernJS port file
117 | .tern-port
118 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@splootcode/core",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "main": "dist/index.js",
6 | "module": "dist/index.mjs",
7 | "typings": "dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "import": "./dist/index.mjs",
11 | "require": "./dist/index.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "private": true,
16 | "scripts": {
17 | "test": "echo 'No tests for core package.'",
18 | "build": "yarn rollup -c"
19 | },
20 | "devDependencies": {
21 | "@rollup/plugin-node-resolve": "^15.0.2",
22 | "@rollup/plugin-typescript": "^11.1.0",
23 | "@types/wicg-file-system-access": "^2020.9.1",
24 | "rollup": "3.20.2",
25 | "rollup-plugin-dts": "^5.3.0",
26 | "typescript": "^4.8.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core/rollup.config.js:
--------------------------------------------------------------------------------
1 | import dts from 'rollup-plugin-dts'
2 | import typescript from '@rollup/plugin-typescript'
3 |
4 | export default [
5 | {
6 | plugins: [typescript()],
7 | input: 'src/index.ts',
8 | output: [
9 | {
10 | file: `dist/index.mjs`,
11 | format: 'es',
12 | sourcemap: true,
13 | },
14 | {
15 | file: `dist/index.js`,
16 | format: 'cjs',
17 | sourcemap: true,
18 | },
19 | ],
20 | },
21 | {
22 | input: 'src/index.ts',
23 | output: [{ file: `dist/index.d.ts`, format: 'es' }],
24 | plugins: [dts()],
25 | },
26 | ]
27 |
--------------------------------------------------------------------------------
/packages/core/src/code_io/filesystem.ts:
--------------------------------------------------------------------------------
1 | import { FileSystemFileLoader } from './filesystem_file_loader'
2 | import { LocalStorageProjectLoader } from './local_storage_project_loader'
3 | import { Project, SerializedProject } from '../language/projects/project'
4 | import { SplootPackage } from '../language/projects/package'
5 |
6 | async function savePackage(directoryHandle: FileSystemDirectoryHandle, project: Project, pack: SplootPackage) {
7 | const packDir = await directoryHandle.getDirectoryHandle(pack.name, { create: true })
8 | const packFile = await packDir.getFileHandle('package.sp', { create: true })
9 | // @ts-ignore
10 | const writable = await packFile.createWritable()
11 | await writable.write(pack.serialize())
12 | await writable.close()
13 | // Save each file
14 | const promises = pack.fileOrder.map(async (filename) => {
15 | const fileHandle = await packDir.getFileHandle(filename + '.sp', { create: true })
16 | // @ts-ignore
17 | const writable = await fileHandle.createWritable()
18 | await writable.write(pack.files[filename].serialize())
19 | await writable.close()
20 | })
21 | await Promise.all(promises)
22 | }
23 |
24 | export async function exportProjectToFolder(directoryHandle: FileSystemDirectoryHandle, project: Project) {
25 | // Write project file
26 | const fileHandle = await directoryHandle.getFileHandle('project.sp', { create: true })
27 | // @ts-ignore
28 | const writable = await fileHandle.createWritable()
29 | await writable.write(project.serialize())
30 | await writable.close()
31 | // Save each package
32 | project.packages.forEach((pack) => {
33 | savePackage(directoryHandle, project, pack)
34 | })
35 | }
36 |
37 | export async function loadProjectFromFolder(directoryHandle: FileSystemDirectoryHandle): Promise {
38 | const fileLoader = new FileSystemFileLoader(directoryHandle)
39 | const projStr = await (await (await directoryHandle.getFileHandle('project.sp')).getFile()).text()
40 | const proj = JSON.parse(projStr) as SerializedProject
41 | const packages = proj.packages.map(async (packRef) => {
42 | return fileLoader.loadPackage('unknown', proj.name, packRef.name)
43 | })
44 |
45 | return new Project('local', proj, await Promise.all(packages), fileLoader, new LocalStorageProjectLoader())
46 | }
47 |
--------------------------------------------------------------------------------
/packages/core/src/code_io/filesystem_file_loader.ts:
--------------------------------------------------------------------------------
1 | import { FileLoader, SaveError } from '../language/projects/file_loader'
2 | import { SerializedNode, deserializeNode } from '../language/type_registry'
3 | import { SerializedSplootPackage, SplootPackage } from '../language/projects/package'
4 | import { SplootFile } from '../language/projects/file'
5 | import { SplootNode } from '../language/node'
6 |
7 | export class FileSystemFileLoader implements FileLoader {
8 | directoryHandle: FileSystemDirectoryHandle
9 |
10 | constructor(directoryHandle: FileSystemDirectoryHandle) {
11 | this.directoryHandle = directoryHandle
12 | }
13 |
14 | isReadOnly() {
15 | return true
16 | }
17 |
18 | async loadPackage(ownerID: string, projectId: string, packageId: string): Promise {
19 | const packDirHandle = await this.directoryHandle.getDirectoryHandle(packageId)
20 | const packStr = await (await (await packDirHandle.getFileHandle('package.sp')).getFile()).text()
21 | const pack = JSON.parse(packStr) as SerializedSplootPackage
22 | return new SplootPackage(ownerID, projectId, pack)
23 | }
24 |
25 | async loadFile(ownerID: string, projectId: string, packageId: string, filename: string): Promise {
26 | const packDirHandle = await this.directoryHandle.getDirectoryHandle(packageId)
27 | const fileStr = await (await (await packDirHandle.getFileHandle(filename + '.sp')).getFile()).text()
28 | const serNode = JSON.parse(fileStr) as SerializedNode
29 | const rootNode = deserializeNode(serNode)
30 | return rootNode
31 | }
32 |
33 | async saveFile(ownerID: string, projectId: string, packageId: string, file: SplootFile): Promise {
34 | throw new SaveError('Cannot save readonly file.')
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/core/src/code_io/local_storage_file_loader.ts:
--------------------------------------------------------------------------------
1 | import { FileLoader } from '../language/projects/file_loader'
2 | import { LocalStorageProjectLoader } from './local_storage_project_loader'
3 | import { SerializedNode, deserializeNode } from '../language/type_registry'
4 | import { SerializedSplootPackage, SplootPackage } from '../language/projects/package'
5 | import { SplootFile } from '../language/projects/file'
6 | import { SplootNode } from '../language/node'
7 |
8 | export class LocalStorageFileLoader implements FileLoader {
9 | projectLoader: LocalStorageProjectLoader
10 |
11 | constructor(projectLoader: LocalStorageProjectLoader) {
12 | this.projectLoader = projectLoader
13 | }
14 |
15 | isReadOnly() {
16 | return false
17 | }
18 |
19 | getProjKey(ownerId: string, projectId: string) {
20 | if (ownerId === 'local') {
21 | return `project/${projectId}`
22 | }
23 | return `project/${ownerId}/${projectId}`
24 | }
25 |
26 | async loadPackage(ownerID: string, projectID: string, packageID: string): Promise {
27 | const packageKey = `${this.getProjKey(ownerID, projectID)}/${packageID}`
28 | const packStr = window.localStorage.getItem(packageKey)
29 | const pack = JSON.parse(packStr) as SerializedSplootPackage
30 | return new SplootPackage(ownerID, projectID, pack)
31 | }
32 |
33 | async loadFile(ownerID: string, projectID: string, packageID: string, filename: string): Promise {
34 | const fileKey = `${this.getProjKey(ownerID, projectID)}/${packageID}/${filename}`
35 | const fileStr = window.localStorage.getItem(fileKey)
36 | const serNode = JSON.parse(fileStr) as SerializedNode
37 | const rootNode = deserializeNode(serNode)
38 | return rootNode
39 | }
40 |
41 | async saveFile(
42 | ownerID: string,
43 | projectID: string,
44 | packageID: string,
45 | file: SplootFile,
46 | base_version: string
47 | ): Promise {
48 | const fileKey = `${this.getProjKey(ownerID, projectID)}/${packageID}/${file.name}`
49 | window.localStorage.setItem(fileKey, file.serialize())
50 | // Randomly generate new version
51 | const newVersion = (Math.random() + 1).toString(36)
52 | return newVersion
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/core/src/code_io/static_file_loader.ts:
--------------------------------------------------------------------------------
1 | import { FileLoader, SaveError } from '../language/projects/file_loader'
2 | import { SerializedNode, deserializeNode } from '../language/type_registry'
3 | import { SerializedSplootPackage, SplootPackage } from '../language/projects/package'
4 | import { SplootFile } from '../language/projects/file'
5 | import { SplootNode } from '../language/node'
6 |
7 | export class StaticFileLoader implements FileLoader {
8 | rootProjectUrl: string
9 |
10 | constructor(rootProjectUrl: string) {
11 | if (!rootProjectUrl.endsWith('/')) {
12 | rootProjectUrl += '/'
13 | }
14 | this.rootProjectUrl = rootProjectUrl
15 | }
16 |
17 | isReadOnly() {
18 | return true
19 | }
20 |
21 | async loadPackage(ownerID: string, projectId: string, packageId: string): Promise {
22 | const packStr = await (await fetch(this.rootProjectUrl + packageId + '/package.sp')).text()
23 | const pack = JSON.parse(packStr) as SerializedSplootPackage
24 | return new SplootPackage(ownerID, projectId, pack)
25 | }
26 |
27 | async loadFile(ownerID: string, projectId: string, packageId: string, filename: string): Promise {
28 | const fileStr = await (await fetch(this.rootProjectUrl + packageId + '/' + filename + '.sp')).text()
29 | const serNode = JSON.parse(fileStr) as SerializedNode
30 | const rootNode = deserializeNode(serNode)
31 | return rootNode
32 | }
33 |
34 | async saveFile(ownerID: string, projectId: string, packageId: string, file: SplootFile): Promise {
35 | throw new SaveError('Cannot save readonly file.')
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/core/src/colors.ts:
--------------------------------------------------------------------------------
1 | export enum HighlightColorCategory {
2 | NONE = 0,
3 | FUNCTION,
4 | FUNCTION_DEFINITION,
5 | VARIABLE,
6 | VARIABLE_DECLARATION,
7 | LITERAL_NUMBER,
8 | LITERAL_STRING,
9 | LITERAL_LIST,
10 | CONTROL,
11 | KEYWORD,
12 | OPERATOR,
13 | HTML_ELEMENT,
14 | HTML_ATTRIBUTE,
15 | STYLE_RULE,
16 | STYLE_PROPERTY,
17 | COMMENT,
18 | }
19 |
20 | export enum ColorUsageType {
21 | NODE_TEXT = 0,
22 | CAP_TEXT,
23 | NODE_FILL,
24 | CAP_FILL,
25 | }
26 |
27 | function colorBase(category: HighlightColorCategory): string {
28 | switch (category) {
29 | case HighlightColorCategory.FUNCTION:
30 | return 'code-yellow'
31 | case HighlightColorCategory.FUNCTION_DEFINITION:
32 | return 'code-purple'
33 | case HighlightColorCategory.VARIABLE_DECLARATION:
34 | return 'code-purple'
35 | case HighlightColorCategory.VARIABLE:
36 | return 'code-lightblue'
37 | case HighlightColorCategory.LITERAL_NUMBER:
38 | return 'code-blue'
39 | case HighlightColorCategory.LITERAL_STRING:
40 | return 'code-neutral'
41 | case HighlightColorCategory.LITERAL_LIST:
42 | return 'code-purple'
43 | case HighlightColorCategory.KEYWORD:
44 | return 'code-purple'
45 | case HighlightColorCategory.CONTROL:
46 | return 'code-purple'
47 | case HighlightColorCategory.COMMENT:
48 | return 'code-comment'
49 | }
50 | return 'code-neutral'
51 | }
52 |
53 | export function getColor(category: HighlightColorCategory, usage: ColorUsageType): string {
54 | // Hardcode for now, but could be configurable in the future
55 | const base = colorBase(category)
56 | if (usage == ColorUsageType.CAP_TEXT) {
57 | return `var(--${base}-cap-text)`
58 | }
59 | if (usage === ColorUsageType.NODE_FILL) {
60 | return `var(--editor-node-block-fill)`
61 | }
62 | if (usage === ColorUsageType.CAP_FILL) {
63 | return `var(--editor-node-block-fill)`
64 | }
65 | return `var(--${base}-text)`
66 | }
67 |
--------------------------------------------------------------------------------
/packages/core/src/feature_flags/feature_flags.ts:
--------------------------------------------------------------------------------
1 | export const ENABLE_DEPLOYMENTS_FLAG = 'ENABLE_DEPLOYMENTS'
2 | export const ENABLE_HTTP_APPS_FLAG = 'ENABLE_HTTP_APPS_FLAG'
3 | export const ENABLE_STREAMLIT_APPS_FLAG = 'ENABLE_STREAMLIT_APPS_FLAG'
4 | export const ENABLE_INSTALLABLE_PACKAGES_FLAG = 'ENABLE_INSTALLABLE_PACKAGES_FLAG'
5 | export const ENABLE_TUTORIALS_FLAG = 'ENABLE_TUTORIALS_FLAG'
6 |
7 | const supportedFlags = new Map([
8 | [ENABLE_DEPLOYMENTS_FLAG, true],
9 | [ENABLE_HTTP_APPS_FLAG, false],
10 | [ENABLE_STREAMLIT_APPS_FLAG, false],
11 | [ENABLE_INSTALLABLE_PACKAGES_FLAG, false],
12 | [ENABLE_TUTORIALS_FLAG, false],
13 | ])
14 |
15 | /*
16 | To enable for yourself in local storage:
17 | localStorage.setItem('SPLOOT_FEATURE_FLAGS', JSON.stringify({'ENABLE_INSTALLABLE_PACKAGES_FLAG': true, 'ENABLE_HTTP_APPS_FLAG': false, 'ENABLE_STREAMLIT_APPS_FLAG': false}))
18 | */
19 |
20 | export function loadFeatureFlags(): Map {
21 | const flags = new Map(supportedFlags.entries())
22 | const serializedFlags = localStorage.getItem('SPLOOT_FEATURE_FLAGS')
23 | if (serializedFlags) {
24 | const obj = JSON.parse(serializedFlags) as Record
25 | for (const key of Object.keys(obj)) {
26 | // Skip any flags that we don't use anymore.
27 | if (flags.has(key)) {
28 | flags.set(key, obj[key])
29 | }
30 | }
31 | }
32 | return flags
33 | }
34 |
--------------------------------------------------------------------------------
/packages/core/src/http_types.ts:
--------------------------------------------------------------------------------
1 | export interface HTTPScenario {
2 | id?: number
3 | name: string
4 | method: string
5 | path: string
6 | protocol: string
7 | rawQueryString: string
8 | headers: Record
9 | body: string
10 | isBase64Encoded: boolean
11 | }
12 |
13 | export interface HTTPRequestAWSEvent {
14 | version: string
15 | rawPath: string
16 | rawQueryString: string
17 | cookies: string[]
18 | headers: Record
19 | requestContext: RequestContextEvent
20 | body: string
21 | isBase64Encoded: boolean
22 | }
23 |
24 | export interface RequestContextEvent {
25 | http: HTTPEvent
26 | }
27 |
28 | export interface HTTPEvent {
29 | method: string
30 | path: string
31 | protocol: string
32 | sourceIp: string
33 | userAgent: string
34 | }
35 |
36 | export interface HTTP {
37 | method: string
38 | path: string
39 | protocol: string
40 | }
41 |
42 | export interface HTTPResponse {
43 | statusCode: number
44 | headers: Record
45 | body: string
46 | isBase64Encoded: boolean
47 | }
48 |
49 | export function httpScenarioToHTTPRequestEvent(scenario: HTTPScenario): HTTPRequestAWSEvent {
50 | const sourceIP = ''
51 | let userAgent = ''
52 | let cookies = []
53 |
54 | const loweredHeaders = Object.fromEntries(
55 | Object.entries(scenario.headers).map(([key, value]) => [key.toLowerCase(), value])
56 | )
57 |
58 | if (loweredHeaders['cookie']) {
59 | cookies = loweredHeaders['cookie'].split(';')
60 | }
61 |
62 | if (loweredHeaders['user-agent']) {
63 | userAgent = loweredHeaders['user-agent']
64 | }
65 |
66 | return {
67 | version: '2.0',
68 | rawPath: scenario.path,
69 | rawQueryString: scenario.rawQueryString,
70 | cookies: cookies,
71 | headers: scenario.headers,
72 | requestContext: {
73 | http: {
74 | method: scenario.method,
75 | path: scenario.path,
76 | protocol: scenario.protocol,
77 | sourceIp: sourceIP,
78 | userAgent: userAgent,
79 | },
80 | },
81 | body: scenario.body,
82 | isBase64Encoded: scenario.isBase64Encoded,
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/core/src/language/annotations/annotations.ts:
--------------------------------------------------------------------------------
1 | import { StatementCapture } from '../capture/runtime_capture'
2 |
3 | export enum NodeAnnotationType {
4 | ParseError = 1,
5 | RuntimeError,
6 | Assignment,
7 | ReturnValue,
8 | SideEffect,
9 | LoopIterations,
10 | }
11 |
12 | export type NodeAnnotation = {
13 | type: NodeAnnotationType
14 | value:
15 | | RuntimeErrorAnnotation
16 | | AssignmentAnnotation
17 | | SideEffectAnnotation
18 | | ReturnValueAnnotation
19 | | ParseErrorAnnotation
20 | }
21 |
22 | export type ParseErrorAnnotation = {
23 | message: string
24 | blameChild: number
25 | }
26 |
27 | export type LoopAnnotation = {
28 | label: string
29 | iterations: number
30 | currentFrame: number
31 | }
32 |
33 | export type RuntimeErrorAnnotation = {
34 | errorType: string
35 | errorMessage: string
36 | }
37 |
38 | export type AssignmentAnnotation = {
39 | variableName: string
40 | value: string
41 | type: string
42 | }
43 |
44 | export type SideEffectAnnotation = {
45 | message: string
46 | }
47 |
48 | export type ReturnValueAnnotation = {
49 | type: string
50 | value: string
51 | }
52 |
53 | export function getSideEffectAnnotations(capture: StatementCapture): NodeAnnotation[] {
54 | if (!capture.sideEffects || capture.sideEffects.length === 0) {
55 | return []
56 | }
57 | const annotations: NodeAnnotation[] = []
58 | let stdout = capture.sideEffects
59 | .filter((sideEffect) => sideEffect.type === 'stdout')
60 | .map((sideEffect) => sideEffect.value)
61 | .join('')
62 |
63 | if (stdout.length > 20) {
64 | stdout = stdout.substring(0, 17) + '...'
65 | }
66 | return annotations
67 | }
68 |
--------------------------------------------------------------------------------
/packages/core/src/language/autocomplete/autocompleter.ts:
--------------------------------------------------------------------------------
1 | import { ParentReference } from '../node'
2 | import { SuggestedNode } from './suggested_node'
3 | import { SuggestionGenerator } from '../node_category_registry'
4 |
5 | export class Autocompleter {
6 | constants: SuggestedNode[]
7 | staticFunctions: SuggestionGenerator[]
8 | dynamicFunctions: SuggestionGenerator[]
9 | prefixOverrides: Map
10 |
11 | constructor(
12 | contants: SuggestedNode[],
13 | staticFunctions: SuggestionGenerator[],
14 | dynamicFunctions: SuggestionGenerator[],
15 | prefixOverrides: Map
16 | ) {
17 | this.constants = contants
18 | this.staticFunctions = staticFunctions
19 | this.dynamicFunctions = dynamicFunctions
20 | this.prefixOverrides = prefixOverrides
21 | }
22 |
23 | getStaticSuggestions(parent: ParentReference, index: number): SuggestedNode[] {
24 | let suggestions: SuggestedNode[] = this.constants
25 | this.staticFunctions.forEach((generator: SuggestionGenerator) => {
26 | suggestions = suggestions.concat(generator.staticSuggestions(parent, index))
27 | })
28 | return suggestions
29 | }
30 |
31 | async getDynamicSuggestions(parent: ParentReference, index: number, userInput: string): Promise {
32 | const suggestionSets = await Promise.all(
33 | this.dynamicFunctions.map(async (generator: SuggestionGenerator) => {
34 | return generator.dynamicSuggestions(parent, index, userInput)
35 | })
36 | )
37 |
38 | return suggestionSets.flat()
39 | }
40 |
41 | async getPrefixSuggestions(parent: ParentReference, index: number, userInput: string): Promise {
42 | if (this.prefixOverrides.has(userInput[0])) {
43 | const generators = this.prefixOverrides.get(userInput[0])
44 |
45 | const suggestionSets = await Promise.all(
46 | generators.map(async (generator: SuggestionGenerator) => {
47 | return generator.dynamicSuggestions(parent, index, userInput)
48 | })
49 | )
50 |
51 | return suggestionSets.flat()
52 | }
53 | return null
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/core/src/language/autocomplete/suggested_node.ts:
--------------------------------------------------------------------------------
1 | import { ChildSet } from '../childset'
2 | import { SplootNode } from '../node'
3 |
4 | export class SuggestedNode {
5 | node: SplootNode
6 | key: string
7 | exactMatch: string
8 | searchTerms: string
9 | valid: boolean
10 | description: string
11 | wrapChildSetId: string
12 | insertPreviousChildSetId: string
13 | overrideLocationChildSet: ChildSet
14 | overrideLocationIndex: number
15 |
16 | constructor(
17 | node: SplootNode,
18 | key: string,
19 | searchTerms: string,
20 | valid: boolean,
21 | description = '',
22 | wrapChildSetId: string = null
23 | ) {
24 | this.node = node
25 | this.key = `${node.type}_${key}`
26 | this.exactMatch = key
27 | this.searchTerms = searchTerms
28 | this.description = description
29 | this.valid = valid
30 | this.wrapChildSetId = wrapChildSetId
31 | }
32 |
33 | isExactMatch(text: string) {
34 | // Allow exact matches to be case insensitive
35 | return text.toLowerCase() === this.exactMatch.toLowerCase()
36 | }
37 |
38 | isPrefixMatch(text: string) {
39 | // But prefixes only come into play when there's conflicting suggestions
40 | // e.g. "is" and "is not", "else" and "else if" so we'll be a little stricter
41 | // so we don't unnecessarily reject a match.
42 | return this.exactMatch.startsWith(text)
43 | }
44 |
45 | hasOverrideLocation(): boolean {
46 | return !!this.overrideLocationChildSet
47 | }
48 |
49 | setOverrideLocation(childSet: ChildSet, index: number, wrapChildSetId: string = null) {
50 | this.overrideLocationChildSet = childSet
51 | this.overrideLocationIndex = index
52 | this.wrapChildSetId = wrapChildSetId
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/core/src/language/capture/runtime_capture.ts:
--------------------------------------------------------------------------------
1 | export interface SideEffect {
2 | type: string
3 | value: string
4 | }
5 |
6 | export interface WhileLoopData {
7 | frames?: StatementCapture[]
8 | }
9 |
10 | export interface WhileLoopIteration {
11 | condition: StatementCapture[]
12 | block?: StatementCapture[]
13 | }
14 |
15 | export interface ForLoopData {
16 | frames?: StatementCapture[]
17 | }
18 |
19 | export interface ForLoopIteration {
20 | iterable: StatementCapture[]
21 | block?: StatementCapture[]
22 | }
23 |
24 | export interface IfStatementData {
25 | condition: StatementCapture[]
26 | trueblock?: StatementCapture[]
27 | elseblocks?: StatementCapture[]
28 | }
29 |
30 | export interface ImportStatementData {
31 | import: StatementCapture[]
32 | }
33 |
34 | export interface ElseIfStatementData {
35 | condition: StatementCapture[]
36 | block: StatementCapture[]
37 | }
38 |
39 | export interface ElseStatementData {
40 | block: StatementCapture[]
41 | }
42 |
43 | export interface PythonFileData {
44 | body?: StatementCapture[]
45 | }
46 |
47 | export interface FunctionDeclarationData {
48 | count: number
49 | calls: StatementCapture[]
50 | exception: {
51 | frameno: number
52 | lineno: number
53 | message: string
54 | type: string
55 | }
56 | }
57 |
58 | export interface FunctionCallData {
59 | body: StatementCapture[]
60 | }
61 |
62 | export interface SingleStatementData {
63 | result: string
64 | resultType: string
65 | }
66 |
67 | export interface StatementCapture {
68 | type: string
69 | data?:
70 | | PythonFileData
71 | | WhileLoopData
72 | | WhileLoopIteration
73 | | IfStatementData
74 | | SingleStatementData
75 | | ElseStatementData
76 | | ImportStatementData
77 | | FunctionDeclarationData
78 | | FunctionCallData
79 | sideEffects?: SideEffect[]
80 | exceptionType?: string
81 | exceptionMessage?: string
82 | exceptionInFunction?: string
83 | }
84 |
85 | export interface CapturePayload {
86 | root: StatementCapture
87 | detached: { [key: string]: { count: number; frames: StatementCapture[] } }
88 | lastException?: {
89 | func_id: string
90 | frameno: number
91 | lineno: number
92 | message: string
93 | type: string
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/core/src/language/fragment.ts:
--------------------------------------------------------------------------------
1 | import { InvariantViolationError, InvariantViolationType } from './invariants'
2 | import { NodeCategory, isNodeInCategory } from './node_category_registry'
3 | import { SerializedNode, deserializeNode } from './type_registry'
4 | import { SplootNode } from './node'
5 |
6 | export interface SerializedFragment {
7 | category: NodeCategory
8 | nodes: SerializedNode[]
9 | }
10 |
11 | export class SplootFragment {
12 | nodes: SplootNode[]
13 | nodeCategory: NodeCategory
14 |
15 | constructor(nodes: SplootNode[], nodeCategory: NodeCategory, trim = true) {
16 | this.nodeCategory = nodeCategory
17 | this.nodes = nodes
18 | if (trim) {
19 | this.trim()
20 | }
21 | this.nodes.forEach((node) => {
22 | if (!isNodeInCategory(node.type, this.nodeCategory)) {
23 | throw new InvariantViolationError(
24 | InvariantViolationType.FragmentNodeCategory,
25 | `${node.type} is not valid for fragment category ${this.nodeCategory}.`
26 | )
27 | }
28 | })
29 | }
30 |
31 | trim() {
32 | // Trim invisible nodes
33 | if (this.nodes.length === 1 && this.nodes[0].getNodeLayout().isInvisible()) {
34 | const node = this.nodes[0]
35 | if (node.childSetOrder.length === 1) {
36 | const childSet = node.getChildSet(node.childSetOrder[0])
37 | this.nodes = childSet.children
38 | this.nodeCategory = childSet.nodeCategory
39 | }
40 | }
41 | }
42 |
43 | isEmpty() {
44 | return this.nodes.length === 0
45 | }
46 |
47 | clone(): SplootFragment {
48 | return new SplootFragment(
49 | this.nodes.map((node) => node.clone()),
50 | this.nodeCategory
51 | )
52 | }
53 |
54 | isSingle() {
55 | return this.nodes.length == 1
56 | }
57 |
58 | serialize(): SerializedFragment {
59 | return {
60 | category: this.nodeCategory,
61 | nodes: this.nodes.map((node) => node.serialize()),
62 | }
63 | }
64 | }
65 |
66 | export function deserializeFragment(serFragment: SerializedFragment): SplootFragment {
67 | const nodes = serFragment.nodes.map((serNode) => deserializeNode(serNode))
68 | return new SplootFragment(nodes, serFragment.category)
69 | }
70 |
--------------------------------------------------------------------------------
/packages/core/src/language/invariants.ts:
--------------------------------------------------------------------------------
1 | export enum InvariantViolationType {
2 | Unknown = 0,
3 | ChildSetNodeCategory,
4 | FragmentNodeCategory,
5 | }
6 |
7 | export class InvariantViolationError extends Error {
8 | constructor(type: InvariantViolationType, msg: string) {
9 | super(msg)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/core/src/language/mutations/child_set_mutations.ts:
--------------------------------------------------------------------------------
1 | import { ChildSet } from '../childset'
2 | import { SplootNode } from '../node'
3 |
4 | export enum ChildSetMutationType {
5 | INSERT,
6 | DELETE,
7 | }
8 |
9 | export class ChildSetMutation {
10 | childSet: ChildSet
11 | type: ChildSetMutationType
12 | nodes: SplootNode[]
13 | index: number
14 | }
15 |
--------------------------------------------------------------------------------
/packages/core/src/language/mutations/node_mutations.ts:
--------------------------------------------------------------------------------
1 | import { LoopAnnotation, NodeAnnotation } from '../annotations/annotations'
2 | import { SplootNode } from '../node'
3 |
4 | export enum NodeMutationType {
5 | SET_PROPERTY,
6 | SET_RUNTIME_ANNOTATIONS,
7 | SET_VALIDITY,
8 | UPDATE_NODE_LAYOUT,
9 | }
10 |
11 | export class NodeMutation {
12 | node: SplootNode
13 | type: NodeMutationType
14 | property: string
15 | value: string
16 | annotations: NodeAnnotation[]
17 | validity: { valid: boolean; reason: string; childset?: string; index?: number }
18 | loopAnnotation: LoopAnnotation
19 | }
20 |
--------------------------------------------------------------------------------
/packages/core/src/language/mutations/project_mutations.ts:
--------------------------------------------------------------------------------
1 | import { Dependency } from '../projects/project'
2 | import { RunSettings } from '../projects/run_settings'
3 |
4 | export enum ProjectMutationType {
5 | SET_ENVIRONMENT_VAR,
6 | DELETE_ENVIRONMENT_VAR,
7 | UPDATE_RUN_SETTINGS,
8 | UPDATE_DEPENDENCIES,
9 | }
10 |
11 | export type ProjectMutation =
12 | | ProjectUpdateEnvVarMutation
13 | | ProjectDeleteEnvVarMutation
14 | | ProjectUpdateRunSettingsMutation
15 | | ProjectUpdateDependenciesMutation
16 |
17 | export class ProjectUpdateEnvVarMutation {
18 | type: ProjectMutationType.SET_ENVIRONMENT_VAR
19 | newName: string
20 | newValue: string
21 | secret: boolean
22 | }
23 |
24 | export class ProjectDeleteEnvVarMutation {
25 | type: ProjectMutationType.DELETE_ENVIRONMENT_VAR
26 | name: string
27 | }
28 |
29 | export class ProjectUpdateRunSettingsMutation {
30 | type: ProjectMutationType.UPDATE_RUN_SETTINGS
31 | newSettings: RunSettings
32 | }
33 |
34 | export class ProjectUpdateDependenciesMutation {
35 | type: ProjectMutationType.UPDATE_DEPENDENCIES
36 | newDependencies: Dependency[]
37 | }
38 |
--------------------------------------------------------------------------------
/packages/core/src/language/mutations/scope_mutations.ts:
--------------------------------------------------------------------------------
1 | export enum ScopeMutationType {
2 | ADD_CHILD_SCOPE,
3 | REMOVE_CHILD_SCOPE,
4 | ADD_OR_UPDATE_ENTRY,
5 | REMOVE_ENTRY,
6 | RENAME_ENTRY,
7 | IMPORT_MODULE,
8 | }
9 |
10 | export type ScopeMutation = ChildScopeScopeMutation | RenameScopeMutation | EntryScopeMutation | ImportModuleMutation
11 |
12 | export interface ChildScopeScopeMutation {
13 | type: ScopeMutationType.ADD_CHILD_SCOPE | ScopeMutationType.REMOVE_CHILD_SCOPE
14 | }
15 |
16 | export interface EntryScopeMutation {
17 | type: ScopeMutationType.ADD_OR_UPDATE_ENTRY | ScopeMutationType.REMOVE_ENTRY
18 | name: string
19 | }
20 |
21 | export interface RenameScopeMutation {
22 | type: ScopeMutationType.RENAME_ENTRY
23 | previousName: string
24 | newName: string
25 | }
26 |
27 | export interface ImportModuleMutation {
28 | type: ScopeMutationType.IMPORT_MODULE
29 | moduleName: string
30 | }
31 |
--------------------------------------------------------------------------------
/packages/core/src/language/observers.ts:
--------------------------------------------------------------------------------
1 | import { ChildSetMutation } from './mutations/child_set_mutations'
2 | import { NodeMutation } from './mutations/node_mutations'
3 | import { ProjectMutation } from './mutations/project_mutations'
4 | import { ScopeMutation } from './mutations/scope_mutations'
5 |
6 | export interface NodeObserver {
7 | handleNodeMutation(nodeMutation: NodeMutation): void
8 | }
9 |
10 | export interface ChildSetObserver {
11 | handleChildSetMutation(mutations: ChildSetMutation): void
12 | }
13 |
14 | export interface ScopeObserver {
15 | handleScopeMutation(mutation: ScopeMutation): void
16 | }
17 |
18 | export interface ProjectObserver {
19 | handleProjectMutation(mutation: ProjectMutation): void
20 | }
21 |
--------------------------------------------------------------------------------
/packages/core/src/language/projects/file.ts:
--------------------------------------------------------------------------------
1 | import { SplootNode } from '../node'
2 |
3 | export interface SerializedSplootFileRef {
4 | name: string
5 | type: string
6 | }
7 |
8 | export class SplootFile {
9 | name: string
10 | type: string // Sploot node type
11 | rootNode: SplootNode
12 | isLoaded: boolean
13 |
14 | constructor(name: string, type: string) {
15 | this.rootNode = null
16 | this.name = name
17 | this.type = type
18 | this.isLoaded = false
19 | }
20 |
21 | fileLoaded(node: SplootNode) {
22 | this.rootNode = node
23 | this.isLoaded = true
24 | }
25 |
26 | serialize(): string {
27 | // We can't do this if we're not loaded.
28 | if (!this.isLoaded) {
29 | throw 'Error: Cannot serialize a file that is not loaded.'
30 | }
31 | const ser = this.rootNode.serialize()
32 | return JSON.stringify(ser) + '\n'
33 | }
34 |
35 | getSerializedRef(): SerializedSplootFileRef {
36 | return {
37 | name: this.name,
38 | type: this.type,
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/core/src/language/projects/file_loader.ts:
--------------------------------------------------------------------------------
1 | import { Dependency, Project } from './project'
2 | import { HTTPScenario } from '../../http_types'
3 | import { RunType } from './run_settings'
4 | import { SplootFile } from './file'
5 | import { SplootNode } from '../node'
6 | import { SplootPackage } from './package'
7 |
8 | export interface FileLoader {
9 | isReadOnly: () => boolean
10 | loadPackage: (ownerID: string, projectId: string, packageId: string) => Promise
11 | loadFile: (ownerID: string, projectId: string, packageId: string, filename: string) => Promise
12 | saveFile: (
13 | ownerID: string,
14 | projectId: string,
15 | packageId: string,
16 | file: SplootFile,
17 | base_version: string
18 | ) => Promise
19 | }
20 |
21 | export interface ProjectMetadata {
22 | owner: string
23 | id: string
24 | title: string
25 | lastModified: string
26 | live?: boolean
27 | shared?: boolean
28 | runType: RunType
29 | }
30 |
31 | export interface ProjectLoader {
32 | listProjectMetadata: () => Promise
33 | isValidProjectId: (ownerId: string, projectId: string) => Promise
34 | generateValidProjectId: (ownerId: string, projectId: string, title: string) => Promise<[string, string]>
35 | loadProject: (ownerId: string, projectId: string) => Promise
36 | newProject: (
37 | ownerId: string,
38 | projectId: string,
39 | title: string,
40 | layoutType: string,
41 | runType: RunType
42 | ) => Promise
43 | deleteProject: (ownerId: string, projectId: string) => Promise
44 | cloneProject: (newOwnerId: string, newProjectId: string, title: string, existingProject: Project) => Promise
45 | saveProject: (project: Project) => Promise
46 | deleteHTTPScenario: (project: Project, scenarioID: number) => Promise
47 | saveHTTPScenario: (project: Project, scenario: HTTPScenario) => Promise
48 | isCurrentVersion: (project: Project) => Promise
49 | saveDependency: (project: Project, dependency: Dependency) => Promise
50 | deleteDependency: (project: Project, dependencyID: number) => Promise
51 | }
52 |
53 | export class SaveError extends Error {
54 | constructor(msg: string) {
55 | super(msg)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/core/src/language/projects/package.ts:
--------------------------------------------------------------------------------
1 | import { FileLoader } from './file_loader'
2 | import { SerializedSplootFileRef, SplootFile } from './file'
3 | import { SplootNode } from '../node'
4 |
5 | export interface SerializedSplootPackageRef {
6 | name: string
7 | buildType: number
8 | }
9 |
10 | export interface SerializedSplootPackage {
11 | name: string
12 | files: SerializedSplootFileRef[]
13 | buildType: number
14 | }
15 |
16 | export enum PackageBuildType {
17 | STATIC = 0,
18 | JS_BUNDLE = 1,
19 | STYLE_BUNDLE = 2,
20 | PYTHON = 4,
21 | }
22 |
23 | export class SplootPackage {
24 | ownerID: string
25 | projectId: string
26 | name: string
27 | files: { [key: string]: SplootFile }
28 | fileOrder: string[]
29 | buildType: PackageBuildType
30 |
31 | constructor(ownerID: string, projectId: string, pack: SerializedSplootPackage) {
32 | this.ownerID = ownerID
33 | this.projectId = projectId
34 | this.name = pack.name
35 | this.buildType = pack.buildType
36 | this.fileOrder = pack.files.map((file) => file.name)
37 | this.files = {}
38 | pack.files.forEach((file) => {
39 | this.files[file.name] = new SplootFile(file.name, file.type)
40 | })
41 | }
42 |
43 | serialize(): string {
44 | const ser: SerializedSplootPackage = {
45 | name: this.name,
46 | buildType: this.buildType,
47 | files: [],
48 | }
49 | this.fileOrder.forEach((filename) => {
50 | ser.files.push(this.files[filename].getSerializedRef())
51 | })
52 | return JSON.stringify(ser, null, 2) + '\n'
53 | }
54 |
55 | getDefaultFile(): SplootFile {
56 | return this.files[this.fileOrder[0]]
57 | }
58 |
59 | async addFile(name: string, type: string, rootNode: SplootNode) {
60 | const splootFile = new SplootFile(name, type)
61 | splootFile.fileLoaded(rootNode)
62 | this.files[name] = splootFile
63 | if (!this.fileOrder.includes(name)) {
64 | this.fileOrder.push(name)
65 | }
66 | }
67 |
68 | async getLoadedFile(fileLoader: FileLoader, name: string): Promise {
69 | const file = this.files[name]
70 | if (!file.isLoaded) {
71 | return await fileLoader.loadFile(this.ownerID, this.projectId, this.name, name).then((rootNode: SplootNode) => {
72 | file.fileLoaded(rootNode)
73 | return file
74 | })
75 | }
76 | return file
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/core/src/language/projects/run_settings.ts:
--------------------------------------------------------------------------------
1 | import { HTTPScenario } from '../../http_types'
2 |
3 | export enum RunType {
4 | COMMAND_LINE = 'COMMAND_LINE',
5 | HTTP_REQUEST = 'HTTP_REQUEST',
6 | SCHEDULE = 'SCHEDULE',
7 | STREAMLIT = 'STREAMLIT',
8 | }
9 |
10 | export interface RunSettings {
11 | runType: RunType
12 |
13 | httpScenarios: HTTPScenario[]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/core/src/language/tray/tray.ts:
--------------------------------------------------------------------------------
1 | import { SerializedFragment } from '../fragment'
2 | import { SerializedNode } from '../type_registry'
3 |
4 | export type TrayListing = TrayCategory | TrayEntry
5 |
6 | export interface TrayCategory {
7 | category: string
8 | entries: TrayListing[]
9 | }
10 |
11 | export interface TrayExample {
12 | serializedNodes: SerializedFragment
13 | description: string
14 | }
15 |
16 | export interface TrayEntry {
17 | key: string
18 | title?: string
19 | serializedNode?: SerializedNode
20 | abstract: string
21 | examples?: TrayExample[]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/core/src/language/validation/validation_watcher.ts:
--------------------------------------------------------------------------------
1 | import { NodeMutation, NodeMutationType } from '../mutations/node_mutations'
2 | import { NodeObserver } from '../observers'
3 | import { SplootNode } from '../node'
4 | import { globalMutationDispatcher } from '../mutations/mutation_dispatcher'
5 |
6 | export class ValidationWatcher implements NodeObserver {
7 | private invalidNodes: Set
8 |
9 | constructor() {
10 | this.invalidNodes = new Set()
11 | }
12 |
13 | handleNodeMutation(nodeMutation: NodeMutation): void {
14 | if (nodeMutation.type === NodeMutationType.SET_VALIDITY) {
15 | if (nodeMutation.validity.valid) {
16 | this.invalidNodes.delete(nodeMutation.node)
17 | } else {
18 | this.invalidNodes.add(nodeMutation.node)
19 | }
20 | }
21 | }
22 |
23 | isValid(): boolean {
24 | return this.invalidNodes.size === 0
25 | }
26 |
27 | registerSelf() {
28 | globalMutationDispatcher.registerNodeObserver(this)
29 | }
30 |
31 | deregisterSelf() {
32 | globalMutationDispatcher.deregisterNodeObserver(this)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "lib": ["DOM", "ES2020"],
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "experimentalDecorators": true,
10 | "resolveJsonModule": true,
11 | "skipDefaultLibCheck": true,
12 | "downlevelIteration": true,
13 | "noEmit": true,
14 | "baseUrl": ".",
15 | "types": ["@types/wicg-file-system-access"]
16 | },
17 | "include": ["src"],
18 | "exclude": [
19 | "node_modules",
20 | "dist",
21 | "out",
22 | "*.config.*"
23 | ],
24 | "rootDir": "src",
25 | }
26 |
--------------------------------------------------------------------------------
/packages/editor/.gitignore:
--------------------------------------------------------------------------------
1 | # non-dev env files
2 | production.env
3 |
4 | # Firebase files
5 | .firebase
6 |
7 | lib
8 |
9 | # Terraform
10 | .terraform
11 |
12 | # Lambda zips
13 |
14 | # Mac files
15 | .DS_Store
16 |
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | lerna-debug.log*
24 |
25 | # Diagnostic reports (https://nodejs.org/api/report.html)
26 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
27 |
28 | # Runtime data
29 | pids
30 | *.pid
31 | *.seed
32 | *.pid.lock
33 |
34 | # Directory for instrumented libs generated by jscoverage/JSCover
35 | lib-cov
36 |
37 | # Coverage directory used by tools like istanbul
38 | coverage
39 | *.lcov
40 |
41 | # nyc test coverage
42 | .nyc_output
43 |
44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
45 | .grunt
46 |
47 | # Bower dependency directory (https://bower.io/)
48 | bower_components
49 |
50 | # node-waf configuration
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 | build/Release
55 |
56 | # Dependency directories
57 | node_modules/
58 | jspm_packages/
59 |
60 | # TypeScript v1 declaration files
61 | typings/
62 |
63 | # TypeScript cache
64 | *.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 | .npm
68 |
69 | # Optional eslint cache
70 | .eslintcache
71 |
72 | # Microbundle cache
73 | .rpt2_cache/
74 | .rts2_cache_cjs/
75 | .rts2_cache_es/
76 | .rts2_cache_umd/
77 |
78 | # Optional REPL history
79 | .node_repl_history
80 |
81 | # Output of 'npm pack'
82 | *.tgz
83 |
84 | # Yarn Integrity file
85 | .yarn-integrity
86 |
87 | # parcel-bundler cache (https://parceljs.org/)
88 | .cache
89 |
90 | # Next.js build output
91 | .next
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 | out
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # Serverless directories
108 | .serverless/
109 |
110 | # FuseBox cache
111 | .fusebox/
112 |
113 | # DynamoDB Local files
114 | .dynamodb/
115 |
116 | # TernJS port file
117 | .tern-port
118 |
--------------------------------------------------------------------------------
/packages/editor/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types'
2 |
3 | const config: Config.InitialOptions = {
4 | preset: 'ts-jest',
5 | verbose: true,
6 | testPathIgnorePatterns: ['node_modules', 'lib', 'dist', 'out'],
7 | moduleNameMapper: {
8 | '\\.(css|less)$': 'identity-obj-proxy',
9 | },
10 | }
11 | export default config
12 |
--------------------------------------------------------------------------------
/packages/editor/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@splootcode/editor",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "styles": "./dist/index.css",
8 | "typings": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": "./dist/index.mjs",
12 | "require": "./dist/index.js"
13 | },
14 | "./styles.css": {
15 | "import": "./dist/index.css"
16 | }
17 | },
18 | "files": [
19 | "dist/index.css"
20 | ],
21 | "private": true,
22 | "scripts": {
23 | "test": "jest",
24 | "build": "yarn rollup -c"
25 | },
26 | "peerDependencies": {
27 | "@chakra-ui/icons": "^1.1.7",
28 | "@chakra-ui/react": "^1.6.9",
29 | "@emotion/react": "^11.1.4",
30 | "@emotion/styled": "^11.0.0",
31 | "react-router-dom": "^5.1.2"
32 | },
33 | "devDependencies": {
34 | "@chakra-ui/icons": "^1.1.7",
35 | "@chakra-ui/react": "^1.6.9",
36 | "@rollup/plugin-node-resolve": "^15.0.2",
37 | "@rollup/plugin-typescript": "^11.1.0",
38 | "@types/content-type": "^1.1.5",
39 | "@types/jest": "^28.1.3",
40 | "@types/react": "^17.0.2",
41 | "@types/react-dom": "^17.0.2",
42 | "@types/react-router-dom": "^5.3.3",
43 | "allotment": "^1.11.1",
44 | "content-type": "^1.0.5",
45 | "fuse.js": "^6.6.2",
46 | "http-status-codes": "^2.2.0",
47 | "identity-obj-proxy": "^3.0.0",
48 | "jest": "^28.1.1",
49 | "jest-canvas-mock": "^2.4.0",
50 | "jest-environment-jsdom": "^28.1.1",
51 | "mobx": "^5.15.0",
52 | "mobx-react": "^6.2.5",
53 | "pretty": "^2.0.0",
54 | "react": "^17.0.2",
55 | "react-dom": "^17.0.2",
56 | "react-icons": "^4.4.0",
57 | "rollup": "3.20.2",
58 | "rollup-plugin-dts": "^5.3.0",
59 | "rollup-plugin-import-css": "^3.2.1",
60 | "ts-jest": "^28.0.5",
61 | "ts-node": "^10.8.1",
62 | "typescript": "^4.8.4",
63 | "xterm": "^5.1.0",
64 | "xterm-addon-fit": "^0.7.0"
65 | },
66 | "dependencies": {
67 | "@rollup/plugin-commonjs": "^24.0.1"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/editor/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs'
2 | import css from 'rollup-plugin-import-css'
3 | import dts from 'rollup-plugin-dts'
4 | import typescript from '@rollup/plugin-typescript'
5 | import { nodeResolve } from '@rollup/plugin-node-resolve'
6 |
7 | export default [
8 | {
9 | external: ['react', '@emotion/core', '@emotion/styled', '@chakra-ui/react', '@chakra-ui/icons'],
10 | plugins: [
11 | typescript(),
12 | commonjs(),
13 | nodeResolve({
14 | resolveOnly: ['react-icons', 'http-status-codes', 'content-type', 'xterm', 'structured-pyright'],
15 | }),
16 | css({ minify: true }),
17 | ],
18 | input: 'src/index.ts',
19 | output: [
20 | {
21 | file: `dist/index.mjs`,
22 | format: 'es',
23 | sourcemap: true,
24 | },
25 | {
26 | file: `dist/index.js`,
27 | format: 'cjs',
28 | sourcemap: true,
29 | },
30 | ],
31 | },
32 | {
33 | input: 'src/index.ts',
34 | output: [{ file: `dist/index.d.ts`, format: 'es' }],
35 | plugins: [dts(), css()],
36 | },
37 | ]
38 |
--------------------------------------------------------------------------------
/packages/editor/src/components/attached_child.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SplootCode/splootcode/d621bcd5f30df97150d3e1388f60307636568780/packages/editor/src/components/attached_child.css
--------------------------------------------------------------------------------
/packages/editor/src/components/autosave_info.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { EditorState } from '../context/editor_context'
3 | import { Text, useToast } from '@chakra-ui/react'
4 | import { observer } from 'mobx-react'
5 |
6 | interface AutosaveInfoProps {
7 | editorState: EditorState
8 | reloadProject: () => void
9 | }
10 |
11 | export const AutosaveInfo = observer((props: AutosaveInfoProps) => {
12 | const toast = useToast()
13 |
14 | const { editorState, reloadProject } = props
15 |
16 | useEffect(() => {
17 | if (!editorState?.project.isReadOnly) {
18 | const checkVersion = async () => {
19 | if (document['hidden'] === false) {
20 | const isCurrent = await editorState.autosaveWatcher.projectLoader.isCurrentVersion(editorState?.project)
21 | if (!isCurrent) {
22 | reloadProject()
23 | }
24 | }
25 | }
26 | window.addEventListener('visibilitychange', checkVersion)
27 | return () => {
28 | window.removeEventListener('visibilitychange', checkVersion)
29 | }
30 | }
31 | }, [editorState?.project])
32 |
33 | useEffect(() => {
34 | if (!editorState?.autosaveWatcher?.failedSave) {
35 | return
36 | }
37 |
38 | toast({
39 | title: editorState.autosaveWatcher.failedSaveInfo.title,
40 | position: 'top',
41 | status: 'warning',
42 | })
43 | }, [editorState?.autosaveWatcher.failedSave])
44 |
45 | if (editorState.project?.isReadOnly || editorState.autosaveWatcher.failedSave) {
46 | return {editorState.autosaveWatcher.needsSave ? 'Not saved' : ''}
47 | }
48 | return {editorState.autosaveWatcher.needsSave ? 'Saving...' : 'Saved'}
49 | })
50 |
--------------------------------------------------------------------------------
/packages/editor/src/components/cursor.css:
--------------------------------------------------------------------------------
1 | .inline-cursor-hover {
2 | fill: transparent;
3 | }
4 |
5 | .inline-cursor-group:hover line.inline-cursor {
6 | stroke:white;
7 | }
8 |
9 | .inline-cursor-group:hover line.inline-cursor-lite {
10 | stroke:white;
11 | }
12 |
13 | .inline-cursor-group:hover circle.inline-cursor-circle {
14 | fill: white;
15 | }
16 |
17 | .inline-cursor-circle {
18 | fill: transparent;
19 | }
20 |
21 | .inline-cursor {
22 | stroke: transparent;
23 | stroke-width: 2;
24 | }
25 |
26 | .inline-cursor-lite {
27 | stroke: transparent;
28 | stroke-width: 1;
29 | }
30 |
31 | .inline-cursor.selected {
32 | stroke: white;
33 | }
34 |
35 | .inline-cursor-lite.selected {
36 | stroke: white;
37 | stroke-width: 1;
38 | }
39 |
40 | .active-inline-cursor {
41 | stroke: var(--editor-cursor-color);
42 | fill: transparent;
43 | stroke-width: 2;
44 | animation-name: color;
45 | animation-duration: 3s;
46 | animation-iteration-count: infinite;
47 | }
48 |
49 | @keyframes color {
50 | 0% {
51 | stroke: var(--editor-cursor-color);
52 | }
53 | 50% {
54 | stroke: var(--editor-cursor-blink-color);
55 | }
56 | 100% {
57 | stroke: var(--editor-cursor-color);
58 | }
59 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/cursor.tsx:
--------------------------------------------------------------------------------
1 | import './cursor.css'
2 |
3 | import React from 'react'
4 | import { observer } from 'mobx-react'
5 |
6 | import { NODE_BLOCK_HEIGHT } from '../layout/layout_constants'
7 | import { NodeSelection, SelectionState } from '../context/selection'
8 |
9 | interface ActiveCursorProps {
10 | selection: NodeSelection
11 | }
12 |
13 | @observer
14 | export class ActiveCursor extends React.Component {
15 | render() {
16 | const selection = this.props.selection
17 | if (selection.state !== SelectionState.Cursor || selection.isHidden) {
18 | return null
19 | }
20 |
21 | const [x, y] = selection.getCursorXYPosition()
22 | return
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/editor/src/components/drag_overlay.css:
--------------------------------------------------------------------------------
1 |
2 | .drag-overlay {
3 | position: fixed;
4 | background-color: transparent;
5 | top: 0;
6 | left: 0;
7 | width: 100vw;
8 | height: 100vh;
9 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/edit_box.css:
--------------------------------------------------------------------------------
1 | .edit-box {
2 | box-sizing: border-box;
3 | background-color: var(--editor-selected-node-block-fill);
4 | display: block;
5 | padding: 0;
6 | border-width: 0;
7 | border: none;
8 | line-height: 1;
9 | }
10 |
11 | .edit-box-string {
12 | font-family: var(--string-font);
13 | box-sizing: border-box;
14 | margin: 0;
15 | }
16 |
17 | .edit-box textarea {
18 | background-color: rgba(0, 0, 0, 0);
19 | box-sizing: border-box;
20 | font-family: inherit;
21 | border: none;
22 | color: white;
23 | padding: 0px;
24 | line-height: 16px;
25 | font-size: 15px;
26 | resize: none;
27 | margin: 0;
28 | }
29 |
30 | .edit-box textarea::-webkit-scrollbar {
31 | width: 0;
32 | background: transparent;
33 | }
34 |
35 | .edit-box textarea:focus {
36 | outline: none;
37 | }
38 |
39 | .edit-box input[type=text] {
40 | background-color: rgba(0, 0, 0, 0);
41 | font-family: inherit;
42 | border-style: none;
43 | color:white;
44 | padding: 0px;
45 | line-height: 1;
46 | }
47 |
48 | .edit-box input[type=text]:focus {
49 | outline: none;
50 | }
51 |
--------------------------------------------------------------------------------
/packages/editor/src/components/editor_banner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | import { Button, Flex, Text } from '@chakra-ui/react'
4 | import { ChildSetMutation, NodeMutation, Project, ProjectMutation, globalMutationDispatcher } from '@splootcode/core'
5 |
6 | export function EditorBanner(props: { project: Project; onSaveAs: () => void }) {
7 | const { project, onSaveAs } = props
8 | const [mutationCount, setMutationCount] = useState(0)
9 | const [dismissed, setDismissed] = useState(false)
10 |
11 | useEffect(() => {
12 | if (!dismissed) {
13 | const mutationObserver = {
14 | handleNodeMutation: (mutation: NodeMutation) => {
15 | setMutationCount((count) => count + 1)
16 | },
17 | handleChildSetMutation: (mutation: ChildSetMutation) => {
18 | setMutationCount((count) => count + 1)
19 | },
20 | handleProjectMutation: (mutation: ProjectMutation) => {
21 | setMutationCount((count) => count + 1)
22 | },
23 | }
24 | globalMutationDispatcher.registerChildSetObserver(mutationObserver)
25 | globalMutationDispatcher.registerNodeObserver(mutationObserver)
26 | globalMutationDispatcher.registerProjectObserver(mutationObserver)
27 | const cleanup = () => {
28 | globalMutationDispatcher.deregisterNodeObserver(mutationObserver)
29 | globalMutationDispatcher.deregisterChildSetObserver(mutationObserver)
30 | globalMutationDispatcher.deregisterProjectObserver(mutationObserver)
31 | }
32 | return cleanup
33 | }
34 | }, [project, dismissed])
35 |
36 | if (mutationCount < 3 || dismissed) {
37 | return null
38 | }
39 |
40 | return (
41 |
42 | This is an example, make a copy to save your progress.
43 | {' '}
46 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/packages/editor/src/components/editor_side_menu.css:
--------------------------------------------------------------------------------
1 |
2 | .editor-side-menu {
3 | --editor-bg-color: var(--chakra-colors-gray-900);
4 | width: 100%;
5 | height: 100%;
6 | overflow-y: scroll;
7 | overflow-x: hidden;
8 | border-right: 1px solid var(--section-separator-color);
9 | box-sizing: border-box;
10 | padding: 0;
11 | background-color: var(--chakra-colors-gray-900);
12 | color: var(--chakra-colors-gray-300)
13 | }
14 |
15 | .editor-side-menu::-webkit-scrollbar {
16 | width: 0; /* Remove scrollbar space */
17 | background: transparent; /* Optional: just make scrollbar invisible */
18 | }
19 |
20 | .editor-side-menu-bar {
21 | width: 46px;
22 | box-sizing: border-box;
23 | background-color: var(--chakra-colors-gray-900);
24 | display: flex;
25 | flex-direction: column;
26 | align-items: center;
27 | border-right: 2px solid var(--section-separator-color);
28 | padding: 0px;
29 | }
30 |
31 | .editor-side-menu-container {
32 | width: 100%;
33 | }
34 |
35 | .editor-side-menu-icon {
36 | fill: currentColor
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor/src/components/fragment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { EditorNodeBlock } from './node_block'
4 | import { NodeBlock } from '../layout/rendered_node'
5 | import { NodeSelectionState } from '../context/selection'
6 | import { RenderedFragment } from '../layout/rendered_fragment'
7 |
8 | interface FragmentViewProps {
9 | fragment: RenderedFragment
10 | }
11 |
12 | export class FragmentView extends React.Component {
13 | render() {
14 | const fragment = this.props.fragment
15 |
16 | return (
17 |
18 | {fragment.nodes.map((nodeBlock: NodeBlock, idx: number) => {
19 | return (
20 |
21 |
22 |
23 | )
24 | })}
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor/src/components/list_block.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SplootCode/splootcode/d621bcd5f30df97150d3e1388f60307636568780/packages/editor/src/components/list_block.css
--------------------------------------------------------------------------------
/packages/editor/src/components/list_block.tsx:
--------------------------------------------------------------------------------
1 | import './list_block.css'
2 |
3 | import React from 'react'
4 | import { observer } from 'mobx-react'
5 |
6 | import { EditorNodeBlock } from './node_block'
7 | import { NodeBlock } from '../layout/rendered_node'
8 | import { RenderedChildSetBlock } from '../layout/rendered_childset_block'
9 |
10 | interface ExpandedListBlockViewProps {
11 | block: RenderedChildSetBlock
12 | isSelected: boolean
13 | }
14 |
15 | @observer
16 | export class ExpandedListBlockView extends React.Component {
17 | render() {
18 | const block = this.props.block
19 |
20 | return (
21 |
22 | {block.nodes.map((nodeBlock: NodeBlock, idx: number) => {
23 | const selectionState = block.getChildSelectionState(idx)
24 | return (
25 |
26 |
27 |
28 | )
29 | })}
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor/src/components/node_block.css:
--------------------------------------------------------------------------------
1 |
2 | .inline-component {
3 | display: inline-block;
4 | padding-left: 4px;
5 | padding-right: 4px;
6 | padding-bottom: 2px;
7 | padding-top: 2px;
8 | margin: 2px;
9 | }
10 |
11 | .svgsplootnode {
12 | stroke: var(--editor-node-block-edge);
13 | fill: var(--editor-node-block-fill);
14 | }
15 |
16 | .svgsplootnode:focus {
17 | outline: 0;
18 | }
19 |
20 | .svgsplootnode.cap-line {
21 | stroke: var(--editor-cap-line);
22 | }
23 |
24 | .svgsplootnode.gap {
25 | fill: var(--editor-bg-color);
26 | stroke: var(--editor-bg-color);
27 | }
28 |
29 | .svgsplootnode.gap.selected {
30 | fill: var(--editor-bg-color);
31 | stroke: var(--editor-bg-color);
32 | }
33 |
34 | .svgsplootnode.gap.placeholder-outline {
35 | stroke-dasharray: 3px 3px;
36 | stroke: var(--editor-placeholder-stroke);
37 | }
38 |
39 | .svgsplootnode.gap.selected.placeholder-outline {
40 | stroke: var(--editor-placeholder-stroke);
41 | }
42 |
43 | .svgsplootnode.selected {
44 | stroke: var(--editor-selected-node-block-edge);
45 | fill: var(--editor-selected-node-block-fill);
46 | }
47 |
48 | .svgsplootnode.invalid {
49 | stroke: var(--editor-invalid-node-block-edge);
50 | }
51 |
52 | .invisible-splootnode-invalid {
53 | fill: transparent;
54 | stroke: var(--editor-invalid-node-block-edge);
55 | }
56 |
57 | .indented-rule {
58 | stroke: var(--editor-indented-block-stroke);
59 | stroke-width: 1.4px;
60 | }
61 |
62 | .indented-rule.selected {
63 | stroke: var(--editor-selected-indented-block-stroke);
64 | stroke-width: 1.4px;
65 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/node_block.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import 'jest-canvas-mock'
5 | import React from 'react'
6 | import pretty from 'pretty'
7 | import { act } from 'react-dom/test-utils'
8 | import { render, unmountComponentAtNode } from 'react-dom'
9 |
10 | import { PythonStringLiteral, loadPythonTypes } from '@splootcode/language-python'
11 |
12 | import { EditorNodeBlock } from './node_block'
13 | import { NodeBlock } from '../layout/rendered_node'
14 | import { NodeSelectionState } from '../context/selection'
15 |
16 | let container: HTMLElement = null
17 |
18 | beforeAll(() => {
19 | loadPythonTypes()
20 | })
21 |
22 | beforeEach(() => {
23 | container = document.createElement('div')
24 | document.body.appendChild(container)
25 | })
26 |
27 | afterEach(() => {
28 | unmountComponentAtNode(container)
29 | container.remove()
30 | container = null
31 | })
32 |
33 | it('renders a single string literal node', () => {
34 | const node = new PythonStringLiteral(null, 'hello this is string')
35 | const nodeBlock = new NodeBlock(null, node, null, 0)
36 | nodeBlock.calculateDimensions(0, 0, null)
37 |
38 | act(() => {
39 | render(
40 | ,
43 | container
44 | )
45 | })
46 | expect(container.children).toHaveLength(1)
47 | expect(pretty(container.innerHTML)).toMatchInlineSnapshot(`
48 | ""
57 | `)
58 | })
59 |
--------------------------------------------------------------------------------
/packages/editor/src/components/placeholder_label.css:
--------------------------------------------------------------------------------
1 |
2 | .placeholder-label {
3 | fill: var(--editor-placeholder-label);
4 | font-style: italic;
5 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/placeholder_label.tsx:
--------------------------------------------------------------------------------
1 | import './placeholder_label.css'
2 |
3 | import React from 'react'
4 |
5 | import { EXPRESSION_TOKEN_SPACING, NODE_TEXT_OFFSET } from '../layout/layout_constants'
6 |
7 | export function PlaceholderLabel(props: { x: number; y: number; label: string }) {
8 | const { x, y, label } = props
9 | return (
10 |
11 | {label}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/packages/editor/src/components/property.css:
--------------------------------------------------------------------------------
1 |
2 | .inline-component-property {
3 | display: inline-block;
4 | padding-left: 4px;
5 | padding-right: 4px;
6 | padding-bottom: 2px;
7 | padding-top: 2px;
8 | margin: 2px;
9 | }
10 |
11 |
12 | .editing.inline-component-property {
13 | padding-bottom: 1px;
14 | padding-top: 1px;
15 | }
16 |
17 | .inline-component-property > input {
18 | font-family: inherit;
19 | border: none;
20 | }
21 |
22 | .inline-component-property > input:focus {
23 | outline: none;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/editor/src/components/property.tsx:
--------------------------------------------------------------------------------
1 | import './property.css'
2 |
3 | import React from 'react'
4 |
5 | import { NODE_TEXT_OFFSET } from '../layout/layout_constants'
6 | import { NodeBlock } from '../layout/rendered_node'
7 | import { NodeSelectionState } from '../context/selection'
8 |
9 | interface PropertyProps {
10 | block: NodeBlock
11 | leftPos: number
12 | topPos: number
13 | propertyName: string
14 | selectState: NodeSelectionState
15 | }
16 |
17 | export class InlineProperty extends React.Component {
18 | render() {
19 | const { block, leftPos, topPos, propertyName } = this.props
20 | const { node } = this.props.block
21 |
22 | return (
23 |
24 | {node.getProperty(propertyName)}
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor/src/components/runtime_annotations.css:
--------------------------------------------------------------------------------
1 |
2 | .annotation {
3 | fill: var(--editor-annotation-label);
4 | font-size: 11pt;
5 | pointer-events: none;
6 | }
7 |
8 | .error-annotation {
9 | fill: var(--editor-annotation-error);
10 | font-size: 11pt;
11 | pointer-events: none;
12 | }
13 |
14 | .slider-track {
15 | fill: var(--chakra-colors-gray-600);
16 | }
17 |
18 | .slider-fill {
19 | fill: var(--chakra-colors-gray-400);
20 | transition: width 0.5s;
21 | }
22 |
23 | .slider-handle {
24 | fill: var(--chakra-colors-gray-400);
25 | transition: cx 0.5s;
26 | }
27 |
28 | .slider-background {
29 | fill: transparent;
30 | }
31 |
32 | .slider-button {
33 | fill: var(--editor-node-block-fill);
34 | }
35 |
36 | .slider-button-label {
37 | fill: var(--chakra-colors-gray-400);
38 | pointer-events: none;
39 | -webkit-user-select: none;
40 | -moz-user-select: none;
41 | -ms-user-select: none;
42 | user-select: none;
43 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/separator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NODE_BLOCK_HEIGHT, NODE_TEXT_OFFSET } from '../layout/layout_constants'
3 |
4 | interface StringLiteralProps {
5 | x: number
6 | y: number
7 | selectorType: string
8 | isSelected: boolean
9 | }
10 |
11 | export class Separator extends React.Component {
12 | render() {
13 | const { x, y, selectorType, isSelected } = this.props
14 | const className = 'separator' + (isSelected ? ' selected' : '')
15 | const nodeClassname = 'svgsplootnode' + (isSelected ? ' selected' : '')
16 | return (
17 |
18 |
19 |
20 | {selectorType}
21 |
22 |
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/editor/src/components/string_literal.css:
--------------------------------------------------------------------------------
1 |
2 | .string-literal {
3 | display: inline-block;
4 | padding-left: 4px;
5 | padding-right: 4px;
6 | padding-bottom: 2px;
7 | padding-top: 2px;
8 | margin: 2px;
9 | }
10 |
11 | .editing.string-literal {
12 | padding-bottom: 1px;
13 | padding-top: 1px;
14 | }
15 |
16 | .string-literal > input {
17 | font-family: inherit;
18 | border: none;
19 | }
20 |
21 | .string-literal > input:focus {
22 | outline: none;
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/packages/editor/src/components/string_literal.tsx:
--------------------------------------------------------------------------------
1 | import './string_literal.css'
2 |
3 | import React from 'react'
4 |
5 | import { NODE_TEXT_OFFSET } from '../layout/layout_constants'
6 | import { NodeBlock } from '../layout/rendered_node'
7 | import { NodeSelectionState } from '../context/selection'
8 |
9 | interface StringLiteralProps {
10 | block: NodeBlock
11 | leftPos: number
12 | topPos: number
13 | propertyName: string
14 | selectState: NodeSelectionState
15 | }
16 |
17 | export class InlineStringLiteral extends React.Component {
18 | render() {
19 | const { block, propertyName, selectState, leftPos, topPos } = this.props
20 | const { node } = this.props.block
21 | const isEditing = selectState === NodeSelectionState.EDITING
22 | const className = isEditing ? 'editing' : ''
23 |
24 | return (
25 |
32 | "{node.getProperty(propertyName)}"
33 |
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor/src/components/string_node.css:
--------------------------------------------------------------------------------
1 |
2 | .string-node {
3 | font-family: var(--string-font);
4 | font-size: 15px;
5 | line-height: 18px;
6 | white-space: pre-wrap;
7 | color: var(--code-green-text);
8 | margin: 0;
9 | font-weight: normal;
10 | padding: 0px;
11 | }
12 |
13 | .svgsplootnode.stringnode {
14 | fill: var(--editor-string-block-outline);
15 | stroke: var(--editor-string-block-outline);
16 | }
17 |
18 | .svgsplootnode.stringnode.selected {
19 | fill: var(--editor-selected-node-block-fill);
20 | stroke: var(--editor-selected-node-block-edge);
21 | }
22 |
23 | .svgsplootnode.stringnode.selected.invalid {
24 | fill: var(--editor-selected-node-block-fill);
25 | stroke: var(--editor-invalid-node-block-edge);
26 | }
27 |
28 |
29 | .svgsplootnode.stringnode.invalid {
30 | fill: var(--editor-string-block-outline);
31 | stroke: var(--editor-invalid-node-block-edge);
32 | }
33 |
34 |
35 | .string-node.measurement {
36 | position: absolute;
37 | left: -1000px;
38 | top: 0;
39 | }
40 |
41 | .string-node-cap-text {
42 | fill: var(--code-green-text);
43 | font-family: 'Inconsolata';
44 | font-size: 15px;
45 | font-weight: bold;
46 | }
47 |
48 | .editing.string-node {
49 | padding-bottom: 1px;
50 | padding-top: 1px;
51 | }
52 |
53 | .string-node > input {
54 | font-family: inherit;
55 | border: none;
56 | font-size: 15px;
57 | }
58 |
59 | .string-node > input:focus {
60 | outline: none;
61 | }
62 |
63 | .string-node-background {
64 | fill: var(--editor-string-block-fill);
65 | stroke: var(--editor-string-block-fill);
66 | }
67 |
68 |
69 | .string-node-selected-background {
70 | fill: var(--editor-selected-node-block-fill);
71 | stroke: transparent
72 | }
73 |
74 | .string-node-selected-background:focus {
75 | outline: 0;
76 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/token_list_block.tsx:
--------------------------------------------------------------------------------
1 | import './tree_list_block.css'
2 |
3 | import React from 'react'
4 | import { observer } from 'mobx-react'
5 |
6 | import { EditorNodeBlock } from './node_block'
7 | import { NODE_BLOCK_HEIGHT } from '../layout/layout_constants'
8 | import { NodeBlock } from '../layout/rendered_node'
9 | import { PlaceholderLabel } from './placeholder_label'
10 | import { RenderedChildSetBlock } from '../layout/rendered_childset_block'
11 |
12 | interface TokenListBlockViewProps {
13 | block: RenderedChildSetBlock
14 | isSelected: boolean
15 | isValid: boolean
16 | invalidIndex: number
17 | isInline: boolean
18 | }
19 |
20 | @observer
21 | export class TokenListBlockView extends React.Component {
22 | render() {
23 | const { block, isValid, isInline, invalidIndex } = this.props
24 | let shape = null
25 | const childsetInvalid = !isValid && invalidIndex === undefined
26 |
27 | const classname = 'svgsplootnode gap' + (childsetInvalid ? ' invalid' : ' ')
28 | if (isInline || childsetInvalid) {
29 | shape = (
30 |
31 | )
32 | }
33 |
34 | const label =
35 | block.nodes.length === 0 && block.labels.length > 0 ? (
36 |
37 | ) : undefined
38 |
39 | return (
40 |
41 | {shape}
42 | {label}
43 | {block.nodes.map((nodeBlock: NodeBlock, idx: number) => {
44 | const selectionState = block.getChildSelectionState(idx)
45 | const invalidChild = idx === invalidIndex
46 | const label = block.labels.length > idx ? block.labels[idx] : undefined
47 | return (
48 |
49 |
55 |
56 | )
57 | })}
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/editor/src/components/tray/entry.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Box, Text } from '@chakra-ui/react'
4 | import { MicroNode } from './category'
5 | import { RenderedFragment } from '../../layout/rendered_fragment'
6 | import { TrayEntry, deserializeFragment } from '@splootcode/core'
7 |
8 | export interface EntryProps {
9 | entry: TrayEntry
10 | startDrag: (fragment: RenderedFragment, offsetX: number, offsetY: number) => any
11 | }
12 |
13 | export const Entry = (props: EntryProps) => {
14 | const { entry, startDrag } = props
15 | return (
16 | <>
17 |
18 | {entry.abstract}
19 |
20 | {!entry.examples || entry.examples?.length === 0 ? null : (
21 |
30 | Examples:
31 |
32 | )}
33 | {entry.examples?.map((example, idx) => {
34 | const fragment = new RenderedFragment(deserializeFragment(example.serializedNodes), true)
35 | return (
36 |
37 |
38 |
39 | {example.description}
40 |
41 |
42 | )
43 | })}
44 | >
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/packages/editor/src/components/tray/scope_tray.css:
--------------------------------------------------------------------------------
1 |
2 | .scope-tray-entry {
3 | padding: 2px
4 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/tray/tray.css:
--------------------------------------------------------------------------------
1 | .tray-expanded-category {
2 | border-left: 1px solid var(--chakra-colors-gray-600);
3 | }
4 |
5 | .tray-expanded-entry {
6 | border-top: 1px solid var(--chakra-colors-gray-500);
7 | border-bottom: 1px solid var(--chakra-colors-gray-500);
8 | font-size: 14px;
9 | }
10 |
11 | .tray-entry-abstract {
12 | font-style: italic;
13 | line-height: 1.1;
14 | }
15 |
16 | .tray-entry-example-label {
17 | font-style: italic;
18 | line-height: 1.1;
19 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/tree_list_block.css:
--------------------------------------------------------------------------------
1 | .svg-anchor-dot {
2 | fill: var(--editor-anchor-dot);
3 | }
4 |
5 | .svg-anchor-dot.selected {
6 | fill: var(--editor-selected-anchor-dot);
7 | }
8 |
9 | .tree-connector {
10 | stroke: var(--editor-connector-stroke);
11 | stroke-width: 2px;
12 | }
13 |
14 | .tree-connector.selected {
15 | stroke: var(--editor-selected-connector-stroke);
16 | }
17 |
18 | .tree-label {
19 | fill: var(--editor-connector-label);
20 | }
21 |
22 | .tree-label.selected {
23 | fill: var(--editor-selected-connector-label);
24 | }
--------------------------------------------------------------------------------
/packages/editor/src/context/edit_box.ts:
--------------------------------------------------------------------------------
1 | import { NodeBlock } from '../layout/rendered_node'
2 |
3 | export class EditBoxData {
4 | x: number
5 | y: number
6 | property: string
7 | contents: string
8 | node: NodeBlock
9 |
10 | constructor(nodeBlock: NodeBlock, property: string, coordindates: number[]) {
11 | this.node = nodeBlock
12 | this.property = property
13 | this.contents = this.node.node.getProperty(property)
14 | this.x = coordindates[0]
15 | this.y = coordindates[1]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/context/insert_box.ts:
--------------------------------------------------------------------------------
1 | export class InsertBoxData {
2 | x: number
3 | y: number
4 | contents: string
5 |
6 | constructor(coordindates: number[]) {
7 | this.x = coordindates[0] + 3
8 | this.y = coordindates[1]
9 | this.contents = ''
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/src/context/multiselect_deleter.ts:
--------------------------------------------------------------------------------
1 | import { ChildSetType, SplootFragment } from '@splootcode/core'
2 | import { NodeBlock } from '../layout/rendered_node'
3 | import { NodeCursor } from './selection'
4 | import { RenderedChildSetBlock } from '../layout/rendered_childset_block'
5 | import { RenderedTreeIterator } from './rendered_tree_iterator'
6 |
7 | export interface DeleteSet {
8 | node: NodeBlock
9 | keep: SplootFragment[]
10 | }
11 |
12 | export class MultiselectDeleter extends RenderedTreeIterator {
13 | childSetStack: boolean[]
14 | toFullyDelete: DeleteSet[]
15 | toKeep: SplootFragment[]
16 |
17 | constructor(start: NodeCursor, end: NodeCursor) {
18 | super(start, end)
19 | this.childSetStack = []
20 | this.toFullyDelete = []
21 | this.toKeep = []
22 | }
23 |
24 | visitNodeMiddle(node: NodeBlock): void {
25 | if (this.childSetStack.length <= 1 && node.parentChildSet.childSet.type !== ChildSetType.Immutable) {
26 | this.toFullyDelete.push({ node: node, keep: [] })
27 | }
28 | }
29 |
30 | startRangeRight(listBlock: RenderedChildSetBlock): void {
31 | if (listBlock.childSet.type !== ChildSetType.Immutable) {
32 | this.childSetStack.push(true)
33 | }
34 | }
35 |
36 | visitedRangeRight(listBlock: RenderedChildSetBlock, startIndex: number, endIndex: number) {
37 | if (listBlock.childSet.type !== ChildSetType.Immutable) {
38 | this.childSetStack.pop()
39 | }
40 | // Add leftovers to return set
41 | if (endIndex < listBlock.nodes.length && this.childSetStack.length !== 0) {
42 | const nodes = listBlock.childSet.children
43 | .slice(endIndex)
44 | .filter((node) => !node.isEmpty())
45 | .map((node) => node.clone())
46 |
47 | if (nodes.length > 0) {
48 | const fragment = new SplootFragment(nodes, listBlock.childSet.nodeCategory)
49 | this.toFullyDelete[this.toFullyDelete.length - 1].keep.push(fragment)
50 | }
51 | }
52 | }
53 |
54 | getDeletions(): DeleteSet[] {
55 | return this.toFullyDelete
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/editor/src/context/multiselect_tree_walker.ts:
--------------------------------------------------------------------------------
1 | import { NodeCursor, SelectionState } from './selection'
2 | import { RenderedChildSetBlock } from '../layout/rendered_childset_block'
3 | import { RenderedTreeIterator } from './rendered_tree_iterator'
4 |
5 | export class MultiselectTreeWalker extends RenderedTreeIterator {
6 | selectedListBlocks: Set
7 |
8 | constructor(start: NodeCursor, end: NodeCursor) {
9 | super(start, end)
10 | this.selectedListBlocks = new Set()
11 | }
12 |
13 | visitedRangeLeft(listBlock: RenderedChildSetBlock, startIndex: number, endIndex: number) {
14 | listBlock.selectionState = SelectionState.MultiNode
15 | listBlock.selectedIndexStart = startIndex
16 | listBlock.selectedIndexEnd = endIndex
17 | this.selectedListBlocks.add(listBlock)
18 | }
19 |
20 | visitedRangeRight(listBlock: RenderedChildSetBlock, startIndex: number, endIndex: number) {
21 | listBlock.selectionState = SelectionState.MultiNode
22 | listBlock.selectedIndexStart = startIndex
23 | listBlock.selectedIndexEnd = endIndex
24 | this.selectedListBlocks.add(listBlock)
25 | }
26 |
27 | getSelectedListBlocks(): Set {
28 | return this.selectedListBlocks
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/src/editor_hosting_config.ts:
--------------------------------------------------------------------------------
1 | export interface EditorHostingConfig {
2 | FRAME_VIEW_SCHEME: 'http' | 'https'
3 | FRAME_VIEW_DOMAIN: string
4 | TYPESHED_PATH: string
5 | }
6 |
--------------------------------------------------------------------------------
/packages/editor/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Tray } from './components/tray/tray'
2 | export { EditorState, EditorStateContext } from './context/editor_context'
3 | export { Editor } from './components/editor'
4 | export type { EditorHostingConfig } from './editor_hosting_config'
5 | export { EditorBanner } from './components/editor_banner'
6 | export { preloadFonts } from './layout/layout_constants'
7 | export { NodeBlock } from './layout/rendered_node'
8 | export { ExpandedListBlockView } from './components/list_block'
9 | export { ActiveCursor } from './components/cursor'
10 | export { NodeSelection } from './context/selection'
11 | export { EditBox } from './components/edit_box'
12 | export { InsertBox } from './components/insert_box'
13 | export { PythonFrame, RuntimeToken } from './runtime/python_frame'
14 | export { FileChangeWatcher, FileSpec } from './runtime/file_change_watcher'
15 | export { EditorSideMenu, EditorSideMenuView, EditorSideMenuPane, ModuleTrayLoader } from './components/editor_side_menu'
16 | export { PythonRuntimePanel } from './runtime/python_runtime_panel'
17 | export { RenderedFragment } from './layout/rendered_fragment'
18 | export { ConfigPanel } from './components/config_panel'
19 | export { AutosaveInfo } from './components/autosave_info'
20 | export { AutosaveWatcher, AutosaveWatcherFailedSaveInfo } from './context/autosave_watcher'
21 | export { TestRequestPanel } from './components/test_request_panel'
22 | export { MicroNode } from './components/tray/category'
23 | export { Headers, ResponseViewerInfo, BodyInfo, getHeader } from './runtime/response_viewer'
24 |
--------------------------------------------------------------------------------
/packages/editor/src/layout/childset_layout_handler.ts:
--------------------------------------------------------------------------------
1 | import { CursorMap } from '../context/cursor_map'
2 | import { LayoutComponent } from '@splootcode/core'
3 | import { NodeBlock } from './rendered_node'
4 | import { NodeSelection } from '../context/selection'
5 | import { RenderedChildSetBlock } from './rendered_childset_block'
6 |
7 | export interface ChildSetLayoutHandler {
8 | x: number
9 | y: number
10 | width: number
11 | height: number
12 | marginTop: number
13 |
14 | updateLayout(layoutComponent: LayoutComponent): void
15 | calculateDimensions(
16 | x: number,
17 | y: number,
18 | nodes: NodeBlock[],
19 | selection: NodeSelection,
20 | allowInsert: boolean,
21 | insertCursor: number,
22 | insertBoxWidth: number,
23 | marginAlreadyApplied?: boolean
24 | ): void
25 |
26 | getInsertCoordinates(insertIndex: number, cursorOnly?: boolean): [number, number]
27 | allowInsertCursor(insertIndex: number): boolean
28 |
29 | registerCursorPositions(cursorMap: CursorMap, renderedChildSet: RenderedChildSetBlock): void
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/src/runtime/file_change_watcher.ts:
--------------------------------------------------------------------------------
1 | import { SerializedNode } from '@splootcode/core'
2 |
3 | export type FileSpec = SplootFile | BlobFile
4 | export interface SplootFile {
5 | type: 'sploot'
6 | content: SerializedNode
7 | }
8 |
9 | export interface BlobFile {
10 | type: 'blob'
11 | content: Uint8Array
12 | }
13 |
14 | export interface FileChangeWatcher {
15 | registerObservers: (setDirty: () => void) => void
16 | deregisterObservers: () => void
17 | isValid: () => boolean
18 | getUpdatedFileState: () => Promise