) : kind === "pdf" && url ? (
+ ) : kind === "html" ? (
+
) : kind === "text" ? (
{text}
) : (
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 7a27d3b..d5c1b43 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -78,7 +78,7 @@ export {
} from "./context-bundle";
export { startWindowDrag } from "./window-drag";
export { exportPreviewToPdf, PdfExportError } from "./pdf-export";
-export { previewKindForPath, previewMimeForPath, type PreviewKind } from "./preview";
+export { previewKindForPath, previewMimeForPath, htmlDocWithBase, type PreviewKind } from "./preview";
export { IS_MAC, IS_WINDOWS, IS_LINUX, displayKey, shortcutLabel } from "./platform";
export {
pickFolder,
diff --git a/src/lib/preview.ts b/src/lib/preview.ts
index 1dad20d..ccbe3dd 100644
--- a/src/lib/preview.ts
+++ b/src/lib/preview.ts
@@ -3,8 +3,10 @@
*
* The sidebar opens `.md/.markdown/.mdx/.csv` in the editor; every other file
* goes through the preview overlay. `previewKindForPath` decides which renderer
- * the overlay uses. Mime values are webview-native (no parsing library) so the
- * overlay can build blob/data URLs straight from `readFile` bytes.
+ * the overlay uses (`.html/.htm` render in a sandboxed iframe with a `
`
+ * pointing at the file's directory via the asset protocol). Mime values are
+ * webview-native (no parsing library) so the overlay can build blob/data URLs
+ * straight from `readFile` bytes.
*/
export type PreviewKind =
@@ -12,6 +14,7 @@ export type PreviewKind =
| "video"
| "audio"
| "pdf"
+ | "html"
| "office"
| "text"
| "unsupported";
@@ -87,8 +90,6 @@ const TEXT_EXT = new Set([
"properties",
"env",
"xml",
- "html",
- "htm",
"css",
"scss",
"less",
@@ -170,6 +171,7 @@ export function previewKindForPath(path: string): PreviewKind {
if (VIDEO_MIME[ext]) return "video";
if (AUDIO_MIME[ext]) return "audio";
if (ext === "pdf") return "pdf";
+ if (ext === "html" || ext === "htm") return "html";
if (OFFICE_EXT.has(ext)) return "office";
if (TEXT_EXT.has(ext)) return "text";
if (TEXT_BASENAMES.has(basenameLower(path))) return "text";
@@ -182,3 +184,21 @@ export function previewMimeForPath(path: string): string {
if (ext === "pdf") return "application/pdf";
return IMAGE_MIME[ext] ?? VIDEO_MIME[ext] ?? AUDIO_MIME[ext] ?? "";
}
+
+/**
+ * Inject a `
` into an html document so the previewed file resolves
+ * relative resources (images, css, scripts) against its own directory.
+ * `baseHref` is a `convertFileSrc()` asset URL of that directory. An existing
+ * `
` in the document would lose (the first one wins), so local files
+ * keep working.
+ */
+export function htmlDocWithBase(html: string, baseHref: string): string {
+ const href = baseHref.endsWith("/") ? baseHref : `${baseHref}/`;
+ const base = `
`;
+ const head = /]*>/i.exec(html);
+ if (head) {
+ const at = head.index + head[0].length;
+ return html.slice(0, at) + base + html.slice(at);
+ }
+ return base + html;
+}
diff --git a/src/styles/overlays/file-preview.css b/src/styles/overlays/file-preview.css
index 5ab8008..5ad3566 100644
--- a/src/styles/overlays/file-preview.css
+++ b/src/styles/overlays/file-preview.css
@@ -276,6 +276,16 @@ html.is-mac .mdv-file-viewer__toolbar {
background: var(--surface);
}
+/* html — pages assume a white canvas, so don't inherit the app theme. */
+.mdv-file-viewer__html {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ background: #fff;
+}
+
/* text */
.mdv-file-viewer__text {
position: absolute;
diff --git a/tests/preview.test.ts b/tests/preview.test.ts
index c3f5fb2..51420e5 100644
--- a/tests/preview.test.ts
+++ b/tests/preview.test.ts
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
-import { previewKindForPath, previewMimeForPath } from "../src/lib/preview";
+import { previewKindForPath, previewMimeForPath, htmlDocWithBase } from "../src/lib/preview";
test("classifies images by extension", () => {
expect(previewKindForPath("a.png")).toBe("image");
@@ -20,6 +20,19 @@ test("classifies pdf", () => {
expect(previewKindForPath("PAPER.PDF")).toBe("pdf");
});
+test("classifies html for rendered preview", () => {
+ expect(previewKindForPath("index.html")).toBe("html");
+ expect(previewKindForPath("docs/page.HTM")).toBe("html");
+});
+
+test("injects a base href for relative html resources", () => {
+ expect(htmlDocWithBase("
x", "asset://localhost/a/b"))
+ .toBe('
x');
+ // no — prepend
+ expect(htmlDocWithBase("
hi
", "asset://localhost/a/b/"))
+ .toBe('
hi
');
+});
+
test("classifies office documents as info-card", () => {
expect(previewKindForPath("report.docx")).toBe("office");
expect(previewKindForPath("sheet.xlsx")).toBe("office");