Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding reactive html export #1360

Merged
merged 9 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
144 changes: 140 additions & 4 deletions marimo/_islands/island_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
from typing import TYPE_CHECKING, 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
from marimo._output.formatting import as_html
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._utils.marimo_path import MarimoPath

if TYPE_CHECKING:
from marimo._server.session.session_view import SessionView
Expand Down Expand Up @@ -178,6 +182,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(
gvarnavi marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -230,9 +270,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")

Expand All @@ -257,6 +294,11 @@ def render_head(
"""
Render the header for the app.
This should be included in the <head> 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:
Expand Down Expand Up @@ -316,6 +358,100 @@ def render_head(
"""
).strip()

def render_body(
gvarnavi marked this conversation as resolved.
Show resolved Hide resolved
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 <body> 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"""
<div style="{style}">
{body}
</div>
"""
).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"""<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title> {title} </title>
{head}
</head>
<body>
{body}
</body>
</html>
"""
).strip()


def remove_empty_lines(text: str) -> str:
return "\n".join([line for line in text.split("\n") if line.strip() != ""])
Expand Down
18 changes: 18 additions & 0 deletions marimo/_server/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ 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]:
import os

from marimo._islands.island_generator import MarimoIslandGenerator

generator = MarimoIslandGenerator.from_file(
path.absolute_name, display_code=include_code
)
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


async def run_app_until_completion(
file_manager: AppFileManager,
cli_args: SerializedCLIArgs,
Expand Down