7b5abc6ad9
Clicking an .html/.htm file now renders it instead of showing its source or downloading it: - Changes/Files panel: a whole HTML file opens on Preview (Raw = the editor). - Chat file preview: a workspace page brings its relative files along. - File Explorer: double-click / Enter opens a viewer; Download stays on its header and right-click menu. The page runs in the chat artifact's sandbox (iframe srcdoc, scripts, never allow-same-origin). Its relative stylesheets (and their url()s), scripts, images, media and icons are read by the dashboard through the existing download routes and inlined as <style>/data: URLs (lib/htmlInline.ts: never above the folder's root, ≤ 60 files / 30 MB) — the login cookie is SameSite=Strict, so a frame could not fetch them itself, and no new route serves files without it.
54 lines
2.4 KiB
TypeScript
54 lines
2.4 KiB
TypeScript
/**
|
|
* @file htmlInline.test.ts
|
|
* @description An HTML preview's relative CSS, scripts and images are inlined
|
|
* from the place's own files, never from above its root or off a URL.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { inlineHtml, resolveRef } from "../htmlInline";
|
|
|
|
const files: Record<string, string> = {
|
|
"site/css/app.css": "body{background:url(../img/bg.png)}",
|
|
"site/js/app.js": "window.ok=1",
|
|
"site/img/logo.png": "PNG",
|
|
"site/img/bg.png": "BG",
|
|
};
|
|
const fetchFile = vi.fn(async (p: string) => {
|
|
if (!(p in files)) throw new Error("404");
|
|
return new Blob([files[p]!]);
|
|
});
|
|
|
|
describe("resolveRef", () => {
|
|
it("resolves against the page's folder, from the root on a leading slash", () => {
|
|
expect(resolveRef("site", "css/a.css?v=2#x")).toBe("site/css/a.css");
|
|
expect(resolveRef("site/sub", "../img/a%20b.png")).toBe("site/img/a b.png");
|
|
expect(resolveRef("site", "/top.css")).toBe("top.css");
|
|
});
|
|
it("skips URLs, anchors and anything above the root", () => {
|
|
for (const r of ["https://x/a.js", "//cdn/a.js", "data:x", "#top", "", "../../etc/passwd"])
|
|
expect(resolveRef("site", r)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("inlineHtml", () => {
|
|
it("inlines stylesheets (with their url()s), scripts and images", async () => {
|
|
const html = `<!doctype html><html><head><link rel="stylesheet" href="css/app.css">
|
|
<script src="js/app.js"></script><script src="https://cdn.example/x.js"></script></head>
|
|
<body><img src="img/logo.png" srcset="img/logo@2x.png 2x"><img src="missing.png"></body></html>`;
|
|
const out = await inlineHtml(html, "site/index.html", fetchFile);
|
|
expect(out.startsWith("<!doctype html>")).toBe(true);
|
|
expect(out).toContain(`<style>body{background:url("data:image/png;base64,${btoa("BG")}")}</style>`);
|
|
expect(out).toContain(`src="data:text/javascript;base64,${btoa("window.ok=1")}"`);
|
|
expect(out).toContain(`src="data:image/png;base64,${btoa("PNG")}"`);
|
|
expect(out).not.toContain("srcset");
|
|
expect(out).toContain('src="https://cdn.example/x.js"');
|
|
expect(out).toContain('src="missing.png"');
|
|
expect(fetchFile).not.toHaveBeenCalledWith(expect.stringContaining("cdn"));
|
|
});
|
|
it("returns the page untouched when nothing is local", async () => {
|
|
const html = "<p>hi <a href='https://x'>x</a></p>";
|
|
expect(await inlineHtml(html, "a.html", fetchFile)).toBe(html);
|
|
});
|
|
});
|