From c74e1862e73304d409c28a5e3f1de22c92a77623 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 17:30:57 -0400 Subject: [PATCH 01/36] Install 'mcp' extras in docs workflow --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7e46a342..92e56fef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e . + python -m pip install -e ".[mcp]" python -m pip install jupyter polars pandas pyarrow plotnine - name: Set up Quarto From 3b3ec60abe70a686be22451087441aaf3a9821a0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 17:31:15 -0400 Subject: [PATCH 02/36] Enable Marimo notebooks in docs config --- great-docs.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/great-docs.yml b/great-docs.yml index aa00e871..2d88c77c 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -180,6 +180,11 @@ sidebar_filter: enabled: true min_items: 1 # Set low for testing (default: 20) +# Marimo Notebooks +# ---------------- +# Enable interactive WASM notebooks via marimo islands +marimo: true + # Site Settings # ------------- # Configure page metadata timestamps and other site-wide options From a0662b6654852337a575b1e9da72fc961ffa61f8 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 17:31:29 -0400 Subject: [PATCH 03/36] Add Marimo islands CSS and JS assets --- great_docs/assets/marimo-islands.css | 157 +++++++++++++++++++++++++++ great_docs/assets/marimo-islands.js | 89 +++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 great_docs/assets/marimo-islands.css create mode 100644 great_docs/assets/marimo-islands.js diff --git a/great_docs/assets/marimo-islands.css b/great_docs/assets/marimo-islands.css new file mode 100644 index 00000000..f3a82f67 --- /dev/null +++ b/great_docs/assets/marimo-islands.css @@ -0,0 +1,157 @@ +/* marimo-islands.css — Styling for embedded Marimo island notebooks. */ + +/* Island group container */ +.gd-marimo-island-group { + margin: 1.5rem 0; + border: 1px solid var(--bs-border-color, #dee2e6); + border-radius: 0.5rem; + overflow: hidden; + background: var(--bs-body-bg, #fff); +} + +/* Individual island cells */ +.gd-marimo-island-group marimo-island { + display: block; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); +} + +.gd-marimo-island-group marimo-island:last-of-type { + border-bottom: none; +} + +/* Cell code styling (our visible code block) */ +.gd-marimo-code { + display: block; + font-family: var(--bs-font-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace); + font-size: 0.875rem; + line-height: 1.5; + padding: 0.75rem 1rem; + margin: 0; + background: var(--bs-tertiary-bg, #f8f9fa); + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; +} + +.gd-marimo-code code { + font-size: inherit; + color: inherit; + background: none; + padding: 0; +} + +/* Hide the raw marimo-cell-code (runtime uses it internally) */ +marimo-cell-code { + display: none; +} + +/* Cell output area */ +marimo-cell-output { + display: block; + min-height: 1.5rem; +} + +/* Loading placeholder */ +.gd-marimo-loading { + color: var(--bs-secondary-color, #6c757d); + font-size: 0.85rem; + font-style: italic; + padding: 0.25rem 0; +} + +/* Active state (after Pyodide boots) */ +.gd-marimo-island-group.gd-marimo-active .gd-marimo-loading { + display: none; +} + +/* Copy notebook button area */ +.gd-marimo-copy { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 1rem; + background: var(--bs-tertiary-bg, #f8f9fa); + border-top: 1px solid var(--bs-border-color, #dee2e6); +} + +.gd-marimo-copy-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.65rem; + font-size: 0.8rem; + font-weight: 500; + color: var(--bs-body-color, #212529); + background: var(--bs-body-bg, #fff); + border: 1px solid var(--bs-border-color, #dee2e6); + border-radius: 0.375rem; + cursor: pointer; + transition: border-color 0.15s, background-color 0.15s; +} + +.gd-marimo-copy-btn:hover { + background: var(--bs-secondary-bg, #e9ecef); + border-color: var(--bs-secondary, #6c757d); +} + +.gd-marimo-copy-btn.gd-marimo-copied { + color: var(--bs-success, #198754); + border-color: var(--bs-success, #198754); +} + +.gd-marimo-copy-hint { + font-size: 0.75rem; + color: var(--bs-secondary-color, #6c757d); +} + +.gd-marimo-copy-hint code { + font-size: 0.75rem; + padding: 0.1rem 0.3rem; + background: var(--bs-body-bg, #fff); + border-radius: 0.2rem; +} + +/* Error state */ +.gd-marimo-error { + padding: 1rem; + color: var(--bs-danger, #dc3545); + font-style: italic; + border: 1px dashed var(--bs-danger, #dc3545); + border-radius: 0.5rem; + margin: 1rem 0; +} + +/* Iframe mode */ +.gd-marimo-iframe-wrap { + margin: 1.5rem 0; + border-radius: 0.5rem; + overflow: hidden; + border: 1px solid var(--bs-border-color, #dee2e6); +} + +.gd-marimo-iframe { + display: block; + border: none; + width: 100%; +} + +/* Dark mode adjustments */ +[data-bs-theme="dark"] .gd-marimo-island-group { + border-color: var(--bs-border-color); +} + +[data-bs-theme="dark"] marimo-cell-code { + background: var(--bs-tertiary-bg); +} + +[data-bs-theme="dark"] .gd-marimo-copy { + background: var(--bs-tertiary-bg); +} + +[data-bs-theme="dark"] .gd-marimo-copy-btn { + background: var(--bs-body-bg); + border-color: var(--bs-border-color); + color: var(--bs-body-color); +} diff --git a/great_docs/assets/marimo-islands.js b/great_docs/assets/marimo-islands.js new file mode 100644 index 00000000..11b0912b --- /dev/null +++ b/great_docs/assets/marimo-islands.js @@ -0,0 +1,89 @@ +/** + * marimo-islands.js — Lazy-loads marimo islands and handles copy-notebook. + * + * Attached to pages that use the {{< marimo >}} shortcode. + * - Uses IntersectionObserver to defer Pyodide boot until islands scroll into view. + * - Provides "Copy Notebook" button handler. + */ +(function () { + "use strict"; + + // --- Copy Notebook Handler --- + function initCopyButtons() { + document.querySelectorAll(".gd-marimo-copy-btn").forEach(function (btn) { + btn.addEventListener("click", function () { + var group = btn.closest(".gd-marimo-island-group"); + if (!group) return; + var sourceEl = group.querySelector("script.gd-marimo-source"); + if (!sourceEl) return; + + var text = sourceEl.textContent; + navigator.clipboard.writeText(text).then(function () { + var original = btn.innerHTML; + btn.innerHTML = + ' Copied!'; + btn.classList.add("gd-marimo-copied"); + setTimeout(function () { + btn.innerHTML = original; + btn.classList.remove("gd-marimo-copied"); + }, 2000); + }); + }); + }); + } + + // --- Lazy-Load Islands Runtime --- + var runtimeLoaded = false; + + function loadIslandsRuntime() { + if (runtimeLoaded) return; + runtimeLoaded = true; + + // The actual marimo islands JS/CSS is loaded via tags injected by + // the build pipeline. Once those are present, the custom elements + // () self-initialize. This function just marks that we've + // triggered observation. The CDN script handles the rest. + document.querySelectorAll(".gd-marimo-island-group").forEach(function (group) { + group.classList.add("gd-marimo-active"); + }); + } + + function initLazyLoad() { + var groups = document.querySelectorAll(".gd-marimo-island-group"); + if (groups.length === 0) return; + + if (!("IntersectionObserver" in window)) { + // Fallback: load immediately + loadIslandsRuntime(); + return; + } + + var observer = new IntersectionObserver( + function (entries) { + for (var i = 0; i < entries.length; i++) { + if (entries[i].isIntersecting) { + loadIslandsRuntime(); + observer.disconnect(); + return; + } + } + }, + { rootMargin: "200px" } + ); + + groups.forEach(function (group) { + observer.observe(group); + }); + } + + // --- Init --- + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", function () { + initCopyButtons(); + initLazyLoad(); + }); + } else { + initCopyButtons(); + initLazyLoad(); + } +})(); From 22188bc3a8022552c1891493e322e077cfcc086e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 18:41:41 -0400 Subject: [PATCH 04/36] Add Marimo notebook integration utilities --- great_docs/_marimo.py | 161 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 great_docs/_marimo.py diff --git a/great_docs/_marimo.py b/great_docs/_marimo.py new file mode 100644 index 00000000..a349278a --- /dev/null +++ b/great_docs/_marimo.py @@ -0,0 +1,161 @@ +"""Marimo notebook integration for Great Docs. + +Provides utilities for generating marimo island HTML at build time +using MarimoIslandGenerator, and supporting the marimo Quarto shortcode. +""" + +from __future__ import annotations + +import asyncio +import re +import sys +from pathlib import Path + +# Marimo islands CDN base +_ISLANDS_CDN = "https://cdn.jsdelivr.net/npm/@marimo-team/islands" +_DEFAULT_VERSION = "0.23.8" + + +def get_islands_head_html(version: str = _DEFAULT_VERSION) -> str: + """Return the \n' + f'\n' + '\n' + '\n' + '' + ) + + +def generate_islands_html( + notebook_path: Path, + *, + display_code: bool = True, + reactive: bool = True, +) -> str: + """Generate marimo island HTML from a notebook file. + + Uses MarimoIslandGenerator to produce correct island markup + that the @marimo-team/islands runtime can activate. + + Parameters + ---------- + notebook_path + Path to the .py marimo notebook file. + display_code + Whether to show cell source code. + reactive + Whether cells should be reactive (run with Pyodide in browser). + + Returns + ------- + str + HTML string containing elements. + """ + import io + + from marimo import MarimoIslandGenerator + + gen = MarimoIslandGenerator.from_file(str(notebook_path), display_code=display_code) + + # Build the app (runs cells to capture output; errors are non-fatal) + # Redirect stdout/stderr during build to avoid marimo writing to + # wrapped streams that might lack attributes + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout = io.TextIOWrapper(io.BytesIO(), encoding="utf-8") + sys.stderr = io.TextIOWrapper(io.BytesIO(), encoding="utf-8") + try: + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(gen.build()) + finally: + loop.close() + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + # Render body HTML (the islands themselves) + body_html = gen.render_body( + include_init_island=False, + max_width="100%", + ) + + # Ensure data-reactive matches the requested mode + if not reactive: + body_html = body_html.replace('data-reactive="true"', 'data-reactive="false"') + + return body_html + + +def generate_islands_for_build( + notebook_path: Path, + output_path: Path, + *, + display_code: bool = True, + reactive: bool = True, +) -> None: + """Pre-generate island HTML and save to a file for the Lua shortcode to read. + + Parameters + ---------- + notebook_path + Path to the .py marimo notebook. + output_path + Path to write the generated HTML fragment. + display_code + Whether to show cell source code. + reactive + Whether cells should be reactive. + """ + html = generate_islands_html( + notebook_path, + display_code=display_code, + reactive=reactive, + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(html, encoding="utf-8") + + +def parse_marimo_source(source: str) -> list[dict[str, str]]: + """Parse marimo notebook source text into cells (for fallback/testing).""" + cells: list[dict[str, str]] = [] + + cell_pattern = re.compile( + r"@app\.cell(?:\([^)]*\))?\s*\n" + r"def\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*(?:->[^:]*)?:\s*\n" + r"((?:(?: .*)?\n)*)", + re.MULTILINE, + ) + + for match in cell_pattern.finditer(source): + name = match.group(1) + body = match.group(2) + + lines = body.split("\n") + dedented = [] + for line in lines: + if line.startswith(" "): + dedented.append(line[4:]) + elif line.strip() == "": + dedented.append("") + else: + dedented.append(line) + + while dedented and dedented[-1].strip() == "": + dedented.pop() + if dedented and dedented[-1].strip().startswith("return"): + dedented.pop() + while dedented and dedented[-1].strip() == "": + dedented.pop() + + code = "\n".join(dedented) + if code.strip(): + cells.append({"code": code, "name": name}) + + return cells + + +def notebook_source(path: Path) -> str: + """Return raw notebook source for copy-to-clipboard.""" + return path.read_text(encoding="utf-8") From 0583c0d69af3c5cb2d0d97ed383ec36b04f0831f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 18:42:04 -0400 Subject: [PATCH 05/36] Add Marimo shortcode extension --- .../assets/_extensions/marimo/_extension.yml | 7 + .../assets/_extensions/marimo/marimo.lua | 134 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 great_docs/assets/_extensions/marimo/_extension.yml create mode 100644 great_docs/assets/_extensions/marimo/marimo.lua diff --git a/great_docs/assets/_extensions/marimo/_extension.yml b/great_docs/assets/_extensions/marimo/_extension.yml new file mode 100644 index 00000000..930cd61b --- /dev/null +++ b/great_docs/assets/_extensions/marimo/_extension.yml @@ -0,0 +1,7 @@ +title: Marimo Islands +author: Great Docs +version: 1.0.0 +quarto-required: ">=1.3.0" +contributes: + shortcodes: + - marimo.lua diff --git a/great_docs/assets/_extensions/marimo/marimo.lua b/great_docs/assets/_extensions/marimo/marimo.lua new file mode 100644 index 00000000..03bf8e07 --- /dev/null +++ b/great_docs/assets/_extensions/marimo/marimo.lua @@ -0,0 +1,134 @@ +-- marimo.lua — Quarto shortcode for embedding Marimo notebooks as WASM islands. +-- +-- Usage in .qmd files: +-- +-- {{< marimo file="notebooks/gt-basics.py" >}} +-- +-- {{< marimo file="notebooks/gt-basics.py" show-code="false" >}} +-- +-- {{< marimo file="notebooks/gt-basics.py" mode="iframe" height="600px" >}} +-- +-- Options: +-- file (required) Path to .py marimo notebook relative to project root +-- mode "island" (default), "iframe" +-- show-copy "true"/"false" — show Copy Notebook button (default: true) +-- theme "auto"/"light"/"dark" — color theme (default: auto) +-- height CSS height for iframe mode (default: 600px) +-- +-- Island mode uses pre-generated HTML from MarimoIslandGenerator (built +-- during the Great Docs build step). The HTML is read from +-- _marimo_islands/.html. + +local function escape_html(s) + if s == nil then return "" end + return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """)) +end + +local function kwarg(kwargs, key, default) + local raw = kwargs[key] + if raw == nil then return default end + local s = pandoc.utils.stringify(raw) + if s == "" then return default end + return s +end + +--- Read a file relative to the Quarto project root. +local function read_project_file(rel_path) + local base = "" + if quarto and quarto.project and quarto.project.directory then + base = quarto.project.directory .. "/" + end + local path = base .. rel_path + local f = io.open(path, "r") + if not f then return nil end + local content = f:read("*a") + f:close() + return content +end + +return { + ["marimo"] = function(args, kwargs, meta) + -- Get file path (required) + local file = kwarg(kwargs, "file", "") + if file == "" and #args > 0 then + file = pandoc.utils.stringify(args[1]) + end + if file == "" then + quarto.log.warning("[marimo] 'file' attribute is required") + return pandoc.Null() + end + + -- Read options + local mode = kwarg(kwargs, "mode", "island") + local show_copy = kwarg(kwargs, "show-copy", "true") + local theme = kwarg(kwargs, "theme", "auto") + local height = kwarg(kwargs, "height", "600px") + + -- IFRAME MODE -------------------------------------------------------- + if mode == "iframe" then + local offset = "" + if quarto and quarto.project and quarto.project.offset then + offset = quarto.project.offset .. "/" + end + local wasm_path = file:gsub("%.py$", "") .. "/index.html" + local parts = {} + table.insert(parts, '
') + table.insert(parts, '') + table.insert(parts, '
') + return pandoc.RawInline("html", table.concat(parts)) + end + + -- ISLAND MODE -------------------------------------------------------- + -- Read pre-generated island HTML from _marimo_islands/.html + local stem = file:match("([^/]+)%.py$") + if not stem then + quarto.log.warning("[marimo] Cannot determine notebook stem from: " .. file) + return pandoc.RawInline("html", + '
Invalid notebook path: ' + .. escape_html(file) .. '
') + end + + local island_html = read_project_file("_marimo_islands/" .. stem .. ".html") + if not island_html then + quarto.log.warning("[marimo] Pre-generated island HTML not found for: " .. stem) + return pandoc.RawInline("html", + '
Island HTML not generated for: ' + .. escape_html(file) .. '
') + end + + local parts = {} + table.insert(parts, '
') + table.insert(parts, island_html) + + -- Copy notebook button + if show_copy == "true" then + local source = read_project_file(file) + if source then + local escaped_source = escape_html(source) + table.insert(parts, + '\n ') + table.insert(parts, '\n
') + table.insert(parts, '') + table.insert(parts, + 'Run locally: marimo edit ' .. + escape_html(file:match("[^/]+$") or file) .. '') + table.insert(parts, '
') + end + end + + table.insert(parts, '\n
') + + return pandoc.RawInline("html", table.concat(parts)) + end +} From bc89e5b32030650bc6b7c7e391b042c21a01b2e7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 18:42:32 -0400 Subject: [PATCH 06/36] Add marimo islands config and helpers --- great_docs/config.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/great_docs/config.py b/great_docs/config.py index ac775b6f..32448d1b 100644 --- a/great_docs/config.py +++ b/great_docs/config.py @@ -215,6 +215,11 @@ # True (default): show back-to-top button on all pages # False: disable back-to-top button "back_to_top": True, + # Marimo notebook islands (interactive WASM notebooks embedded in pages) + # None/False: disabled (no marimo islands runtime loaded) + # True: enabled with default version + # dict: {"enabled": True, "version": "0.23.7", "lazy_load": True} + "marimo": None, # Attribution text in the footer ("Site created with Great Docs") # True (default): show attribution # False: hide attribution @@ -739,6 +744,22 @@ def back_to_top(self) -> bool: """Check if back-to-top button is enabled.""" return self.get("back_to_top", True) + @property + def marimo_enabled(self) -> bool: + """Check if marimo islands integration is enabled.""" + val = self.get("marimo") + if val is None or val is False: + return False + return True + + @property + def marimo_version(self) -> str: + """Get the @marimo-team/islands CDN version.""" + val = self.get("marimo") + if isinstance(val, dict): + return val.get("version", "0.23.8") + return "0.23.8" + @property def markdown_pages(self) -> bool: """Check if Markdown page generation is enabled.""" From 42e3c14eb1bed1cd983156bc38336465ec501f9c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 19:00:30 -0400 Subject: [PATCH 07/36] Add gt-basics notebook demonstrating Great Tables --- notebooks/gt-basics.py | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 notebooks/gt-basics.py diff --git a/notebooks/gt-basics.py b/notebooks/gt-basics.py new file mode 100644 index 00000000..eee149ce --- /dev/null +++ b/notebooks/gt-basics.py @@ -0,0 +1,97 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "marimo", +# "great-tables", +# "polars", +# ] +# /// + +import marimo + +app = marimo.App() + + +@app.cell +def __(): + import marimo as mo + + return (mo,) + + +@app.cell +async def __(): + import sys + + if "pyodide" in sys.modules: + import micropip + + await micropip.install(["great-tables", "polars"]) + return + + +@app.cell +def __(mo): + mo.md( + """ + # Getting Started with Great Tables + + This notebook demonstrates the basics of creating tables with **Great Tables**. + Edit the code below and see the output update reactively! + """ + ) + return + + +@app.cell +def __(): + import great_tables as gt + import polars as pl + + return gt, pl + + +@app.cell +def __(gt, pl): + # Create sample data + df = pl.DataFrame( + { + "name": ["Alice", "Bob", "Charlie", "Diana"], + "score": [95, 87, 92, 88], + "grade": ["A", "B+", "A-", "B+"], + } + ) + + # Build a Great Table + ( + gt.GT(df) + .tab_header( + title="Student Scores", + subtitle="Fall 2026 Semester", + ) + .cols_label( + name="Student", + score="Score", + grade="Grade", + ) + .data_color( + columns="score", + palette=["#fde725", "#21918c"], + ) + ) + return df + + +@app.cell +def __(mo): + mo.md( + """ + Try modifying the data or the table styling above — + the output will update automatically thanks to marimo's reactive execution. + """ + ) + return + + +if __name__ == "__main__": + app.run() From 1e20c21be990d39ff2938f9eb0bb794d3699928b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 19:00:52 -0400 Subject: [PATCH 08/36] Pre-generate Marimo islands from notebooks --- great_docs/core.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/great_docs/core.py b/great_docs/core.py index e42fb239..c8628b5d 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -289,6 +289,37 @@ def _prepare_build_directory(self) -> None: extensions_dst = self.project_path / "_extensions" shutil.copytree(extensions_src, extensions_dst, dirs_exist_ok=True) + # Copy notebooks directory and pre-generate marimo island HTML + if self._config.marimo_enabled: + notebooks_src = self.project_root / "notebooks" + if notebooks_src.exists() and notebooks_src.is_dir(): + notebooks_dst = self.project_path / "notebooks" + shutil.copytree(notebooks_src, notebooks_dst, dirs_exist_ok=True) + + # Pre-generate island HTML for each .py notebook + # Marimo's import accesses sys.stdout.encoding at class-def time, + # so ensure the stream has that attribute before importing. + import io as _io + import sys as _sys + + _orig_stdout = _sys.stdout + _orig_stderr = _sys.stderr + if not hasattr(_sys.stdout, "encoding"): + _sys.stdout = _io.TextIOWrapper(_io.BytesIO(), encoding="utf-8") + if not hasattr(_sys.stderr, "encoding"): + _sys.stderr = _io.TextIOWrapper(_io.BytesIO(), encoding="utf-8") + try: + from great_docs._marimo import generate_islands_for_build + + islands_dir = self.project_path / "_marimo_islands" + islands_dir.mkdir(exist_ok=True) + for nb_file in notebooks_src.glob("*.py"): + out_file = islands_dir / f"{nb_file.stem}.html" + generate_islands_for_build(nb_file, out_file, reactive=True) + finally: + _sys.stdout = _orig_stdout + _sys.stderr = _orig_stderr + # Copy JavaScript files js_files = [ "github-widget.js", From 8c752dbc8e6aac9d0dffed9af531f24c241ee301 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 19:01:09 -0400 Subject: [PATCH 09/36] Add optional Marimo Islands assets --- great_docs/core.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/great_docs/core.py b/great_docs/core.py index c8628b5d..fda6e0de 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -365,12 +365,21 @@ def _prepare_build_directory(self) -> None: js_files.append("skill-switcher.js") # termshow player is always available (lightweight, only activates if shortcode used) js_files.append("termshow.js") + # marimo islands (only when enabled) + if self._config.marimo_enabled: + js_files.append("marimo-islands.js") for js_file in js_files: js_src = self.assets_path / js_file if js_src.exists(): js_dst = self.project_path / js_file shutil.copy2(js_src, js_dst) + # Copy marimo CSS when enabled + if self._config.marimo_enabled: + marimo_css_src = self.assets_path / "marimo-islands.css" + if marimo_css_src.exists(): + shutil.copy2(marimo_css_src, self.project_path / "marimo-islands.css") + # Create .gitignore for the great-docs directory gitignore_content = """# Great Docs build directory # This directory is ephemeral and regenerated on each build @@ -10910,6 +10919,20 @@ def _update_quarto_config(self) -> None: if "termshow.css" not in config["project"]["resources"]: config["project"]["resources"].append("termshow.css") + # Add marimo islands resources when enabled + if self._config.marimo_enabled: + for marimo_res in ("marimo-islands.js", "marimo-islands.css"): + if marimo_res not in config["project"]["resources"]: + config["project"]["resources"].append(marimo_res) + # Include notebooks directory so .py files are available to the shortcode + notebooks_dir = self.project_path / "notebooks" + if notebooks_dir.exists() and notebooks_dir.is_dir(): + if "notebooks/**" not in config["project"]["resources"]: + config["project"]["resources"].append("notebooks/**") + # Include pre-generated island HTML fragments + if "_marimo_islands/**" not in config["project"]["resources"]: + config["project"]["resources"].append("_marimo_islands/**") + # Add assets directory to resources if it exists assets_dir = self.project_path / "assets" if assets_dir.exists() and assets_dir.is_dir(): From 549f565363fd45c2cb393907db16a85c29e3fe41 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 19:01:22 -0400 Subject: [PATCH 10/36] Add Marimo Islands runtime to HTML header --- great_docs/core.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/great_docs/core.py b/great_docs/core.py index fda6e0de..60e5e870 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -11032,6 +11032,55 @@ def _update_quarto_config(self) -> None: ): config["format"]["html"]["include-in-header"].append(tp_css_entry) + # Add marimo islands runtime (CDN JS/CSS) when enabled + if self._config.marimo_enabled: + marimo_version = self._config.marimo_version + marimo_entry = { + "text": ( + f'\n' + f'\n' + '' + ) + } + if not any( + "marimo-team/islands" in str(item) + for item in config["format"]["html"]["include-in-header"] + ): + config["format"]["html"]["include-in-header"].append(marimo_entry) + + # Add marimo-islands.js (lazy-load + copy handler) + marimo_js_entry = { + "text": ( + "" + ) + } + if not any( + "marimo-islands.js" in str(item) + for item in config["format"]["html"]["include-in-header"] + ): + config["format"]["html"]["include-in-header"].append(marimo_js_entry) + + # Add marimo-islands.css + marimo_css_entry = { + "text": ( + "" + ) + } + if not any( + "marimo-islands.css" in str(item) + for item in config["format"]["html"]["include-in-header"] + ): + config["format"]["html"]["include-in-header"].append(marimo_css_entry) + # Add website navigation if not present if "website" not in config: config["website"] = {} From e68ac2f94b1e2f5a9d4b08fa8d0c0c38bfcf0b33 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sat, 30 May 2026 19:08:52 -0400 Subject: [PATCH 11/36] Create 43-marimo-notebooks.qmd --- user_guide/43-marimo-notebooks.qmd | 238 +++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 user_guide/43-marimo-notebooks.qmd diff --git a/user_guide/43-marimo-notebooks.qmd b/user_guide/43-marimo-notebooks.qmd new file mode 100644 index 00000000..4471b762 --- /dev/null +++ b/user_guide/43-marimo-notebooks.qmd @@ -0,0 +1,238 @@ +--- +title: "Marimo Notebooks" +guide-section: "Site Content" +bread-crumbs: false +tags: [Content, Extensions, Interactive, Notebooks] +status: experimental +--- + +# Marimo Notebooks + +Great Docs can embed interactive [Marimo](https://marimo.io/) notebooks directly in your +documentation pages. Readers run code examples live in their browser (no installation required) +powered by WebAssembly (Pyodide). They can also copy the notebook source to run locally with +`marimo edit`. + +## How It Works + +Marimo notebooks are Python files with `@app.cell` decorators. When you use the `{{{< marimo >}}}` +shortcode, Great Docs: + +1. Reads the `.py` notebook at build time +2. Extracts each cell and emits `` HTML elements +3. On page load, the `@marimo-team/islands` runtime boots a Pyodide kernel in the browser +4. Cells become reactive, where editing one re-executes its dependents automatically + +This gives your readers a live coding environment embedded right in the documentation, +with zero backend infrastructure. + +## Quick Start + +Getting a live notebook onto your documentation page takes three steps: enable the feature, create a +notebook, and drop in a shortcode. Here's each step in detail. + +### 1. Enable marimo in `great-docs.yml` + +```yaml +marimo: true +``` + +Or with explicit version pinning: + +```yaml +marimo: + enabled: true + version: "0.23.7" +``` + +### 2. Create a notebook + +Marimo notebooks are standard Python files. Create one in a `notebooks/` directory (or anywhere in +your project): + +```bash +marimo edit --sandbox notebooks/my-demo.py +``` + +The `--sandbox` flag inlines package dependencies as [PEP 723](https://peps.python.org/pep-0723/) +metadata, ensuring they install automatically in the WASM environment. + +### 3. Embed with the shortcode + +In any `.qmd` page: + +```markdown +{{{{< marimo file="notebooks/my-demo.py" >}}}} +``` + +Once you render the site, the notebook cells appear inline with full interactivity. Readers can edit +code and see outputs update without leaving the page. + +## Example: Great Tables in Marimo + +Here's a complete notebook that builds a styled table with Great Tables. Each cell is +reactive and the output updates when you edit the code: + +{{< marimo file="notebooks/gt-basics.py" >}} + +::: {.callout-tip} +## Try it live + +Edit any cell above and press **Shift+Enter** to re-run. The table output updates +instantly thanks to Marimo's reactive execution model. +::: + +## Shortcode Options + +The `{{{< marimo >}}}` shortcode accepts several options that control how the notebook is +rendered and how readers interact with it. + +| Option | Default | Description | +|--------|---------|-------------| +| `file` | *(required)* | Path to the `.py` marimo notebook (relative to project root) | +| `mode` | `"island"` | `"island"` for inline WASM cells, `"iframe"` for full notebook embed | +| `show-code` | `"true"` | Show source code alongside output | +| `show-copy` | `"true"` | Display "Copy Notebook" button | +| `autorun` | `"true"` | Auto-execute cells when the page loads | +| `theme` | `"auto"` | Color theme: `"auto"`, `"light"`, or `"dark"` | +| `height` | `"600px"` | Height of the iframe (only for `mode="iframe"`) | + +### Island mode (default) + +```markdown +{{{{< marimo file="notebooks/gt-basics.py" >}}}} +``` + +Cells render inline in the page. The Pyodide runtime loads lazily (only when the notebook scrolls +into view) to avoid impacting initial page load. + +### Iframe mode + +```markdown +{{{{< marimo file="notebooks/gt-basics.py" mode="iframe" height="700px" >}}}} +``` + +Embeds the full Marimo notebook UI in a sandboxed iframe. Use this for complex notebooks +that benefit from the complete editor interface. + +### Hide code + +```markdown +{{{{< marimo file="notebooks/gt-basics.py" show-code="false" >}}}} +``` + +Shows only outputs, which is useful for dashboards or app-style presentations where the code is +secondary. + +These options can be combined freely. For instance, you might use `show-code="false"` with +`theme="dark"` for a polished demo, or `mode="iframe"` with a custom `height` for a full-featured +exploratory tool. + +## Writing WASM-Compatible Notebooks + +Because notebooks run in the browser via Pyodide, there are some differences from a standard +Python environment. Not all packages work in WebAssembly, and memory is limited. The guidelines +below will help you author notebooks that run reliably for your readers. + +### Supported packages + +All packages with **pure Python wheels** on PyPI work, plus many compiled packages bundled with +Pyodide: NumPy, SciPy, pandas, Polars, scikit-learn, Matplotlib, Plotly, and more. See +[Pyodide's package list](https://pyodide.org/en/stable/usage/packages-in-pyodide.html). + +### Best practices + +1. **Use `--sandbox`** when authoring: `marimo edit --sandbox notebook.py` +2. **Put `import marimo as mo` in its own cell**: this speeds up initial rendering +3. **Keep notebooks lightweight**: WASM has a 2 GB memory limit +4. **Use `mo.notebook_location()`** for data file paths that work both locally and in WASM +5. **Test in browser**: export with `marimo export html-wasm notebook.py -o test/` and + serve with `python -m http.server -d test/` + +### Including data files + +Place data files in a `public/` folder next to your notebook: + +``` +notebooks/ +├── gt-basics.py +└── public/ + └── sample-data.csv +``` + +Access them portably: + +```python +import marimo as mo + +path = mo.notebook_location() / "public" / "sample-data.csv" +``` + +Following these guidelines ensures your notebooks work consistently across local development +and the browser-based WASM environment your readers will use. + +## Copy Notebook + +Every embedded notebook includes a **Copy Notebook** button. Readers can copy the full `.py` +source and run it locally: + +```bash +# Install marimo +pip install marimo + +# Run the copied notebook +marimo edit gt-basics.py +``` + +The button copies the complete notebook source including the PEP 723 dependency metadata, +so `marimo edit --sandbox` will auto-install all required packages. + +## Performance Notes + +Interactive notebooks add weight to a page, but several optimizations keep the experience +smooth for readers. Here's what to expect: + +- **Pyodide weighs ~15 MB** on first load (cached by the browser afterward) +- **Lazy loading**: the WASM kernel only boots when the notebook scrolls into view +- **Single kernel per page**: multiple `{{{{< marimo >}}}}` shortcodes share one Pyodide instance +- **First execution takes 3–5 seconds** while packages install; subsequent runs are fast + +For pages where instant rendering matters, consider providing a static screenshot or +pre-rendered output above the interactive notebook as a visual anchor while Pyodide loads. + +## Limitations + +While Marimo notebooks offer a powerful interactive experience, there are some constraints +imposed by the browser-based execution environment. The table below summarizes the key +limitations and suggested workarounds. + +| Limitation | Workaround | +|------------|------------| +| Not all packages available | Stick to Pyodide-supported packages | +| 2 GB memory cap | Keep datasets small; use aggregated data for demos | +| No threading/multiprocessing | Use single-threaded patterns | +| ~15 MB initial download | Browser caches aggressively; lazy-load defers cost | +| No PDB debugging | Use `print()` or `mo.output` for debugging | + +For most documentation use cases (small datasets, pure-Python packages, and focused +examples) these limitations won't be an issue. Design your notebooks around concise, +self-contained demonstrations and the experience will be seamless. + +## Next Steps + +Now that you have interactive notebooks embedded in your documentation, here are some ways to +build on it: + +- **[Marimo docs](https://docs.marimo.io/)**: Learn about reactive programming, UI elements + (`mo.ui.slider`, `mo.ui.dropdown`), and layout tools to make your notebooks more interactive. +- **Add interactivity**: Use `mo.ui` widgets to let readers explore data with sliders, dropdowns, + and toggles; all reactive by default, so outputs update the moment a value changes. +- **Embed multiple notebooks**: Place several `{{{{< marimo >}}}}` shortcodes on a page to build + progressive tutorials where each section builds on the last. +- **Check WASM compatibility**: Run `marimo export html-wasm notebook.py -o test/` locally to + verify your notebook works in the browser before publishing. +- **[Marimo gallery](https://marimo.io/gallery)**: Browse examples of dashboards, explorable + explanations, and interactive tutorials you can adapt for your own documentation. + +With Marimo notebooks, your documentation goes beyond static code examples. Readers can experiment +with your package directly in the browser, building intuition through hands-on exploration. From 47715ce859ac4f3355757906199a6dc9318a20cd Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:20:14 -0400 Subject: [PATCH 12/36] Add app_id and hide empty islands when code hidden --- great_docs/_marimo.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/great_docs/_marimo.py b/great_docs/_marimo.py index a349278a..d2c40929 100644 --- a/great_docs/_marimo.py +++ b/great_docs/_marimo.py @@ -33,6 +33,7 @@ def generate_islands_html( *, display_code: bool = True, reactive: bool = True, + app_id: str | None = None, ) -> str: """Generate marimo island HTML from a notebook file. @@ -47,6 +48,9 @@ def generate_islands_html( Whether to show cell source code. reactive Whether cells should be reactive (run with Pyodide in browser). + app_id + Unique app identifier for namespacing islands on the same page. + Defaults to the notebook stem name. Returns ------- @@ -85,6 +89,22 @@ def generate_islands_html( if not reactive: body_html = body_html.replace('data-reactive="true"', 'data-reactive="false"') + # When hiding code, strip islands whose output is empty (utility cells) + if not display_code: + body_html = re.sub( + r"]*>\s*\s*\s*" + r"\s*(?:]*>.*?\s*)?" + r"\s*", + "", + body_html, + flags=re.DOTALL, + ) + + # Namespace islands with a unique app_id (defaults to notebook stem) + resolved_app_id = app_id or notebook_path.stem + if resolved_app_id != "main": + body_html = body_html.replace('data-app-id="main"', f'data-app-id="{resolved_app_id}"') + return body_html @@ -94,6 +114,7 @@ def generate_islands_for_build( *, display_code: bool = True, reactive: bool = True, + app_id: str | None = None, ) -> None: """Pre-generate island HTML and save to a file for the Lua shortcode to read. @@ -107,11 +128,15 @@ def generate_islands_for_build( Whether to show cell source code. reactive Whether cells should be reactive. + app_id + Unique app identifier for namespacing islands on the same page. + Defaults to the notebook stem name. """ html = generate_islands_html( notebook_path, display_code=display_code, reactive=reactive, + app_id=app_id, ) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(html, encoding="utf-8") From 0f8b7d5eea759dc72e1ebf8ffe23e9bfc58fa9a0 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:20:25 -0400 Subject: [PATCH 13/36] Add show-code option for marimo islands --- great_docs/assets/_extensions/marimo/marimo.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/great_docs/assets/_extensions/marimo/marimo.lua b/great_docs/assets/_extensions/marimo/marimo.lua index 03bf8e07..ffb5f8af 100644 --- a/great_docs/assets/_extensions/marimo/marimo.lua +++ b/great_docs/assets/_extensions/marimo/marimo.lua @@ -84,6 +84,8 @@ return { end -- ISLAND MODE -------------------------------------------------------- + local show_code = kwarg(kwargs, "show-code", "true") + -- Read pre-generated island HTML from _marimo_islands/.html local stem = file:match("([^/]+)%.py$") if not stem then @@ -93,7 +95,12 @@ return { .. escape_html(file) .. '') end - local island_html = read_project_file("_marimo_islands/" .. stem .. ".html") + -- Use -nocode variant when show-code is false + local island_file = "_marimo_islands/" .. stem .. ".html" + if show_code == "false" then + island_file = "_marimo_islands/" .. stem .. "-nocode.html" + end + local island_html = read_project_file(island_file) if not island_html then quarto.log.warning("[marimo] Pre-generated island HTML not found for: " .. stem) return pandoc.RawInline("html", @@ -102,7 +109,11 @@ return { end local parts = {} - table.insert(parts, '
') + local wrapper_classes = "gd-marimo-island-group" + if show_code == "false" then + wrapper_classes = wrapper_classes .. " gd-marimo-nocode" + end + table.insert(parts, '
') table.insert(parts, island_html) -- Copy notebook button From 1c4f212355fc3cfeb328ed75bbcecabeba1749ee Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:20:35 -0400 Subject: [PATCH 14/36] Add no-code mode CSS to hide code cells --- great_docs/assets/marimo-islands.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/great_docs/assets/marimo-islands.css b/great_docs/assets/marimo-islands.css index f3a82f67..0ee5766d 100644 --- a/great_docs/assets/marimo-islands.css +++ b/great_docs/assets/marimo-islands.css @@ -113,6 +113,15 @@ marimo-cell-output { border-radius: 0.2rem; } +/* No-code mode: hide source, show outputs only */ +.gd-marimo-nocode .gd-marimo-code { + display: none; +} + +.gd-marimo-nocode marimo-cell-code { + display: none; +} + /* Error state */ .gd-marimo-error { padding: 1rem; From 9d04c848a81dafd017c5d13029b009466ec3bc7f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:20:51 -0400 Subject: [PATCH 15/36] Add nocode HTML and WASM exports for notebooks --- great_docs/core.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/great_docs/core.py b/great_docs/core.py index 60e5e870..157931f2 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -316,6 +316,35 @@ def _prepare_build_directory(self) -> None: for nb_file in notebooks_src.glob("*.py"): out_file = islands_dir / f"{nb_file.stem}.html" generate_islands_for_build(nb_file, out_file, reactive=True) + # Also generate nocode variant for show-code="false" + nocode_file = islands_dir / f"{nb_file.stem}-nocode.html" + generate_islands_for_build( + nb_file, nocode_file, display_code=False, reactive=True + ) + + # Generate WASM exports for iframe mode + import subprocess + + for nb_file in notebooks_src.glob("*.py"): + wasm_dir = self.project_path / "notebooks" / nb_file.stem + wasm_dir.mkdir(parents=True, exist_ok=True) + subprocess.run( + [ + _sys.executable, + "-m", + "marimo", + "export", + "html-wasm", + str(nb_file), + "-o", + str(wasm_dir) + "/", + "--mode", + "run", + ], + input="n\n", + capture_output=True, + text=True, + ) finally: _sys.stdout = _orig_stdout _sys.stderr = _orig_stderr From b47b787d30cb884ed6f48d9124928acaada8d78c Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:21:08 -0400 Subject: [PATCH 16/36] Add GT output demo and package-ready flag --- notebooks/gt-basics.py | 6 ++-- notebooks/gt-output-demo.py | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 notebooks/gt-output-demo.py diff --git a/notebooks/gt-basics.py b/notebooks/gt-basics.py index eee149ce..3b31bd7c 100644 --- a/notebooks/gt-basics.py +++ b/notebooks/gt-basics.py @@ -27,7 +27,9 @@ async def __(): import micropip await micropip.install(["great-tables", "polars"]) - return + + _packages_ready = True + return (_packages_ready,) @app.cell @@ -44,7 +46,7 @@ def __(mo): @app.cell -def __(): +def __(_packages_ready): import great_tables as gt import polars as pl diff --git a/notebooks/gt-output-demo.py b/notebooks/gt-output-demo.py new file mode 100644 index 00000000..9d36bf5a --- /dev/null +++ b/notebooks/gt-output-demo.py @@ -0,0 +1,70 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "marimo", +# "great-tables", +# "polars", +# ] +# /// + +import marimo + +app = marimo.App() + + +@app.cell +def __(): + return + + +@app.cell +async def __(): + import sys + + if "pyodide" in sys.modules: + import micropip + + await micropip.install(["great-tables", "polars"]) + + _packages_ready = True + return (_packages_ready,) + + +@app.cell +def __(_packages_ready): + import great_tables as _gt + import polars as _pl + + # Monthly sales data + sales = _pl.DataFrame( + { + "month": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], + "revenue": [12400, 15800, 14200, 18900, 21300, 19700], + "growth": [None, 0.274, -0.101, 0.331, 0.127, -0.075], + } + ) + + ( + _gt.GT(sales) + .tab_header( + title="Monthly Revenue", + subtitle="H1 2026 Performance", + ) + .cols_label( + month="Month", + revenue="Revenue", + growth="Growth", + ) + .fmt_currency(columns="revenue", decimals=0) + .fmt_percent(columns="growth", decimals=1) + .data_color( + columns="revenue", + palette=["#f0f9e8", "#0868ac"], + ) + .sub_missing(missing_text="—") + ) + return + + +if __name__ == "__main__": + app.run() From fee8238664ea42538532193593fe44111ecdb0c6 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 1 Jun 2026 17:21:21 -0400 Subject: [PATCH 17/36] Add iframe example and hide-code demo --- user_guide/43-marimo-notebooks.qmd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/user_guide/43-marimo-notebooks.qmd b/user_guide/43-marimo-notebooks.qmd index 4471b762..d32f1542 100644 --- a/user_guide/43-marimo-notebooks.qmd +++ b/user_guide/43-marimo-notebooks.qmd @@ -115,15 +115,19 @@ into view) to avoid impacting initial page load. Embeds the full Marimo notebook UI in a sandboxed iframe. Use this for complex notebooks that benefit from the complete editor interface. +{{< marimo file="notebooks/gt-basics.py" mode="iframe" height="700px" >}} + ### Hide code ```markdown -{{{{< marimo file="notebooks/gt-basics.py" show-code="false" >}}}} +{{{{< marimo file="notebooks/gt-output-demo.py" show-code="false" >}}}} ``` Shows only outputs, which is useful for dashboards or app-style presentations where the code is secondary. +{{< marimo file="notebooks/gt-output-demo.py" show-code="false" >}} + These options can be combined freely. For instance, you might use `show-code="false"` with `theme="dark"` for a polished demo, or `mode="iframe"` with a custom `height` for a full-featured exploratory tool. From 300557401cde8fa899bb885015caa8baba3c2794 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 17:13:45 -0400 Subject: [PATCH 18/36] Fix marimo island runtime integration --- great_docs/_marimo.py | 130 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/great_docs/_marimo.py b/great_docs/_marimo.py index d2c40929..09b07c3a 100644 --- a/great_docs/_marimo.py +++ b/great_docs/_marimo.py @@ -1,7 +1,7 @@ """Marimo notebook integration for Great Docs. -Provides utilities for generating marimo island HTML at build time -using MarimoIslandGenerator, and supporting the marimo Quarto shortcode. +Provides utilities for generating marimo island HTML at build time using MarimoIslandGenerator, and +supporting the marimo Quarto shortcode. """ from __future__ import annotations @@ -13,18 +13,53 @@ # Marimo islands CDN base _ISLANDS_CDN = "https://cdn.jsdelivr.net/npm/@marimo-team/islands" -_DEFAULT_VERSION = "0.23.8" +# Fallback used only when the installed marimo version can't be determined. +_FALLBACK_VERSION = "0.23.8" -def get_islands_head_html(version: str = _DEFAULT_VERSION) -> str: - """Return the \n' f'\n' + 'rel="stylesheet" title="marimo-islands" crossorigin="anonymous"/>\n' '\n' '\n' - '' + '\n' + f'\n' + f'\n' + "" ) @@ -37,8 +72,8 @@ def generate_islands_html( ) -> str: """Generate marimo island HTML from a notebook file. - Uses MarimoIslandGenerator to produce correct island markup - that the @marimo-team/islands runtime can activate. + Uses MarimoIslandGenerator to produce correct island markup that the @marimo-team/islands + runtime can activate. Parameters ---------- @@ -49,8 +84,8 @@ def generate_islands_html( reactive Whether cells should be reactive (run with Pyodide in browser). app_id - Unique app identifier for namespacing islands on the same page. - Defaults to the notebook stem name. + Unique app identifier for namespacing islands on the same page. Defaults to the notebook + stem name. Returns ------- @@ -79,9 +114,12 @@ def generate_islands_html( sys.stdout = old_stdout sys.stderr = old_stderr - # Render body HTML (the islands themselves) + # Render body HTML (the islands themselves). The init island renders a + # loading spinner and is what triggers the islands runtime to boot the + # Pyodide kernel — without it the custom elements load but never hydrate, + # so cells stay static and non-interactive. body_html = gen.render_body( - include_init_island=False, + include_init_island=True, max_width="100%", ) @@ -89,16 +127,19 @@ def generate_islands_html( if not reactive: body_html = body_html.replace('data-reactive="true"', 'data-reactive="false"') - # When hiding code, strip islands whose output is empty (utility cells) - if not display_code: - body_html = re.sub( - r"]*>\s*\s*\s*" - r"\s*(?:]*>.*?\s*)?" - r"\s*", - "", - body_html, - flags=re.DOTALL, - ) + # NOTE: We intentionally do NOT strip "empty output" islands (e.g. an `import marimo as mo` + # cell) even when hiding code. Those cells are part of the reactive graph and removing their + # island leaves the runtime unable to resolve `mo` and every dependent cell fails with a + # NameError. With display_code=False the code editor isn't shown, and marimo's own + # `empty:hidden` styling collapses the empty output, so the cell stays invisible while still + # executing. + + # Tag the leading run of output-less "setup" cells (imports/utility) so the front-end can + # collapse them behind a disclosure toggle. Only meaningful when code is shown and in no-code + # mode these cells are hidden anyway. This is done at build time (where MarimoIslandGenerator + # has actually run the notebook) so emptiness is authoritative and not subject to render races. + if display_code: + body_html = _tag_setup_islands(body_html) # Namespace islands with a unique app_id (defaults to notebook stem) resolved_app_id = app_id or notebook_path.stem @@ -108,6 +149,45 @@ def generate_islands_html( return body_html +# Matches a marimo cell whose output is empty (e.g. an `import` cell). +_EMPTY_OUTPUT_RE = re.compile( + r"\s*\s*\s*", re.DOTALL +) +_ISLAND_RE = re.compile(r"", re.DOTALL) + + +def _tag_setup_islands(body_html: str) -> str: + """Add `data-gd-setup="true"` to the leading run of empty-output cells. + + Walks islands in document order, skipping the non-reactive init/loader island, and marks each + reactive cell whose output is empty until the first cell that produces output. Only the leading + run is tagged, so a cell that renders anything is never collapsed. + """ + out: list[str] = [] + pos = 0 + leading = True + + for match in _ISLAND_RE.finditer(body_html): + out.append(body_html[pos : match.start()]) + pos = match.end() + block = match.group(0) + + open_tag = block[: block.find(">") + 1] + is_reactive = 'data-reactive="true"' in open_tag + + if is_reactive: + if leading and _EMPTY_OUTPUT_RE.search(block): + block = block.replace(" Date: Mon, 3 Aug 2026 17:20:28 -0400 Subject: [PATCH 19/36] Use installed marimo version as CDN default --- great_docs/config.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/great_docs/config.py b/great_docs/config.py index 32448d1b..1e60246a 100644 --- a/great_docs/config.py +++ b/great_docs/config.py @@ -754,11 +754,19 @@ def marimo_enabled(self) -> bool: @property def marimo_version(self) -> str: - """Get the @marimo-team/islands CDN version.""" + """Get the @marimo-team/islands CDN runtime version. + + Defaults to the installed marimo version so the browser runtime matches + the version that generated the island markup. An explicit ``version`` in + the config (dict form) overrides this. + """ val = self.get("marimo") - if isinstance(val, dict): - return val.get("version", "0.23.8") - return "0.23.8" + if isinstance(val, dict) and val.get("version"): + return str(val["version"]) + + from great_docs._marimo import islands_runtime_version + + return islands_runtime_version() @property def markdown_pages(self) -> bool: From 14c24ef8410d55640e4d8c53feecaa31b1a3fb9a Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 17:20:46 -0400 Subject: [PATCH 20/36] Extract marimo islands HTML to helper function --- great_docs/core.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/great_docs/core.py b/great_docs/core.py index 157931f2..646742a1 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -11063,17 +11063,10 @@ def _update_quarto_config(self) -> None: # Add marimo islands runtime (CDN JS/CSS) when enabled if self._config.marimo_enabled: + from great_docs._marimo import get_islands_head_html + marimo_version = self._config.marimo_version - marimo_entry = { - "text": ( - f'\n' - f'\n' - '' - ) - } + marimo_entry = {"text": get_islands_head_html(marimo_version)} if not any( "marimo-team/islands" in str(item) for item in config["format"]["html"]["include-in-header"] From d6582a4aca54f58e2706bbf4716ac5f907ec6b34 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 17:21:13 -0400 Subject: [PATCH 21/36] Polish marimo island loading and setup UI --- great_docs/assets/marimo-islands.css | 114 ++++++++++++-- great_docs/assets/marimo-islands.js | 220 ++++++++++++++++++++++++++- 2 files changed, 315 insertions(+), 19 deletions(-) diff --git a/great_docs/assets/marimo-islands.css b/great_docs/assets/marimo-islands.css index 0ee5766d..e85bd516 100644 --- a/great_docs/assets/marimo-islands.css +++ b/great_docs/assets/marimo-islands.css @@ -53,19 +53,6 @@ marimo-cell-output { min-height: 1.5rem; } -/* Loading placeholder */ -.gd-marimo-loading { - color: var(--bs-secondary-color, #6c757d); - font-size: 0.85rem; - font-style: italic; - padding: 0.25rem 0; -} - -/* Active state (after Pyodide boots) */ -.gd-marimo-island-group.gd-marimo-active .gd-marimo-loading { - display: none; -} - /* Copy notebook button area */ .gd-marimo-copy { display: flex; @@ -164,3 +151,104 @@ marimo-cell-output { border-color: var(--bs-border-color); color: var(--bs-body-color); } + +/* Loading state ----------------------------------------------------------- */ +/* While the kernel boots and widgets mount, hide the assembling cells so + readers don't watch them shuffle and collapse. Space is reserved (cells are + only made invisible, not removed) and the loader is centered over it; the + whole notebook reveals together once settled. The `gd-marimo-booting` class + is added by JS, so with no JS (or a failed boot) the static output still + shows. */ +.gd-marimo-island-group.gd-marimo-booting:not(.gd-marimo-ready) { + position: relative; + min-height: 8rem; +} + +.gd-marimo-island-group.gd-marimo-booting:not(.gd-marimo-ready) + marimo-island:not(.gd-marimo-loader) { + visibility: hidden; +} + +/* The captured loader island, centered over the reserved space. */ +.gd-marimo-loader { + position: absolute !important; + inset: 0; + display: flex !important; + align-items: center; + justify-content: center; + border-bottom: none !important; +} + +.gd-marimo-island-group.gd-marimo-ready .gd-marimo-loader { + display: none !important; +} + +/* No-code mode: hide marimo's per-cell action toolbar (copy/run icons). In an + outputs-only presentation the code-oriented controls don't belong. */ +.gd-marimo-nocode marimo-island .absolute.top-0.right-0.z-50 { + display: none !important; +} + +/* Trim the leading/trailing block margins inside a cell's rendered output. + marimo's prose gives the first heading a large top margin, which as the first + element in a cell reads as excess space above the output. */ +.gd-marimo-island-group marimo-island .prose > :first-child, +.gd-marimo-island-group marimo-island .markdown > :first-child { + margin-top: 0 !important; +} + +.gd-marimo-island-group marimo-island .prose > :last-child, +.gd-marimo-island-group marimo-island .markdown > :last-child { + margin-bottom: 0 !important; +} + +/* Collapsible setup block --------------------------------------------------- */ +/* Leading import/utility cells that render no output are collapsed behind a + disclosure toggle (added by JS) so boilerplate doesn't clutter the notebook. + The toggle mirrors the gray "Copy Notebook" footer. */ +/* Setup cells are tagged `data-gd-setup` at build time and hidden by default. + Higher specificity than `.gd-marimo-island-group marimo-island` so they + actually stay hidden. */ +.gd-marimo-island-group marimo-island[data-gd-setup] { + display: none; +} + +.gd-marimo-island-group.gd-marimo-setup-open marimo-island[data-gd-setup] { + display: block; +} + +.gd-marimo-setup-toggle { + display: flex; + align-items: center; + gap: 0.4rem; + width: 100%; + margin: 0; + padding: 0.4rem 1rem; + font-size: 0.78rem; + font-weight: 500; + color: var(--bs-secondary-color, #6c757d); + background: var(--bs-tertiary-bg, #f8f9fa); + border: none; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + cursor: pointer; + text-align: left; +} + +.gd-marimo-setup-toggle:hover { + background: var(--bs-secondary-bg, #e9ecef); + color: var(--bs-body-color, #212529); +} + +.gd-marimo-setup-chevron { + flex-shrink: 0; + transition: transform 0.15s ease; +} + +.gd-marimo-island-group.gd-marimo-setup-open .gd-marimo-setup-chevron { + transform: rotate(90deg); +} + +/* Tint the revealed setup cell so it reads as distinct boilerplate. */ +.gd-marimo-island-group.gd-marimo-setup-open marimo-island[data-gd-setup] { + background: var(--bs-tertiary-bg, #f8f9fa); +} diff --git a/great_docs/assets/marimo-islands.js b/great_docs/assets/marimo-islands.js index 11b0912b..2dfeaa73 100644 --- a/great_docs/assets/marimo-islands.js +++ b/great_docs/assets/marimo-islands.js @@ -4,6 +4,9 @@ * Attached to pages that use the {{< marimo >}} shortcode. * - Uses IntersectionObserver to defer Pyodide boot until islands scroll into view. * - Provides "Copy Notebook" button handler. + * - Syncs marimo's dark theme to the Great Docs site theme (marimo islands read a + * `.dark` class on an ancestor element). + * - Hides the "Initializing…" loader island once the notebook cells have hydrated. */ (function () { "use strict"; @@ -32,6 +35,208 @@ }); } + // --- Theme Sync --- + // marimo islands render in the light DOM and pick up dark styling from a + // `.dark` class on an ancestor. The Great Docs site signals dark mode via a + // `quarto-dark` class / `data-bs-theme="dark"` on , so bridge the two. + function siteIsDark() { + var el = document.documentElement; + return ( + el.classList.contains("quarto-dark") || + el.getAttribute("data-bs-theme") === "dark" + ); + } + + function applyTheme() { + var dark = siteIsDark(); + document.querySelectorAll(".gd-marimo-island-group").forEach(function (group) { + // Per-shortcode override: data-theme="light" | "dark" | "auto" (default). + var mode = group.getAttribute("data-theme") || "auto"; + var isDark = mode === "dark" || (mode !== "light" && dark); + group.classList.toggle("dark", isDark); + }); + } + + function initThemeSync() { + if (document.querySelectorAll(".gd-marimo-island-group").length === 0) return; + applyTheme(); + // React to the site's dark-mode toggle (class / attribute changes on ). + var observer = new MutationObserver(applyTheme); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-bs-theme"], + }); + } + + // --- Hide the "Initializing…" loader once cells hydrate --- + // marimo's init island renders a spinner that boots the kernel but isn't + // auto-removed in embedded islands mode. marimo mounts its cells (CodeMirror + // editors, rendered output) inside shadow DOM, so detection must pierce shadow + // roots — and shadow mutations don't bubble to a light-DOM observer, so we + // poll rather than rely on MutationObserver. + function deepHas(root, selector) { + if (root.querySelector(selector)) return true; + var hosts = root.querySelectorAll("*"); + for (var i = 0; i < hosts.length; i++) { + if (hosts[i].shadowRoot && deepHas(hosts[i].shadowRoot, selector)) return true; + } + return false; + } + + // Inject style fixes into marimo's shadow roots (which our external stylesheet + // and CSS variables can't reach — CodeMirror's styles in particular live in + // adopted stylesheets): + // 1. `.marimo{color:inherit}` — the widget wrapper carries a hardcoded + // light-mode text color that marimo's own dark styling never overrides, + // so labels (e.g. a slider's) stay dark on a dark site. `!important` + // because marimo appends its own rule after ours. Only the container is + // forced to inherit; children with their own color are unaffected. + // 2. Shrink the CodeMirror font — marimo's `.9rem` renders large against the + // docs site's root, so code wraps early (worse on mobile / narrow content). + // Idempotent; recurses into nested roots. + function injectShadowStyles(root) { + var els = root.querySelectorAll("*"); + for (var i = 0; i < els.length; i++) { + var sr = els[i].shadowRoot; + if (!sr) continue; + var needed = sr.querySelector(".marimo") || sr.querySelector(".cm-editor"); + if (needed && !sr.querySelector("style[data-gd-marimo-fix]")) { + var st = document.createElement("style"); + st.setAttribute("data-gd-marimo-fix", "1"); + st.textContent = + ".marimo{color:inherit!important;}" + + ".cm-editor,.cm-content,.cm-line,.cm-gutters{font-size:0.78rem!important;}"; + sr.appendChild(st); + } + injectShadowStyles(sr); + } + } + + // Length of *rendered output* text. marimo renders cell output into shadow + // roots, so count only shadow-DOM text — this deliberately ignores the cell's + // hidden source (a light-DOM ), which the runtime strips + // slightly after boot and which must not count as "has output". + function renderedTextLen(root) { + var len = 0; + var hosts = root.querySelectorAll("*"); + for (var i = 0; i < hosts.length; i++) { + var sr = hosts[i].shadowRoot; + if (sr) len += (sr.textContent || "").trim().length + renderedTextLen(sr); + } + return len; + } + + function groupHasHydrated(group) { + // A code editor mounted (code visible) … + if (deepHas(group, ".cm-editor")) return true; + // … or a reactive cell rendered marimo output (no-code mode). marimo output + // lands in an element carrying the `marimo` class once hydrated. + var cells = group.querySelectorAll('marimo-island[data-reactive="true"]'); + for (var i = 0; i < cells.length; i++) { + if (deepHas(cells[i], ".markdown, .prose, table, img, .cm-editor")) return true; + } + return false; + } + + // In no-code mode, utility cells (e.g. `import marimo as mo`) are kept in the + // markup so the reactive kernel can run them, but render no visible output. + // Once cells have settled, collapse islands that produced neither text nor an + // interactive widget so they don't show as empty boxes. Cells that render only + // a widget (e.g. a bare slider) have no text but must be kept. + function pruneEmptyNocodeCells(group) { + if (!group.classList.contains("gd-marimo-nocode")) return; + group + .querySelectorAll('marimo-island[data-reactive="true"]') + .forEach(function (island) { + var hasText = renderedTextLen(island) > 0; + var hasWidget = deepHas( + island, + "input, button, select, textarea, [role=slider], canvas, svg, img, table" + ); + if (!hasText && !hasWidget) island.style.display = "none"; + }); + } + + // Collapse the "setup" cells behind a disclosure toggle so the boilerplate + // doesn't clutter the notebook. Setup cells are tagged at build time with + // `data-gd-setup` (where emptiness is authoritative — no render race) and + // hidden by CSS from first paint; this just wires up the reveal toggle. + // No-op in no-code mode (which prunes these cells entirely). + function wireSetupToggle(group) { + if (group.classList.contains("gd-marimo-nocode")) return; + if (group.classList.contains("gd-marimo-setup-done")) return; + var setupCells = group.querySelectorAll("marimo-island[data-gd-setup]"); + if (setupCells.length === 0) return; + group.classList.add("gd-marimo-setup-done"); + + var toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "gd-marimo-setup-toggle"; + toggle.setAttribute("aria-expanded", "false"); + var label = setupCells.length > 1 ? "Setup (" + setupCells.length + " cells)" : "Setup"; + toggle.innerHTML = + '' + + ''; + toggle.querySelector("span").textContent = label; + toggle.addEventListener("click", function () { + var open = group.classList.toggle("gd-marimo-setup-open"); + toggle.setAttribute("aria-expanded", open ? "true" : "false"); + }); + setupCells[0].parentNode.insertBefore(toggle, setupCells[0]); + } + + // Reveal a group's cells and drop its loader. Called once the notebook has + // settled (or as a timeout fallback so a failed boot still shows something). + function revealGroup(group, loaderIsland) { + if (loaderIsland) loaderIsland.style.display = "none"; + group.classList.add("gd-marimo-ready"); + } + + function initPostHydrationCleanup() { + document.querySelectorAll(".gd-marimo-island-group").forEach(function (group) { + // Capture the loader island now, while its spinner is present: the runtime + // may clear the spinner before we hide it, and the empty island would then + // linger as a tall blank box above the content. + var spinner = group.querySelector(".animate-spin"); + var loaderIsland = spinner ? spinner.closest("marimo-island") : null; + if (loaderIsland) loaderIsland.classList.add("gd-marimo-loader"); + // Enter the loading state: CSS hides the assembling cells (reserving space) + // and centers the loader, so readers don't see cells shuffle/collapse as + // widgets mount. Added via JS so no-JS/failed-boot still shows static output. + group.classList.add("gd-marimo-booting"); + + // Wire the setup-collapse toggle up-front: the setup cells are tagged at + // build time and hidden by CSS already, so this is race-free. + wireSetupToggle(group); + + var elapsed = 0; + var settled = false; + var timer = setInterval(function () { + elapsed += 500; + // Re-apply on every tick: marimo mounts widget shadow roots lazily as + // cells execute, so late-mounted sliders/labels still get themed. The + // injection is idempotent, so repeating is cheap. + injectShadowStyles(group); + if (!settled && groupHasHydrated(group)) { + settled = true; + // Let widgets finish mounting, then prune empty cells and reveal. + setTimeout(function () { + injectShadowStyles(group); + pruneEmptyNocodeCells(group); + revealGroup(group, loaderIsland); + }, 1500); + } + if (elapsed >= 30000) { + // Fallback: never leave cells hidden if boot stalls. + revealGroup(group, loaderIsland); + clearInterval(timer); + } + }, 500); + }); + } + // --- Lazy-Load Islands Runtime --- var runtimeLoaded = false; @@ -77,13 +282,16 @@ } // --- Init --- - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", function () { - initCopyButtons(); - initLazyLoad(); - }); - } else { + function init() { initCopyButtons(); + initThemeSync(); + initPostHydrationCleanup(); initLazyLoad(); } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } })(); From d1c32d9ecfffe4e6ea5117189e317fa092b6de0e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 17:28:46 -0400 Subject: [PATCH 22/36] Add gdtest_marimo GDG test package and update demo notebooks Adds a Marimo-focused synthetic package (island + nocode modes) for fast iteration on the marimo islands integration, registered as #193 in the catalog. Also tweaks the demo notebooks. Co-Authored-By: Claude Opus 4.8 --- notebooks/gt-basics.py | 14 +- notebooks/gt-output-demo.py | 6 - test-packages/synthetic/catalog.py | 2 + .../synthetic/specs/gdtest_marimo.py | 153 ++++++++++++++++++ 4 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 test-packages/synthetic/specs/gdtest_marimo.py diff --git a/notebooks/gt-basics.py b/notebooks/gt-basics.py index 3b31bd7c..8134772d 100644 --- a/notebooks/gt-basics.py +++ b/notebooks/gt-basics.py @@ -28,8 +28,10 @@ async def __(): await micropip.install(["great-tables", "polars"]) - _packages_ready = True - return (_packages_ready,) + import great_tables as gt + import polars as pl + + return gt, pl @app.cell @@ -45,14 +47,6 @@ def __(mo): return -@app.cell -def __(_packages_ready): - import great_tables as gt - import polars as pl - - return gt, pl - - @app.cell def __(gt, pl): # Create sample data diff --git a/notebooks/gt-output-demo.py b/notebooks/gt-output-demo.py index 9d36bf5a..1be8a301 100644 --- a/notebooks/gt-output-demo.py +++ b/notebooks/gt-output-demo.py @@ -26,12 +26,6 @@ async def __(): await micropip.install(["great-tables", "polars"]) - _packages_ready = True - return (_packages_ready,) - - -@app.cell -def __(_packages_ready): import great_tables as _gt import polars as _pl diff --git a/test-packages/synthetic/catalog.py b/test-packages/synthetic/catalog.py index 229c5ca0..9a5199b6 100644 --- a/test-packages/synthetic/catalog.py +++ b/test-packages/synthetic/catalog.py @@ -384,6 +384,8 @@ "gdtest_details_shortcode", # 191 # 192: Termshow recording/playback showcase "gdtest_termshow", # 192 + # 193: Marimo notebook islands showcase + "gdtest_marimo", # 193 ] diff --git a/test-packages/synthetic/specs/gdtest_marimo.py b/test-packages/synthetic/specs/gdtest_marimo.py new file mode 100644 index 00000000..a6ebfd4e --- /dev/null +++ b/test-packages/synthetic/specs/gdtest_marimo.py @@ -0,0 +1,153 @@ +""" +gdtest_marimo — Verify the marimo notebook integration (islands). + +Focus: The `{{< marimo >}}` shortcode + `marimo: true` config. Exercises the + build-time island generation (MarimoIslandGenerator) and the browser-side + @marimo-team/islands runtime. + +Uses a lightweight, dependency-free notebook (marimo only, no micropip installs) +so the WASM kernel boots fast and reliably during iteration — this isolates the +island *rendering mechanics* from package-install concerns. +""" + +# A minimal reactive marimo notebook: a slider whose value drives a dependent +# markdown cell. Demonstrates island rendering + reactivity with no external deps. +_DEMO_NOTEBOOK = '''# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "marimo", +# ] +# /// + +import marimo + +app = marimo.App() + + +@app.cell +def __(): + import marimo as mo + return (mo,) + + +@app.cell +def __(mo): + mo.md( + """ + # Interactive Demo + + Drag the slider below — the output updates reactively. + """ + ) + return + + +@app.cell +def __(mo): + n = mo.ui.slider(1, 20, value=5, label="How many?") + n + return (n,) + + +@app.cell +def __(mo, n): + mo.md(f"You chose **{n.value}**. Its square is **{n.value ** 2}**.") + return + + +if __name__ == "__main__": + app.run() +''' + +SPEC = { + "name": "gdtest_marimo", + "description": "Interactive marimo notebook islands via the {{< marimo >}} shortcode", + "dimensions": ["A1", "B1", "C4", "D2", "E6", "F1", "G1", "H7"], + "pyproject_toml": { + "project": { + "name": "gdtest-marimo", + "version": "1.0.0", + "description": "A package demonstrating embedded marimo notebooks", + }, + "build-system": { + "requires": ["setuptools"], + "build-backend": "setuptools.build_meta", + }, + }, + "files": { + # ── Python module (minimal) ────────────────────────────────────── + "gdtest_marimo/__init__.py": ( + '"""Marimo islands demo package."""\n' + "\n" + '__version__ = "1.0.0"\n' + '__all__ = ["greet"]\n' + "\n" + "\n" + "def greet(name: str) -> str:\n" + ' """Return a friendly greeting.\n' + "\n" + " Parameters\n" + " ----------\n" + " name\n" + " Who to greet.\n" + "\n" + " Returns\n" + " -------\n" + " str\n" + " The greeting.\n" + ' """\n' + ' return f"Hello, {name}!"\n' + ), + # ── The marimo notebook the shortcode embeds ───────────────────── + "notebooks/demo.py": _DEMO_NOTEBOOK, + # ── User guide: island mode (default) ──────────────────────────── + "user_guide/01-islands.qmd": ( + "---\n" + "title: Marimo Islands\n" + "---\n" + "\n" + "# Interactive Notebook (Island Mode)\n" + "\n" + "The notebook below is embedded with the default island mode. Its cells\n" + "run in the browser via WebAssembly (Pyodide).\n" + "\n" + '{{< marimo file="notebooks/demo.py" >}}\n' + ), + # ── User guide: hide code (outputs only) ───────────────────────── + "user_guide/02-nocode.qmd": ( + "---\n" + "title: Outputs Only\n" + "---\n" + "\n" + "# Outputs Only (show-code=false)\n" + "\n" + "The same notebook, rendered with the source hidden — useful for\n" + "dashboard-style presentations.\n" + "\n" + '{{< marimo file="notebooks/demo.py" show-code="false" >}}\n' + ), + }, + "config": { + "marimo": True, + "dark_mode": True, + }, + "expected": { + "files_exist": [ + "reference/index.html", + "reference/greet.html", + "user-guide/islands.html", + "user-guide/nocode.html", + ], + "files_contain": { + "user-guide/islands.html": [ + "marimo-island", + "gd-marimo-island-group", + "gd-marimo-copy-btn", + ], + "user-guide/nocode.html": [ + "gd-marimo-nocode", + ], + }, + "coverage_exclude": ['ref', 'nodoc', 'bigcl', 'ug', 'supp', 'title', 'badge', 'sig', 'desc', 'param', 'pmatch', 'ret', 'refidx', 'sechdg', 'sbsec', 'hdg'], + }, +} From 18e1cf8398c7392f32a5341fabad59241c1ed581 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 17:55:44 -0400 Subject: [PATCH 23/36] Add iframe-mode page to gdtest_marimo GDG spec Verifies mode="iframe": great-docs core generates a clean self-hosted marimo WASM export at notebooks// and the shortcode embeds it in a sandboxed iframe. Adds expected checks for the iframe page and the WASM index.html. Co-Authored-By: Claude Opus 4.8 --- .../synthetic/specs/gdtest_marimo.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-packages/synthetic/specs/gdtest_marimo.py b/test-packages/synthetic/specs/gdtest_marimo.py index a6ebfd4e..7a97cb51 100644 --- a/test-packages/synthetic/specs/gdtest_marimo.py +++ b/test-packages/synthetic/specs/gdtest_marimo.py @@ -126,6 +126,19 @@ def __(mo, n): "\n" '{{< marimo file="notebooks/demo.py" show-code="false" >}}\n' ), + # ── User guide: iframe mode (full notebook, self-hosted WASM) ───── + "user_guide/03-iframe.qmd": ( + "---\n" + "title: Iframe Mode\n" + "---\n" + "\n" + "# Full Notebook (Iframe Mode)\n" + "\n" + "The same notebook embedded as a full Marimo app in a sandboxed iframe,\n" + "served from a self-hosted WASM export.\n" + "\n" + '{{< marimo file="notebooks/demo.py" mode="iframe" height="600px" >}}\n' + ), }, "config": { "marimo": True, @@ -137,6 +150,8 @@ def __(mo, n): "reference/greet.html", "user-guide/islands.html", "user-guide/nocode.html", + "user-guide/iframe.html", + "notebooks/demo/index.html", ], "files_contain": { "user-guide/islands.html": [ @@ -147,6 +162,10 @@ def __(mo, n): "user-guide/nocode.html": [ "gd-marimo-nocode", ], + "user-guide/iframe.html": [ + "gd-marimo-iframe", + "notebooks/demo/index.html", + ], }, "coverage_exclude": ['ref', 'nodoc', 'bigcl', 'ug', 'supp', 'title', 'badge', 'sig', 'desc', 'param', 'pmatch', 'ret', 'refidx', 'sechdg', 'sbsec', 'hdg'], }, From 80a8a9ae8d3dc4c08947ac811ae1369d4da9e37e Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 18:37:35 -0400 Subject: [PATCH 24/36] Install marimo in docs CI and degrade gracefully when it's missing The docs site sets `marimo: true`, so the build imported marimo during island generation and crashed in CI ("No module named 'marimo'"). Add a `marimo` optional-dependency extra, install `.[mcp,marimo]` in the docs workflow, and guard the island-generation step: if marimo isn't installed, warn and skip (pages using the shortcode show a "not generated" notice) instead of failing the whole build. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/docs.yml | 2 +- great_docs/core.py | 13 ++++++++++++- pyproject.toml | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 43f74e77..9bab21fa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e ".[mcp]" + python -m pip install -e ".[mcp,marimo]" python -m pip install jupyter polars pandas pyarrow plotnine - name: Set up Quarto diff --git a/great_docs/core.py b/great_docs/core.py index bcd57a8a..ee09e31c 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -324,8 +324,19 @@ def _prepare_build_directory(self) -> None: # Copy notebooks directory and pre-generate marimo island HTML if self._config.marimo_enabled: + import importlib.util + notebooks_src = self.project_root / "notebooks" - if notebooks_src.exists() and notebooks_src.is_dir(): + if importlib.util.find_spec("marimo") is None: + # `marimo: true` is set but the package isn't installed. Warn and + # skip rather than crashing the whole build; pages using the + # {{< marimo >}} shortcode will show a "not generated" notice. + print( + "Warning: marimo notebooks are enabled in great-docs.yml but " + "the 'marimo' package is not installed; skipping notebook " + "island generation. Install it with: pip install marimo" + ) + elif notebooks_src.exists() and notebooks_src.is_dir(): notebooks_dst = self.project_path / "notebooks" shutil.copytree(notebooks_src, notebooks_dst, dirs_exist_ok=True) diff --git a/pyproject.toml b/pyproject.toml index ab0c331e..466d0013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ svg = [ mcp = [ "mcp>=1.0.0", ] +marimo = [ + "marimo>=0.20.0", +] dev = [ "pytest>=6.0", "pytest-cov>=3.0", From dd87b2b45b2909917d3fe07a616c6563ed675270 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 18:43:37 -0400 Subject: [PATCH 25/36] Add marimo to frozen default config in test test_config_defaults_yaml_matches_frozen_defaults guards that great-docs.default.yml stays in sync with a frozen copy. Adding the `marimo` option to the YAML requires the matching entry here. Co-Authored-By: Claude Opus 4.8 --- tests/test_config_defaults.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_config_defaults.py b/tests/test_config_defaults.py index 46b6e02a..c719a4a5 100644 --- a/tests/test_config_defaults.py +++ b/tests/test_config_defaults.py @@ -30,6 +30,7 @@ "site_url": None, "source": {"enabled": True, "branch": None, "path": None, "placement": "usage"}, "sidebar_filter": {"enabled": True, "min_items": 20}, + "marimo": {"enabled": False, "version": None}, "cli": { "enabled": False, "module": None, From 4cd90a4c5c68f94ee5d5c1306576ec09c26b3ac3 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 19:44:11 -0400 Subject: [PATCH 26/36] Fix marimo User Guide page: icon, great-tables in CI, export diagnostics - Add a Lucide icon (notebook-pen) for the "Marimo Notebooks" sidebar page. - Install great-tables in the docs CI: the gt-basics notebook imports it at build time (MarimoIslandGenerator runs the cells to capture static output), so without it the Great Tables island baked in a "No module named 'great_tables'" error and the setup block collapsed to just `import mo`. - Surface WASM-export failures: the export subprocess ran with its exit code and stderr ignored, so a failure left iframe-mode shortcodes pointing at a missing index.html (404) with nothing in the build log. Now warn on non-zero exit or a missing index.html. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/docs.yml | 2 +- great-docs.yml | 1 + great_docs/core.py | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9bab21fa..114a6e09 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e ".[mcp,marimo]" - python -m pip install jupyter polars pandas pyarrow plotnine + python -m pip install jupyter polars pandas pyarrow plotnine great-tables - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 diff --git a/great-docs.yml b/great-docs.yml index 39fa3bde..1f255208 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -199,6 +199,7 @@ nav_icons: Blog: pen-line Diagrams: shapes Videos: video + Marimo Notebooks: notebook-pen Building & Previewing: hammer Freeze & Caching: snowflake Deployment: cloud-upload diff --git a/great_docs/core.py b/great_docs/core.py index ee09e31c..c8629bd8 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -372,7 +372,7 @@ def _prepare_build_directory(self) -> None: for nb_file in notebooks_src.glob("*.py"): wasm_dir = self.project_path / "notebooks" / nb_file.stem wasm_dir.mkdir(parents=True, exist_ok=True) - subprocess.run( + result = subprocess.run( [ _sys.executable, "-m", @@ -389,6 +389,15 @@ def _prepare_build_directory(self) -> None: capture_output=True, text=True, ) + # Surface failures: a silent export failure otherwise + # leaves iframe-mode shortcodes pointing at a missing + # index.html (404) with no explanation in the build log. + if result.returncode != 0 or not (wasm_dir / "index.html").exists(): + _orig_stderr.write( + f"Warning: marimo WASM export failed for " + f"{nb_file.name} (iframe mode will 404). " + f"{(result.stderr or result.stdout or '').strip()[:500]}\n" + ) finally: _sys.stdout = _orig_stdout _sys.stderr = _orig_stderr From 3696b09e9cfdde23b780b997317ac68c2f325e8b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 3 Aug 2026 21:08:30 -0400 Subject: [PATCH 27/36] Make preview artifact download resilient to timeouts --- great_docs/_pr_preview.py | 84 ++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/great_docs/_pr_preview.py b/great_docs/_pr_preview.py index c7dbc803..21362ca9 100644 --- a/great_docs/_pr_preview.py +++ b/great_docs/_pr_preview.py @@ -27,6 +27,10 @@ DEFAULT_ARTIFACT = "docs-html" _TIMEOUT = 15 _DOWNLOAD_TIMEOUT = 300 +# Per-read timeout for the streamed artifact download (generous: blob storage +# can be slow) and how many times to retry, resuming from bytes already on disk. +_READ_TIMEOUT = 120 +_DOWNLOAD_RETRIES = 5 class PreviewError(Exception): @@ -363,39 +367,81 @@ def _gh_download(self, run_id: int, artifact: dict[str, Any], dest: Path) -> Non raise PreviewError(f"'gh run download' failed: {result.stderr.strip()}") def _requests_download(self, artifact: dict[str, Any], dest: Path) -> None: + import time + import requests url = artifact.get("archive_download_url") or ( f"{GITHUB_API}/repos/{self.owner}/{self.repo}/actions/artifacts/{artifact['id']}/zip" ) - headers = {"Accept": "application/vnd.github+json"} + base_headers = {"Accept": "application/vnd.github+json"} if self.token: - headers["Authorization"] = f"Bearer {self.token}" + base_headers["Authorization"] = f"Bearer {self.token}" zip_path = dest / "_artifact.zip" - try: - with requests.get(url, headers=headers, timeout=_TIMEOUT, stream=True) as resp: - if resp.status_code == 410: + zip_path.unlink(missing_ok=True) + + # Artifacts can be hundreds of MB from slow blob storage. A single stalled + # read shouldn't lose the whole transfer, so retry on network errors and + # resume from the bytes already on disk via a Range request (the blob + # store returns 206 + the remainder; if it ignores Range and returns 200, + # we restart the file). + last_exc: Exception | None = None + for attempt in range(_DOWNLOAD_RETRIES): + have = zip_path.stat().st_size if zip_path.exists() else 0 + headers = dict(base_headers) + if have: + headers["Range"] = f"bytes={have}-" + try: + # (connect timeout, per-read timeout) — a generous read timeout + # tolerates slow chunks without abandoning the download. + with requests.get( + url, headers=headers, timeout=(_TIMEOUT, _READ_TIMEOUT), stream=True + ) as resp: + if resp.status_code == 410: + raise PreviewError( + "This artifact has expired and can no longer be downloaded. " + "Re-run the workflow to regenerate it." + ) + if resp.status_code == 206: # resuming + mode, start = "ab", have + total = have + int(resp.headers.get("Content-Length") or 0) + elif resp.status_code == 200: # Range ignored; start over + mode, start = "wb", 0 + total = int(resp.headers.get("Content-Length") or 0) + else: + raise PreviewError( + f"Artifact download failed (HTTP {resp.status_code})." + ) + _stream_to_file(resp, zip_path, total, mode=mode, start=start) + break # completed + except requests.RequestException as exc: + last_exc = exc + if attempt == _DOWNLOAD_RETRIES - 1: raise PreviewError( - "This artifact has expired and can no longer be downloaded. " - "Re-run the workflow to regenerate it." - ) - if resp.status_code != 200: - raise PreviewError(f"Artifact download failed (HTTP {resp.status_code}).") - total = int(resp.headers.get("Content-Length") or 0) - _stream_to_file(resp, zip_path, total) - except requests.RequestException as exc: - raise PreviewError(f"Artifact download failed: {exc}") from exc + f"Artifact download failed after {_DOWNLOAD_RETRIES} attempts: {exc}" + ) from exc + got = zip_path.stat().st_size if zip_path.exists() else 0 + print( + f" … download interrupted ({type(exc).__name__}); " + f"resuming from {got / 1e6:.1f} MB " + f"(attempt {attempt + 2}/{_DOWNLOAD_RETRIES})", + file=sys.stderr, + ) + time.sleep(2 * (attempt + 1)) _safe_extract_zip(zip_path, dest) zip_path.unlink(missing_ok=True) -def _stream_to_file(resp: Any, zip_path: Path, total: int) -> None: +def _stream_to_file( + resp: Any, zip_path: Path, total: int, mode: str = "wb", start: int = 0 +) -> None: """Stream a response body to disk, showing a progress bar on an interactive terminal. Progress is rendered to stderr only when it's a TTY and the size is known. Otherwise the - download runs quietly (e.g. in CI logs or when piped). + download runs quietly (e.g. in CI logs or when piped). ``mode`` is the file open mode + (``"ab"`` to resume) and ``start`` is the byte count already on disk, used to seed the bar. """ chunk_size = 1 << 16 show_bar = total > 0 and sys.stderr.isatty() @@ -404,18 +450,20 @@ def _stream_to_file(resp: Any, zip_path: Path, total: int) -> None: import click with ( - open(zip_path, "wb") as handle, + open(zip_path, mode) as handle, click.progressbar( length=total, label="→ Downloading", file=sys.stderr, ) as bar, ): + if start: + bar.update(start) for chunk in resp.iter_content(chunk_size=chunk_size): handle.write(chunk) bar.update(len(chunk)) else: - with open(zip_path, "wb") as handle: + with open(zip_path, mode) as handle: for chunk in resp.iter_content(chunk_size=chunk_size): handle.write(chunk) From 41fecb6ba5389df3dd3eaf8220742f5777080d0b Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 4 Aug 2026 00:39:29 -0400 Subject: [PATCH 28/36] Add uv to marimo extras --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 466d0013..2c6f4687 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,9 @@ mcp = [ ] marimo = [ "marimo>=0.20.0", + # marimo's `export html-wasm` (iframe mode) shells out to uv to resolve the + # notebook's inline (PEP 723) dependencies. + "uv>=0.5.0", ] dev = [ "pytest>=6.0", From ef2b96e8a48bac5c3b7e5e00d229abd48b9bbe91 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Tue, 4 Aug 2026 09:28:26 -0400 Subject: [PATCH 29/36] Pin great-tables below 0.22 --- .github/workflows/docs.yml | 2 +- notebooks/gt-basics.py | 7 +++++-- notebooks/gt-output-demo.py | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 114a6e09..80e76029 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e ".[mcp,marimo]" - python -m pip install jupyter polars pandas pyarrow plotnine great-tables + python -m pip install jupyter polars pandas pyarrow plotnine 'great-tables<0.22' - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 diff --git a/notebooks/gt-basics.py b/notebooks/gt-basics.py index 8134772d..0fdb06d9 100644 --- a/notebooks/gt-basics.py +++ b/notebooks/gt-basics.py @@ -2,7 +2,10 @@ # requires-python = ">=3.10" # dependencies = [ # "marimo", -# "great-tables", +# # great-tables >= 0.22 depends on multimark, a compiled package with no +# # pure-Python / WASM wheel, so it can't install under Pyodide. Pin to the +# # last pure-Python release (0.21.x) so the notebook runs in the browser. +# "great-tables<0.22", # "polars", # ] # /// @@ -26,7 +29,7 @@ async def __(): if "pyodide" in sys.modules: import micropip - await micropip.install(["great-tables", "polars"]) + await micropip.install(["great-tables<0.22", "polars"]) import great_tables as gt import polars as pl diff --git a/notebooks/gt-output-demo.py b/notebooks/gt-output-demo.py index 1be8a301..0981bd6c 100644 --- a/notebooks/gt-output-demo.py +++ b/notebooks/gt-output-demo.py @@ -2,7 +2,10 @@ # requires-python = ">=3.10" # dependencies = [ # "marimo", -# "great-tables", +# # great-tables >= 0.22 depends on multimark, a compiled package with no +# # pure-Python / WASM wheel, so it can't install under Pyodide. Pin to the +# # last pure-Python release (0.21.x) so the notebook runs in the browser. +# "great-tables<0.22", # "polars", # ] # /// @@ -24,7 +27,7 @@ async def __(): if "pyodide" in sys.modules: import micropip - await micropip.install(["great-tables", "polars"]) + await micropip.install(["great-tables<0.22", "polars"]) import great_tables as _gt import polars as _pl From f04f5af78d008dfba75503f9a1624af3652c1778 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 12 Aug 2026 18:47:51 -0400 Subject: [PATCH 30/36] Add marimo iframe chrome toggle --- great_docs/assets/_extensions/marimo/marimo.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/great_docs/assets/_extensions/marimo/marimo.lua b/great_docs/assets/_extensions/marimo/marimo.lua index ffb5f8af..cbb2db7a 100644 --- a/great_docs/assets/_extensions/marimo/marimo.lua +++ b/great_docs/assets/_extensions/marimo/marimo.lua @@ -63,6 +63,10 @@ return { local show_copy = kwarg(kwargs, "show-copy", "true") local theme = kwarg(kwargs, "theme", "auto") local height = kwarg(kwargs, "height", "600px") + -- iframe chrome: "trimmed" (default) hides marimo's editor sidebar, + -- add-cell bar, and status bar while keeping cells editable/reactive; + -- "full" shows the complete editor chrome. + local chrome = kwarg(kwargs, "chrome", "trimmed") -- IFRAME MODE -------------------------------------------------------- if mode == "iframe" then @@ -71,6 +75,10 @@ return { offset = quarto.project.offset .. "/" end local wasm_path = file:gsub("%.py$", "") .. "/index.html" + -- marimo reads ?show-chrome to toggle the editor sidebar/footer/status. + if chrome ~= "full" then + wasm_path = wasm_path .. "?show-chrome=false" + end local parts = {} table.insert(parts, '
') table.insert(parts, '