From 257eaa2d350245f84675b795e3b46e92cfe6b567 Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Sat, 11 May 2024 12:39:18 -0700 Subject: [PATCH 1/8] adding reactive_html export --- marimo/_cli/export/commands.py | 24 +++++++++++--- marimo/_server/export/__init__.py | 52 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index d53c5a85c2..4a719f4419 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -12,6 +12,7 @@ export_as_md, export_as_script, run_app_then_export_as_html, + run_app_then_export_as_reactive_html, ) from marimo._server.utils import asyncio_run from marimo._utils.file_watcher import FileWatcher @@ -101,6 +102,13 @@ async def start() -> None: type=bool, help="Include notebook code in the exported HTML file.", ) +@click.option( + "--reactive/--no-reactive", + default=False, + show_default=True, + type=bool, + help="Whether to export react HTML file, using marimo islands.", +) @click.option( "--watch/--no-watch", default=False, @@ -127,6 +135,7 @@ async def start() -> None: def html( name: str, include_code: bool, + reactive: bool, output: str, watch: bool, args: tuple[str], @@ -138,11 +147,18 @@ def html( cli_args = parse_args(args) def export_callback(file_path: MarimoPath) -> str: - (html, _filename) = asyncio_run( - run_app_then_export_as_html( - file_path, include_code=include_code, cli_args=cli_args + if reactive: + (html, _filename) = asyncio_run( + run_app_then_export_as_reactive_html( + file_path, include_code=include_code + ) + ) + else: + (html, _filename) = asyncio_run( + run_app_then_export_as_html( + file_path, include_code=include_code, cli_args=cli_args + ) ) - ) return html return watch_and_export(MarimoPath(name), output, watch, export_callback) diff --git a/marimo/_server/export/__init__.py b/marimo/_server/export/__init__.py index 9fc8ee972d..6fc41b4413 100644 --- a/marimo/_server/export/__init__.py +++ b/marimo/_server/export/__init__.py @@ -67,6 +67,58 @@ async def run_app_then_export_as_html( return html, filename +async def run_app_then_export_as_reactive_html( + path: MarimoPath, + include_code: bool, +) -> tuple[str, str]: + from marimo._islands.island_generator import MarimoIslandGenerator + from marimo._server.export.utils import get_download_filename + + # Create a file router and file manager + file_router = AppFileRouter.from_filename(path) + file_key = file_router.get_unique_file_key() + assert file_key is not None + file_manager = file_router.get_file_manager(file_key) + + # Create Marimo islands + stubs = [] + generator = MarimoIslandGenerator() + for cell_data in file_manager.app.cell_manager.cell_data(): + stubs.append( + generator.add_code( + cell_data.code, + display_code=include_code, + ) + ) + + island_file_manager = AppFileManager.from_app(generator._app) + session_view = await run_app_until_completion( + island_file_manager, cli_args={} + ) + + rendered_stubs = [] + for stub in stubs: + stub._internal_app = generator._app + stub._session_view = session_view + rendered_stubs.append(stub.render()) + + head = generator.render_head() + body = "\n".join(rendered_stubs) + html = f""" + + + {head} + + + {body} + + + """ + + filename = get_download_filename(file_manager, ".html") + return html, filename + + async def run_app_until_completion( file_manager: AppFileManager, cli_args: SerializedCLIArgs, From c7484e49f2c3c1e35731ad0c8046d2d9dd3a50ce Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Tue, 14 May 2024 09:26:03 -0700 Subject: [PATCH 2/8] doctype to avoid quirks mode --- marimo/_server/export/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/marimo/_server/export/__init__.py b/marimo/_server/export/__init__.py index 6fc41b4413..6cf8ccf7e2 100644 --- a/marimo/_server/export/__init__.py +++ b/marimo/_server/export/__init__.py @@ -72,6 +72,7 @@ async def run_app_then_export_as_reactive_html( include_code: bool, ) -> tuple[str, str]: from marimo._islands.island_generator import MarimoIslandGenerator + from marimo._server.api.utils import parse_title from marimo._server.export.utils import get_download_filename # Create a file router and file manager @@ -91,22 +92,31 @@ async def run_app_then_export_as_reactive_html( ) ) - island_file_manager = AppFileManager.from_app(generator._app) + app = generator._app + island_file_manager = AppFileManager.from_app(app) session_view = await run_app_until_completion( island_file_manager, cli_args={} ) rendered_stubs = [] for stub in stubs: - stub._internal_app = generator._app + stub._internal_app = app stub._session_view = session_view rendered_stubs.append(stub.render()) head = generator.render_head() + title = ( + parse_title(str(path.path)) + if app.config.app_title is None + else app.config.app_title + ) body = "\n".join(rendered_stubs) - html = f""" - + + html = f""" + + + {title} {head} From bfc58625c76667119389d213a76353f34a87fb8f Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Tue, 14 May 2024 10:47:25 -0700 Subject: [PATCH 3/8] respect config width --- marimo/_server/export/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/marimo/_server/export/__init__.py b/marimo/_server/export/__init__.py index 6cf8ccf7e2..2b28c38569 100644 --- a/marimo/_server/export/__init__.py +++ b/marimo/_server/export/__init__.py @@ -105,12 +105,16 @@ async def run_app_then_export_as_reactive_html( rendered_stubs.append(stub.render()) head = generator.render_head() + body = "\n".join(rendered_stubs) + title = ( parse_title(str(path.path)) if app.config.app_title is None else app.config.app_title ) - body = "\n".join(rendered_stubs) + + max_width_dict = {"normal": "740px;", "medium": "1110px;", "full": "none;"} + max_width = max_width_dict[file_manager.app.config.width] html = f""" @@ -120,7 +124,9 @@ async def run_app_then_export_as_reactive_html( {head} +
{body} +
""" From 83acc9b54cc2d90da212f5057c98d82f5d823e55 Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Tue, 14 May 2024 10:57:42 -0700 Subject: [PATCH 4/8] clarified help message --- marimo/_cli/export/commands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index 4a719f4419..b93028d4e7 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -107,7 +107,10 @@ async def start() -> None: default=False, show_default=True, type=bool, - help="Whether to export react HTML file, using marimo islands.", + help=""" + Whether to export a reactive HTML file. + If enabled, the exported HTML will run Python code using a Pyodide kernel. + """, ) @click.option( "--watch/--no-watch", From a88cedc1d9b1d4c8a81522114e8a6b592a8d646e Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Thu, 16 May 2024 20:48:42 -0700 Subject: [PATCH 5/8] islands from_file static method --- marimo/_islands/island_generator.py | 144 +++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/marimo/_islands/island_generator.py b/marimo/_islands/island_generator.py index 5466723b8b..e3fa51fbc1 100644 --- a/marimo/_islands/island_generator.py +++ b/marimo/_islands/island_generator.py @@ -6,7 +6,7 @@ from typing import List, Optional, Union, cast from marimo import __version__, _loggers -from marimo._ast.app import App, InternalApp +from marimo._ast.app import App, InternalApp, _AppConfig from marimo._ast.cell import Cell, CellConfig from marimo._ast.compiler import compile_cell from marimo._messaging.cell_output import CellOutput @@ -14,7 +14,11 @@ from marimo._output.utils import uri_encode_component from marimo._plugins.stateless.json_output import json_output from marimo._plugins.ui import code_editor +from marimo._server.export import run_app_until_completion +from marimo._server.file_manager import AppFileManager +from marimo._server.file_router import AppFileRouter from marimo._server.session.session_view import SessionView +from marimo._utils.marimo_path import MarimoPath LOGGER = _loggers.marimo_logger() @@ -175,6 +179,42 @@ def __init__(self, app_id: str = "main"): self._app_id = app_id self._app = InternalApp(App()) self._stubs: List[MarimoIslandStub] = [] + self._config = _AppConfig() + + @staticmethod + def from_file( + filename: str, + display_code: bool = False, + ) -> MarimoIslandGenerator: + """ + Create a MarimoIslandGenerator and populate MarimoIslandStubs + using code cells from a marimo *.py file. + + *Args:* + + - filename (str): Marimo .py filename to convert to reactive HTML. + - display_code (bool): Whether to display the code in HTML snippets. + """ + path = MarimoPath(filename) + file_router = AppFileRouter.from_filename(path) + file_key = file_router.get_unique_file_key() + assert file_key is not None + file_manager = file_router.get_file_manager(file_key) + + generator = MarimoIslandGenerator() + stubs = [] + for cell_data in file_manager.app.cell_manager.cell_data(): + stubs.append( + generator.add_code( + cell_data.code, + display_code=display_code, + ) + ) + + generator._stubs = stubs + generator._config = file_manager.app.config + + return generator def add_code( self, @@ -227,9 +267,6 @@ async def build(self) -> App: - App: The built app. """ - from marimo._server.export import run_app_until_completion - from marimo._server.file_manager import AppFileManager - if self.has_run: raise ValueError("You can only call build() once") @@ -254,6 +291,11 @@ def render_head( """ Render the header for the app. This should be included in the tag of the page. + + *Args:* + + - version_override (str): Marimo version to use for loaded js/css. + - _development_url (str): If True, uses local marimo islands js. """ # This loads: @@ -313,6 +355,100 @@ def render_head( """ ).strip() + def render_body( + self, + *, + max_width: Optional[str] = None, + margin: Optional[str] = None, + style: Optional[str] = None, + ) -> str: + """ + Render the body for the app. + This should be included in the tag of the page. + + *Args:* + + - max_width (str): CSS style max_width property. + - margin (str): CSS style margin property. + - style (str): CSS style. Overrides max_width and margin. + """ + + rendered_stubs = [] + for stub in self._stubs: + rendered_stubs.append(stub.render()) + + body = "\n".join(rendered_stubs) + + if margin is None: + margin = "auto;" + if max_width is None: + width = self._config.width + if width == "normal": + max_width = "740px;" + elif width == "medium": + max_width = "1110px;" + else: + max_width = "none;" + + if style is None: + style = f"margin: {margin} max-width: {max_width}" + + return dedent( + f""" +
+ {body} +
+ """ + ).strip() + + def render_html( + self, + *, + version_override: str = __version__, + _development_url: Union[str | bool] = False, + max_width: Optional[str] = None, + margin: Optional[str] = None, + style: Optional[str] = None, + ) -> str: + """ + Render reactive html for the app. + + *Args:* + + - version_override (str): Marimo version to use for loaded js/css. + - _development_url (str): If True, uses local marimo islands js. + - max_width (str): CSS style max_width property. + - margin (str): CSS style margin property. + - style (str): CSS style. Overrides max_width and margin. + """ + head = self.render_head( + version_override=version_override, + _development_url=_development_url, + ) + body = self.render_body( + max_width=max_width, margin=margin, style=style + ) + title = ( + self._app_id + if self._config.app_title is None + else self._config.app_title + ) + + return dedent( + f""" + + + + {title} + {head} + + + {body} + + + """ + ).strip() + def remove_empty_lines(text: str) -> str: return "\n".join([line for line in text.split("\n") if line.strip() != ""]) From d2e073773d44b91afdf16215c10b92c9dd90dda6 Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Thu, 16 May 2024 20:49:18 -0700 Subject: [PATCH 6/8] switching cli export to static method --- marimo/_server/export/__init__.py | 66 ++++--------------------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/marimo/_server/export/__init__.py b/marimo/_server/export/__init__.py index 2b28c38569..e6e264d40e 100644 --- a/marimo/_server/export/__init__.py +++ b/marimo/_server/export/__init__.py @@ -71,67 +71,17 @@ async def run_app_then_export_as_reactive_html( path: MarimoPath, include_code: bool, ) -> tuple[str, str]: - from marimo._islands.island_generator import MarimoIslandGenerator - from marimo._server.api.utils import parse_title - from marimo._server.export.utils import get_download_filename - - # Create a file router and file manager - file_router = AppFileRouter.from_filename(path) - file_key = file_router.get_unique_file_key() - assert file_key is not None - file_manager = file_router.get_file_manager(file_key) - - # Create Marimo islands - stubs = [] - generator = MarimoIslandGenerator() - for cell_data in file_manager.app.cell_manager.cell_data(): - stubs.append( - generator.add_code( - cell_data.code, - display_code=include_code, - ) - ) - - app = generator._app - island_file_manager = AppFileManager.from_app(app) - session_view = await run_app_until_completion( - island_file_manager, cli_args={} - ) + import os - rendered_stubs = [] - for stub in stubs: - stub._internal_app = app - stub._session_view = session_view - rendered_stubs.append(stub.render()) - - head = generator.render_head() - body = "\n".join(rendered_stubs) + from marimo._islands.island_generator import MarimoIslandGenerator - title = ( - parse_title(str(path.path)) - if app.config.app_title is None - else app.config.app_title + generator = MarimoIslandGenerator.from_file( + path.absolute_name, display_code=include_code ) - - max_width_dict = {"normal": "740px;", "medium": "1110px;", "full": "none;"} - max_width = max_width_dict[file_manager.app.config.width] - - html = f""" - - - - {title} - {head} - - -
- {body} -
- - - """ - - filename = get_download_filename(file_manager, ".html") + await generator.build() + html = generator.render_html() + basename = os.path.basename(path.absolute_name) + filename = f"{os.path.splitext(basename)[0]}.html" return html, filename From 0170e7a411a06563e88d71757f1f4bb3d6e521c9 Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Fri, 17 May 2024 18:27:56 -0700 Subject: [PATCH 7/8] adding ; between styles --- marimo/_islands/island_generator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/marimo/_islands/island_generator.py b/marimo/_islands/island_generator.py index e3fa51fbc1..93c395f4ed 100644 --- a/marimo/_islands/island_generator.py +++ b/marimo/_islands/island_generator.py @@ -380,18 +380,18 @@ def render_body( body = "\n".join(rendered_stubs) if margin is None: - margin = "auto;" + margin = "auto" if max_width is None: width = self._config.width if width == "normal": - max_width = "740px;" + max_width = "740px" elif width == "medium": - max_width = "1110px;" + max_width = "1110px" else: - max_width = "none;" + max_width = "none" if style is None: - style = f"margin: {margin} max-width: {max_width}" + style = f"margin: {margin}; max-width: {max_width};" return dedent( f""" From a0d2963293434058173991d7fde8c1a114298b8f Mon Sep 17 00:00:00 2001 From: gvarnavi Date: Fri, 17 May 2024 18:29:48 -0700 Subject: [PATCH 8/8] removing exposing cli option --- marimo/_cli/export/commands.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index b93028d4e7..d53c5a85c2 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -12,7 +12,6 @@ export_as_md, export_as_script, run_app_then_export_as_html, - run_app_then_export_as_reactive_html, ) from marimo._server.utils import asyncio_run from marimo._utils.file_watcher import FileWatcher @@ -102,16 +101,6 @@ async def start() -> None: type=bool, help="Include notebook code in the exported HTML file.", ) -@click.option( - "--reactive/--no-reactive", - default=False, - show_default=True, - type=bool, - help=""" - Whether to export a reactive HTML file. - If enabled, the exported HTML will run Python code using a Pyodide kernel. - """, -) @click.option( "--watch/--no-watch", default=False, @@ -138,7 +127,6 @@ async def start() -> None: def html( name: str, include_code: bool, - reactive: bool, output: str, watch: bool, args: tuple[str], @@ -150,18 +138,11 @@ def html( cli_args = parse_args(args) def export_callback(file_path: MarimoPath) -> str: - if reactive: - (html, _filename) = asyncio_run( - run_app_then_export_as_reactive_html( - file_path, include_code=include_code - ) - ) - else: - (html, _filename) = asyncio_run( - run_app_then_export_as_html( - file_path, include_code=include_code, cli_args=cli_args - ) + (html, _filename) = asyncio_run( + run_app_then_export_as_html( + file_path, include_code=include_code, cli_args=cli_args ) + ) return html return watch_and_export(MarimoPath(name), output, watch, export_callback)