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..838cd4f2d5 --- /dev/null +++ b/packages/core/src/code-editor/fileKind.test.ts @@ -0,0 +1,56 @@ +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], + // Dotfiles: the name after the leading dot is read as the extension, so a + // file literally named ".md" matches, while ".gitignore" does not. + [".md", true], + [".gitignore", 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], + // Dotfiles: ".html" reads as the html extension; ".htaccess" does not. + [".html", true], + [".htaccess", 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], + [".gitignore", null], + [".htaccess", 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. + */} +