From 694dbd77d158fbdb3bb922c1bbbb52055a87b0be Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Fri, 19 Jun 2026 16:35:03 +0100 Subject: [PATCH 1/2] feat(code-editor): render HTML files with a preview/source toggle Clicking an HTML file in the file tree now renders it, like markdown, with a toggle to switch between the rendered preview and the raw source. - Add `getRenderableKind()` to core, a single discriminator over the previewable file kinds (markdown, HTML), reusing the shared `getFileExtension` helper. - HTML renders in a null-origin sandboxed iframe (`allow-scripts` without `allow-same-origin`), the same isolation pattern used by the freeform canvas. - Persist the rendered-vs-source preference per kind in a keyed `renderPreview` record so adding a future previewable type is a one-line change. - A header toggle (eye/code) flips between preview and CodeMirror source for any renderable file. Generated-By: PostHog Code Task-Id: f9f523bf-9c44-41e8-a1ed-dd6bc9d52140 --- .../core/src/code-editor/fileKind.test.ts | 47 ++++++++ packages/core/src/code-editor/fileKind.ts | 27 ++++- .../components/CodeEditorPanel.tsx | 100 +++++++++++++----- .../features/code-editor/filePreviewStore.ts | 31 ++++++ 4 files changed, 178 insertions(+), 27 deletions(-) create mode 100644 packages/core/src/code-editor/fileKind.test.ts create mode 100644 packages/ui/src/features/code-editor/filePreviewStore.ts diff --git a/packages/core/src/code-editor/fileKind.test.ts b/packages/core/src/code-editor/fileKind.test.ts new file mode 100644 index 0000000000..81a7c226c6 --- /dev/null +++ b/packages/core/src/code-editor/fileKind.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { getRenderableKind, isHtmlFile, isMarkdownFile } from "./fileKind"; + +describe("isMarkdownFile", () => { + it.each([ + ["README.md", true], + ["notes.markdown", true], + ["UPPER.MD", true], + ["docs/guide.md", true], + ["index.html", false], + ["script.js", false], + ["no-extension", false], + ["", false], + ])("%s -> %s", (filename, expected) => { + expect(isMarkdownFile(filename)).toBe(expected); + }); +}); + +describe("isHtmlFile", () => { + it.each([ + ["index.html", true], + ["page.htm", true], + ["INDEX.HTML", true], + ["build/report.html", true], + ["README.md", false], + ["styles.css", false], + ["no-extension", false], + ["", false], + ])("%s -> %s", (filename, expected) => { + expect(isHtmlFile(filename)).toBe(expected); + }); +}); + +describe("getRenderableKind", () => { + it.each([ + ["README.md", "markdown"], + ["notes.markdown", "markdown"], + ["index.html", "html"], + ["page.htm", "html"], + ["script.js", null], + ["styles.css", null], + ["no-extension", null], + ["", null], + ])("%s -> %s", (filename, expected) => { + expect(getRenderableKind(filename)).toBe(expected); + }); +}); diff --git a/packages/core/src/code-editor/fileKind.ts b/packages/core/src/code-editor/fileKind.ts index 3080c0f0da..92e4a06bb4 100644 --- a/packages/core/src/code-editor/fileKind.ts +++ b/packages/core/src/code-editor/fileKind.ts @@ -1,6 +1,29 @@ +import { getFileExtension } from "@posthog/shared"; + const MARKDOWN_EXTENSIONS = new Set(["md", "markdown"]); +const HTML_EXTENSIONS = new Set(["html", "htm"]); + +export type RenderableKind = "markdown" | "html"; export function isMarkdownFile(filename: string): boolean { - const ext = filename.split(".").pop()?.toLowerCase(); - return !!ext && MARKDOWN_EXTENSIONS.has(ext); + return MARKDOWN_EXTENSIONS.has(getFileExtension(filename)); +} + +export function isHtmlFile(filename: string): boolean { + return HTML_EXTENSIONS.has(getFileExtension(filename)); +} + +/** + * The inline preview a file supports when opened from the file tree, or null if + * it should open as plain source. Add a kind here (plus a renderer branch in + * CodeEditorPanel) to make another file type previewable. + */ +export function getRenderableKind(filename: string): RenderableKind | null { + if (isMarkdownFile(filename)) { + return "markdown"; + } + if (isHtmlFile(filename)) { + return "html"; + } + return null; } diff --git a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx index 624e4d1161..98b2db2451 100644 --- a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx +++ b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx @@ -1,5 +1,5 @@ -import { Check, Copy } from "@phosphor-icons/react"; -import { isMarkdownFile } from "@posthog/core/code-editor/fileKind"; +import { Check, Code, Copy, Eye } from "@phosphor-icons/react"; +import { getRenderableKind } from "@posthog/core/code-editor/fileKind"; import { collapseFileState, resolveMarkdownLink, @@ -25,6 +25,7 @@ import { usePanelLayoutStore } from "../../panels/panelLayoutStore"; import { useFileTreeStore } from "../../right-sidebar/fileTreeStore"; import { useCwd } from "../../sidebar/useCwd"; import { useIsWorkspaceCloudRun } from "../../workspace/useWorkspace"; +import { useFilePreviewStore } from "../filePreviewStore"; import { useCloudFileContent } from "../hooks/useCloudFileContent"; import { useAbsoluteFileContent, @@ -75,6 +76,25 @@ function FilePanelImagePreview({ ); } +function HtmlFilePreview({ content }: { content: string }) { + return ( + + {/* + Render the HTML in a null-origin sandboxed iframe: allow-scripts WITHOUT + allow-same-origin lets scripts run but keeps the document on a null + origin, so it cannot reach the host renderer's DOM, cookies, or storage. + Do not add allow-same-origin — it collapses that isolation boundary. + */} +