/* Copyright 2026 Marimo. All rights reserved. */ import parse, { type DOMNode, Element, Text } from "html-react-parser"; import type { CellId } from "@/core/cells/ids"; /** * Check if a DOM node matches a selector. */ export const matchesSelector = (domNode: Element, selector: string) => { const [tagName, ...classes] = selector.split("."); // Note domhandler.Element does not have a classList property, just an // (optional) string attribute. const classList = (domNode.attribs.class || "").split(" "); return ( domNode.tagName === tagName && classes.every((cls) => classList.includes(cls)) ); }; /** * Check if a DOM node contains a marimo cell file. */ export const elementContainsMarimoCellFile = (domNode: Element) => { return ( domNode && matchesSelector(domNode, "span.nb") && domNode.firstChild instanceof Text && domNode.firstChild.nodeValue?.includes("__marimo__") ); }; export type TracebackInfo = | { kind: "file"; filePath: string; lineNumber: number; } | { kind: "cell"; cellId: CellId; lineNumber: number; }; /** * Extract the cell id and line number from a traceback DOM node. * * Example transformation: * * File "/tmp/marimo_/__marimo__cell_.py" * , line 1... * * becomes * * { kind: "cell", cellId: , lineNumber: 1 } * * or for files: * * File "/path/to/file.py" * , line 42... * * becomes * * { kind: "file", filePath: "/path/to/file.py", lineNumber: 42 } */ export function getTracebackInfo(domNode: DOMNode): TracebackInfo | null { // The traceback can be manipulated either in output render or in the pygments // parser. pygments extracts tokens and maps them to tags, but has no // inherent knowledge of the traceback structure, so the methodology would // have to be similar. Moreover, the client side "cell-id" is particular to // frontend, so frontend handling would have to occur anyway. // // A little verbose working with intermediate representation, but best reference // for documentation is found in library source (@domhandler/src/node.ts) // // Expected to transform: // // File "/tmp/marimo_/__marimo__cell_.py // , line 1... // // into // // File marimo://notebook#cell=, line 1, in if ( domNode instanceof Element && domNode.firstChild instanceof Text && matchesSelector(domNode, "span.nb") ) { const nextSibling = domNode.next; if (nextSibling && nextSibling instanceof Text) { const lineSibling = nextSibling.next; if ( lineSibling && lineSibling instanceof Element && lineSibling.firstChild instanceof Text && matchesSelector(lineSibling, "span.m") ) { const lineNumber = Number.parseInt( lineSibling.firstChild.nodeValue || "0", 10, ); if (domNode.firstChild.nodeValue?.includes("__marimo__")) { const maybeCellId = /__marimo__cell_(\w+)_/.exec( domNode.firstChild.nodeValue, )?.[1]; if (maybeCellId && lineNumber) { // @ts-expect-error - Custom parser above will return valid cell ids const cellId: CellId = maybeCellId; return { kind: "cell", cellId, lineNumber }; } } else { const filePath = /"(.+?)"/.exec(domNode.firstChild.nodeValue)?.[1]; if (filePath && lineNumber) { return { kind: "file", filePath, lineNumber }; } } } } } return null; } export function extractAllTracebackInfo(traceback: string): TracebackInfo[] { const infos: TracebackInfo[] = []; // Parse the traceback to recurse over the DOM. // We don't do anything with the result. parse(traceback, { replace: (domNode) => { const info = getTracebackInfo(domNode); if (info) { infos.push(info); return "dummy"; } }, }); return infos; }