From b0faa12fa3279c91153e3a9ebc4cc548c3952984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Vogel?= <104342347+cedricvogel-leo@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:06:30 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Workspace:=20Markdown=20files=20as=20a=20pa?= =?UTF-8?q?ge=20(host's=20renderMarkdown),=20=C2=ABAnsicht=20/=20Text?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the new prop renderMarkdown (the host's renderer, returning sanitized HTML), a .md tab has a switch at the end of the tabs: a read-only file opens as the page, an editable one as its text. The page lies over the editor, which stays mounted (inert meanwhile). Styles in the tokens: headings, lists, code in the code font, tables, quotes. Without the prop, nothing changes (the embed doesn't pass it). Co-Authored-By: Claude Opus 5.5 --- src/GlPythonWorkspace.tsx | 21 +++++++++++++++++++-- src/i18n.ts | 6 ++++++ src/style.css | 30 +++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/GlPythonWorkspace.tsx b/src/GlPythonWorkspace.tsx index 8e559c4..645f1c3 100644 --- a/src/GlPythonWorkspace.tsx +++ b/src/GlPythonWorkspace.tsx @@ -80,6 +80,11 @@ type CoreStrings = { // a box in the substitution and tree modes: the text after the function's // name, e.g. "has local variables – the call is shown as a box." visualizerModeTree?: string + // A Markdown tab's switch (with the host's renderMarkdown): its label, and + // the file as a page or as its text. + markdownViewLabel?: string + markdownPage?: string + markdownText?: string visualizerBoxReason?: (reason: BoxReason) => string // The visualizer's "copy the evaluation as a → chain" button, its // confirmation, its failure message, the switch for the chain's reason @@ -269,6 +274,11 @@ type GlPythonWorkspaceProps = { // host's top bar; without it they sit above the editor. Only in the // stacked (not compact) layout. actionsContainer?: HTMLElement | null + // Draws a Markdown file (".md") as a page: the host's renderer, which must + // return sanitized HTML (the IDE: marked + DOMPurify). With it, a + // Markdown tab has a «View / Text» switch; a read-only file opens as + // the page, an editable one as its text. Without it, Markdown is text. + renderMarkdown?: (source: string) => string } // Busy for less than this, the run button stays as it is, so a quick REPL @@ -1297,6 +1307,7 @@ function GlPythonWorkspace(props: GlPythonWorkspaceProps) { visualizerMode: visualizerModePreset, onBusyChange, actionsContainer = null, + renderMarkdown, } = props const workspaceCapabilities = { ...defaultWorkspaceCapabilities, ...capabilities } const [output, setOutput] = useState(() => t.readyOutput) @@ -1406,6 +1417,12 @@ function GlPythonWorkspace(props: GlPythonWorkspaceProps) { const inputChannelRef = useRef<{ control: Int32Array; payload: Uint8Array } | null>(null) const selectedFile = project.files[activeFile] ?? project.files[project.mainFile] const selectedPath = project.files[activeFile] ? activeFile : project.mainFile + // A Markdown file as a page or as its text (see renderMarkdown), per file: + // a read-only one starts as the page, an editable one as its text. + const [markdownViews, setMarkdownViews] = useState>({}) + const isMarkdown = renderMarkdown !== undefined && /\.md$/i.test(selectedPath) + const markdownView = isMarkdown ? markdownViews[selectedPath] ?? (selectedFile.readOnly ? 'page' : 'text') : 'text' + const markdownHtml = useMemo(() => isMarkdown && markdownView === 'page' ? renderMarkdown(selectedFile.code) : '', [isMarkdown, markdownView, renderMarkdown, selectedFile.code]) // The lines the last Tests run left uncovered (see TestCoverageView), // marked in the editor while that file shows, until it is edited or the // program runs again. @@ -2505,8 +2522,8 @@ function GlPythonWorkspace(props: GlPythonWorkspaceProps) { const breadcrumbsNode =
{project.name}/{selectedFile.path}
const visualizerBarNode =
{visualTraceSteps.length > 0 ? <>
{ setVisualStepIndex(Number(event.target.value)) }} />{errorTraceStep && }
{clampedStepIndex === -1 ? t.visualizerBeforeStart : t.visualizerStepOf(clampedStepIndex + 1, visualTraceSteps.length)}{visualTrace?.truncated && {t.visualizerTruncated}} : {t.visualizerNoSteps}}
{workspaceCapabilities.tree && }
{workspaceCapabilities.listTree && }{workspaceCapabilities.export && <>}{workspaceCapabilities.exitVisualizer && }
const visualizerWarningNode = <>{isVisualizing && visualTrace?.error && !errorTraceStep &&
{t.visualizerError(visualTrace.error)}
} - const editorTabsNode =
{openFiles.map((path) => { const file = project.files[path]; return })}
- const editorWrapNode =
{isVisualizing ?
0 ? 'measuring' : 'done') : undefined}>
{rootFrame(currentStepView)}{animationsEnabled && closingBoxes.length > 0 &&
{closingBoxes.map((box) =>
)}
}{(connectorPaths.length > 0 || flights.length > 0) && {connectorPaths.map((connector, index) => )}{animationsEnabled && flights.map((flight) => )}}{animationsEnabled && flights.length > 0 &&
{flights.map((flight) => {flight.value})}
}
{steadyMeasureNode}
: { registerGlpythonPython(instance); defineGlpythonTheme(instance) }} value={selectedFile.code} onChange={updateCode} onMount={setNormalEditor} options={{ minimap: { enabled: false }, bracketPairColorization: { enabled: false }, fontSize: 15, lineHeight: 24, padding: { top: 22 }, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', scrollBeyondLastLine: false, smoothScrolling: true, automaticLayout: true, tabSize: 4, insertSpaces: true, detectIndentation: false, placeholder: t.editorPlaceholder ?? 'Type code here', unicodeHighlight: EDITOR_UNICODE_HIGHLIGHT, readOnly: selectedFile.readOnly === true, domReadOnly: selectedFile.readOnly === true, readOnlyMessage: { value: t.readOnlyFile ?? 'Read-only: this file can be read but not changed.' } }} />}
+ const editorTabsNode =
{openFiles.map((path) => { const file = project.files[path]; return })}{isMarkdown && !isVisualizing &&
}
+ const editorWrapNode =
{isVisualizing ?
0 ? 'measuring' : 'done') : undefined}>
{rootFrame(currentStepView)}{animationsEnabled && closingBoxes.length > 0 &&
{closingBoxes.map((box) =>
)}
}{(connectorPaths.length > 0 || flights.length > 0) && {connectorPaths.map((connector, index) => )}{animationsEnabled && flights.map((flight) => )}}{animationsEnabled && flights.length > 0 &&
{flights.map((flight) => {flight.value})}
}
{steadyMeasureNode}
: <>{isMarkdown && markdownView === 'page' &&
}
{ registerGlpythonPython(instance); defineGlpythonTheme(instance) }} value={selectedFile.code} onChange={updateCode} onMount={setNormalEditor} options={{ minimap: { enabled: false }, bracketPairColorization: { enabled: false }, fontSize: 15, lineHeight: 24, padding: { top: 22 }, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', scrollBeyondLastLine: false, smoothScrolling: true, automaticLayout: true, tabSize: 4, insertSpaces: true, detectIndentation: false, placeholder: t.editorPlaceholder ?? 'Type code here', unicodeHighlight: EDITOR_UNICODE_HIGHLIGHT, readOnly: selectedFile.readOnly === true, domReadOnly: selectedFile.readOnly === true, readOnlyMessage: { value: t.readOnlyFile ?? 'Read-only: this file can be read but not changed.' } }} />
}
const consoleNode = <>{!hideOutputPanel &&
{t.output}{runtimeState !== 'ready' && {t.pyodideRuntime}}
{Object.keys(project.files).length > 1 && {t.runsFile(project.mainFile)}}
{ const element = event.currentTarget; consoleAtBottomRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 24 }}>{isVisualizing ?
{isAtErrorStep ? <>{visualStdout && !visualStdout.endsWith('\n') ? `${visualStdout}\n` : visualStdout}{visualTrace?.error} : visualStdout || t.noOutput}{compact && }
: <>
{output}
{consoleEntries.map((entry) => entry.kind === 'tests' ?
: entry.kind === 'note' ?

{entry.text}

:
 visualizeReplEntry(entry.id) : undefined} busy={isBusy} />
)}}{answeredInputLines.length > 0 &&
{answeredInputLines.join('\n')}
}
{(workspaceCapabilities.repl || programInput) &&
{ event.preventDefault(); if (programInput) respondToInput(false); else submitRepl() }}> { if (programInput) { setInputValue(event.target.value) } else { setReplInput(event.target.value); setReplHistoryIndex(null) } }} onKeyDown={handleReplKeyDown} aria-label={programInput ? `${t.programInputLabel ?? 'Input for the program'}${inputRequest ? `: ${inputRequest}` : ''}` : t.replInputLabel ?? 'Python input'} spellCheck={false} autoComplete="off" autoCapitalize="off" autoCorrect="off" enterKeyHint="send" />
}
} // In the stacked layout the graphics show only while the program draws - // for a visualization: if it draws at all, so the panel doesn't appear in diff --git a/src/i18n.ts b/src/i18n.ts index 9d6e531..26522ad 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -50,6 +50,9 @@ export const coreStrings: Record = { visualizerModeSubstitution: 'Substitution', visualizerBoxHint: 'The box shows which return is executed.', visualizerModeTree: 'Tree', + markdownViewLabel: 'Show the file', + markdownPage: 'View', + markdownText: 'Text', visualizerBoxReason: (reason) => ({ locals: 'has local variables – the call is shown as a box.', branches: 'uses if to choose what runs – the call is shown as a box.', @@ -145,6 +148,9 @@ export const coreStrings: Record = { visualizerModeSubstitution: 'Ersetzung', visualizerBoxHint: 'Der Kasten zeigt, welches return ausgeführt wird.', visualizerModeTree: 'Baum', + markdownViewLabel: 'Datei zeigen', + markdownPage: 'Ansicht', + markdownText: 'Text', visualizerBoxReason: (reason) => ({ locals: 'hat lokale Variablen – der Aufruf wird als Kasten gezeigt.', branches: 'wählt mit if, was ausgeführt wird – der Aufruf wird als Kasten gezeigt.', diff --git a/src/style.css b/src/style.css index 0e218bc..779e4bc 100644 --- a/src/style.css +++ b/src/style.css @@ -71,7 +71,35 @@ .editor-tab.active .tab-file-icon { color: var(--glp-accent); } .tab-close { display: inline-flex; margin-left: auto; padding: 3px; color: var(--glp-text-muted); border-radius: var(--glp-radius); } .tab-close:hover { color: var(--glp-text); background: var(--glp-border); } -.editor-wrap { min-height: 0; min-width: 0; flex: 1; } +.editor-wrap { position: relative; min-height: 0; min-width: 0; flex: 1; } +/* A Markdown file as a page (renderMarkdown): over the editor, which stays + mounted below it; its switch at the end of the tabs. */ +.editor-host { height: 100%; } +.markdown-switch { display: flex; align-items: center; gap: 2px; margin-left: auto; padding: 0 8px; flex-shrink: 0; } +.markdown-switch button { padding: 4px 10px; border: 1px solid var(--glp-border-control); border-radius: var(--glp-radius); color: var(--glp-text); background: var(--glp-bg); font: 600 12px/1.2 var(--glp-font-ui); cursor: pointer; } +.markdown-switch button[aria-pressed="true"] { color: var(--glp-on-accent); background: var(--glp-accent); border-color: var(--glp-accent); } +.markdown-switch button:focus-visible { outline: 2px solid var(--glp-accent); outline-offset: 2px; } +.markdown-page { position: absolute; inset: 0; z-index: 2; overflow: auto; padding: 20px 28px 32px; color: var(--glp-text); background: var(--glp-bg); font: 15px/1.6 var(--glp-font-ui); overflow-wrap: break-word; } +.markdown-page:focus-visible { outline: 2px solid var(--glp-accent); outline-offset: -2px; } +.markdown-page > :first-child { margin-top: 0; } +.markdown-page h1, .markdown-page h2, .markdown-page h3, .markdown-page h4 { margin: 1.4em 0 .5em; line-height: 1.3; color: var(--glp-text); } +.markdown-page h1 { font-size: 1.6em; } +.markdown-page h2 { font-size: 1.3em; padding-bottom: .2em; border-bottom: 1px solid var(--glp-border); } +.markdown-page h3 { font-size: 1.1em; } +.markdown-page p, .markdown-page ul, .markdown-page ol, .markdown-page blockquote, .markdown-page table, .markdown-page pre { margin: 0 0 1em; } +.markdown-page ul, .markdown-page ol { padding-left: 1.6em; } +.markdown-page li + li { margin-top: .25em; } +.markdown-page a { color: var(--glp-info); } +.markdown-page code { padding: 1px 5px; color: var(--glp-text); background: var(--glp-surface); border-radius: var(--glp-radius); font: .92em var(--glp-font-code); } +.markdown-page pre { overflow-x: auto; padding: 12px 14px; background: var(--glp-surface); border: 1px solid var(--glp-border); border-radius: var(--glp-radius); } +.markdown-page pre code { padding: 0; background: none; font-size: 14px; line-height: 1.5; } +.markdown-page blockquote { padding: 4px 14px; color: var(--glp-text-muted); border-left: 3px solid var(--glp-border-control); } +.markdown-page table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; } +.markdown-page th, .markdown-page td { padding: 6px 10px; border: 1px solid var(--glp-border); text-align: left; vertical-align: top; } +.markdown-page th { background: var(--glp-surface); } +.markdown-page img { max-width: 100%; } +.markdown-page hr { border: 0; border-top: 1px solid var(--glp-border); margin: 1.5em 0; } +@media (max-width: 700px) { .markdown-page { padding: 16px; } } .output-panel { min-height: 142px; background: var(--glp-bg); border: 1px solid var(--glp-border); border-radius: var(--glp-radius); overflow: hidden; } .output-heading { height: 40px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border-bottom: 1px solid var(--glp-border); } From 0df7e82ca24445998896ea0033b11bef54cba3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Vogel?= <104342347+cedricvogel-leo@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:13:07 +0100 Subject: [PATCH 2/2] Visualizer: the file that ran, whichever tab is open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module frame was drawn for the open tab (activeFile), while the trace is of the main file: with another tab open (a helper, a library, a Markdown file), «Visualisieren» showed an empty view. Now it shows the main file (or a visualized REPL input), as the → chain already did. Co-Authored-By: Claude Opus 5.5 --- src/GlPythonWorkspace.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/GlPythonWorkspace.tsx b/src/GlPythonWorkspace.tsx index 645f1c3..0193b26 100644 --- a/src/GlPythonWorkspace.tsx +++ b/src/GlPythonWorkspace.tsx @@ -2092,13 +2092,15 @@ function GlPythonWorkspace(props: GlPythonWorkspaceProps) { if (!text) return t.visualizerBoxHint ?? 'The box shows which return is executed.' return <>{funcName} {text} } - // The module's frame at a step, with the boxes and inline calls inside it. + // The module's frame at a step, with the boxes and inline calls inside it: + // the file that ran (a visualized REPL input's pseudo-file, else the main + // file), whichever tab is open. const rootFrame = (view: StepView) => { const collapsedLabel = view.collapsedCalls ? t.visualizerCollapsedCalls?.(view.collapsedCalls.hiddenCount, view.collapsedCalls.callText) ?? `\u2026 ${view.collapsedCalls.hiddenCount} more calls${view.collapsedCalls.callText ? ` of ${view.collapsedCalls.callText}` : ''} \u2026` : '' - return : null} /> + return : null} /> } const changeVisualizerMode = (mode: VisualizerMode) => { if (mode === effectiveMode) return