/* Copyright 2026 Marimo. All rights reserved. */ import { ISLAND_DATA_ATTRIBUTES, ISLAND_TAG_NAMES, } from "@/core/islands/constants"; import { Logger } from "@/utils/Logger"; /** * DOM elements look like this: * * *
Hello, world!
*
* * encoded(print("Hello, world!")) * *
*/ export interface MarimoIslandApp { /** * ID since we allow multiple apps on the same page. */ id: string; /** * Cells in the app. */ cells: MarimoIslandCell[]; } interface MarimoIslandCell { /** * Output of the cell. */ output: string; /** * Code of the cell. */ code: string; /** * Index of the cell. */ idx: number; } /** * Parses marimo island apps from the DOM * @param root - Root element to search within (defaults to document) */ export function parseMarimoIslandApps( root: Document | Element = document, ): MarimoIslandApp[] { const embeds = root.querySelectorAll(ISLAND_TAG_NAMES.ISLAND); if (embeds.length === 0) { Logger.warn("No embedded marimo apps found."); return []; } // eslint-disable-next-line prefer-spread return parseIslandElementsIntoApps(Array.from(embeds)); } /** * Pure function to parse island elements into app structures * @param embeds - Array of island HTML elements */ export function parseIslandElementsIntoApps( embeds: HTMLElement[], ): MarimoIslandApp[] { const apps = new Map(); for (const embed of embeds) { const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID); if (!appId) { Logger.warn("Embedded marimo cell missing data-app-id attribute."); continue; } // Non-reactive islands are static — they don't participate in the kernel const reactive = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true"; if (!reactive) { continue; } const cellData = parseIslandElement(embed); if (!cellData) { Logger.warn(`Embedded marimo app ${appId} missing cell output or code.`); continue; } if (!apps.has(appId)) { apps.set(appId, { id: appId, cells: [] }); } const app = apps.get(appId)!; const idx = app.cells.length; app.cells.push({ output: cellData.output, code: cellData.code, idx: idx, }); // Add data-cell-idx attribute to the island element embed.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString()); } return [...apps.values()]; } /** * Parses a single island element into cell data * @param embed - The island HTML element * @returns Cell data or null if invalid */ export function parseIslandElement( embed: HTMLElement, ): { output: string; code: string } | null { const cellOutput = embed.querySelector( ISLAND_TAG_NAMES.CELL_OUTPUT, ); const code = extractIslandCodeFromEmbed(embed); if (!cellOutput || !code) { return null; } return { output: cellOutput.innerHTML, code: code, }; } export function createMarimoFile(app: { cells: { code: string }[] }): string { const lines = [ "import marimo", "app = marimo.App()", app.cells .map((cell) => { // Add 4 spaces to each line const code = cell.code .split("\n") .map((line) => ` ${line}`) .join("\n"); // TODO: Handle async cells better // This is probably not the best way to check if the code is async // Ideally this is pushed into the Python code const isAsync = code.includes("await "); const prefix = isAsync ? "async def" : "def"; // Wrap in a function return `@app.cell\n${prefix} __():\n${code}\n return`; }) .join("\n"), ]; return lines.join("\n"); } export function parseIslandEditor(code: string | undefined | null): string { if (!code) { return ""; } try { return `${JSON.parse(code)}`; } catch { return code; } } export function parseIslandCode(code: string | undefined | null): string { if (!code) { return ""; } return decodeURIComponent(code).trim(); } export function extractIslandCodeFromEmbed(embed: HTMLElement): string { const reactive = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true"; // Non-reactive cells are not guaranteed to have code, and should be treated as // such. if (!reactive) { return ""; } const cellCodeElement = embed.querySelector( ISLAND_TAG_NAMES.CELL_CODE, ); if (cellCodeElement) { return parseIslandCode(cellCodeElement.textContent); } const editorCodeElement = embed.querySelector( ISLAND_TAG_NAMES.CODE_EDITOR, ); if (editorCodeElement) { return parseIslandEditor( editorCodeElement.getAttribute("data-initial-value"), ); } return ""; }