Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ import {
const parentTag = (node, tag) =>
node.parentNode?.tagName && node.parentNode.tagName.toLowerCase() === tag;

const cssProjectImgs = (projectFile, projectMedia) => {
// Media filenames are only meaningful inside the editor: the preview has no
// file system to resolve them against, so any reference to one has to be
// swapped for the asset's real URL before the file is handed to the iframe.
const mediaSubstitutedExtensions = ["css", "js"];

const substituteProjectMedia = (projectFile, projectMedia) => {
let updatedProjectFile = { ...projectFile };
if (projectFile.extension === "css") {
projectMedia.forEach((media_file) => {
const find = new RegExp(`['"]${media_file.filename}['"]`, "g"); // prevent substring matches
const replace = `"${media_file.url}"`;
updatedProjectFile.content = updatedProjectFile.content.replaceAll(
find,
replace,
);
if (mediaSubstitutedExtensions.includes(projectFile.extension)) {
projectMedia.forEach((mediaFile) => {
// A callback, so $-sequences in the URL are not expanded as
// replacement patterns.
const mediaUrl = () => `"${mediaFile.url}"`;
// Matched with the quotes included, to prevent substring matches.
updatedProjectFile.content = updatedProjectFile.content
.replaceAll(`"${mediaFile.filename}"`, mediaUrl)
.replaceAll(`'${mediaFile.filename}'`, mediaUrl);
});
}
return updatedProjectFile;
Expand Down Expand Up @@ -54,7 +60,7 @@ const replaceHrefNodes = (indexPage, projectMedia, projectCode) => {
if (!!projectFile) {
if (parentTag(hrefNode, "head")) {
const projectFileBlob = getBlobURL(
cssProjectImgs(projectFile, projectMedia).content,
substituteProjectMedia(projectFile, projectMedia).content,
mimeTypes.lookup(`${projectFile.name}.${projectFile.extension}`),
);
hrefNode.setAttribute("href", projectFileBlob);
Expand Down Expand Up @@ -113,7 +119,7 @@ const replaceSrcNodes = (
src = projectMediaFile.url;
} else if (!!projectTextFile) {
src = getBlobURL(
projectTextFile.content,
substituteProjectMedia(projectTextFile, projectMedia).content,
mimeTypes.lookup(
`${projectTextFile.name}.${projectTextFile.extension}`,
),
Expand Down
91 changes: 91 additions & 0 deletions src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ const mediaProject = {
],
};

const scriptMediaProject = {
components: [
{
name: "index",
extension: "html",
content: '<head></head><body><script src="sketch.js"></script></body>',
},
{
name: "sketch",
extension: "js",
content:
"let img;\nfunction preload() {\n img = loadImage(\"image.jpeg\");\n bg = loadImage('cat (1)+x.jpeg');\n}\n",
},
],
image_list: [
{
filename: "image.jpeg",
url: "https://example.com/image.jpeg",
},
{
filename: "cat (1)+x.jpeg",
url: "https://example.com/cat.jpeg",
},
],
};

const allowedExternalLink = {
name: "allowed_external_link",
extension: "html",
Expand Down Expand Up @@ -285,4 +311,69 @@ describe("When run is triggered", () => {
});
});
});

describe("When a JavaScript file references project media", () => {
// Stub Blob/createObjectURL here rather than relying on the runner's
// setup file, so the blobbed contents can be inspected under both
// Jest and Vitest.
let blobbedContents;
let originalBlob;
let originalCreateObjectURL;

beforeEach(() => {
blobbedContents = [];
originalBlob = global.Blob;
originalCreateObjectURL = window.URL.createObjectURL;

global.Blob = class {
constructor(parts) {
this.parts = parts;
blobbedContents.push(String(parts?.[0] ?? ""));
}
};
window.URL.createObjectURL = () => "blob:stubbed";

window.postMessage(
{
type: MSG_HTML_PROJECT_UPDATE,
code: scriptMediaProject.components,
media: scriptMediaProject.image_list,
current: scriptMediaProject.components[0].content,
},
"*",
);
});

afterEach(() => {
global.Blob = originalBlob;
window.URL.createObjectURL = originalCreateObjectURL;
});

test("Substitutes the media URL into the script contents", async () => {
await waitFor(() => {
expect(blobbedContents).toContainEqual(
expect.stringContaining(
'loadImage("https://example.com/image.jpeg")',
),
);
});
});

test("Substitutes single-quoted filenames containing special characters", async () => {
await waitFor(() => {
expect(blobbedContents).toContainEqual(
expect.stringContaining('loadImage("https://example.com/cat.jpeg")'),
);
});
});

test("Does not leave the bare filename in the script contents", async () => {
await waitFor(() => {
expect(blobbedContents.length).toBeGreaterThan(0);
blobbedContents.forEach((contents) => {
expect(contents).not.toContain('"image.jpeg"');
});
});
});
});
});
36 changes: 36 additions & 0 deletions src/projects/draw-a-cat.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"identifier": "draw-a-cat",
"project_type": "html",
"locale": "en",
"name": "Draw a Cat",
"user_id": null,
"instructions": "",
"components": [
{
"id": "2d45c779-ae6a-4da2-a843-fac28e98e29b",
"name": "index",
"extension": "html",
"content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>p5 Image Repro</title>\n <link rel=\"stylesheet\" href=\"styles.css\" />\n <script src=\"https://rpf.io/p5\"></script>\n </head>\n <body>\n <main>\n <h1>p5 image loading repro</h1>\n <p id=\"status\">Waiting to load cat.png\u2026</p>\n <div id=\"canvas-wrap\"></div>\n </main>\n <script src=\"sketch.js\"></script>\n </body>\n</html>\n"
},
{
"id": "e18c5de0-cea7-4ff8-85b2-2093f52d3e02",
"name": "sketch",
"extension": "js",
"content": "let img;\n\nfunction preload() {\n img = loadImage(\n \"cat.png\",\n () => {\n const status = document.getElementById(\"status\");\n if (status) status.textContent = \"Loaded cat.png successfully.\";\n console.log(\"Image loaded\");\n },\n (err) => {\n const status = document.getElementById(\"status\");\n if (status) status.textContent = \"Failed to load cat.png. Check console/network tab.\";\n console.error(\"Image failed to load\", err);\n }\n );\n}\n\nfunction setup() {\n const canvas = createCanvas(256, 256);\n canvas.parent(\"canvas-wrap\");\n background(245);\n image(img, 0, 0, width, height);\n}\n\nfunction draw() {\n // No-op: static repro.\n}"
},
{
"id": "39661499-1802-4481-9d18-e4711ebd3197",
"name": "styles",
"extension": "css",
"content": "html, body {\n margin: 0;\n padding: 0;\n font-family: Arial, sans-serif;\n background: #f7f7f7;\n color: #222;\n}\n\nmain {\n max-width: 720px;\n margin: 40px auto;\n padding: 24px;\n}\n\nh1 {\n margin-top: 0;\n font-size: 24px;\n}\n\n#status {\n margin: 0 0 16px;\n}\n\ncanvas {\n display: block;\n border: 1px solid #ccc;\n background: white;\n}\n"
}
],
"image_list": [
{
"filename": "cat.png",
"url": "data:image/png;base64,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"
}
],
"videos": [],
"audio": []
}
1 change: 1 addition & 0 deletions web-component.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<span>Load Sample Projects:</span>
<a href="?project=blank-html-starter">blank-html-starter</a>
<a href="?project=cool-html">cool-html</a>
<a href="?project=draw-a-cat">draw-a-cat</a>
<a href="?project=blank-python-starter">blank-python-starter</a>
<a href="?project=cool-python">cool-python</a>
<a href="?project=python-plotly">python-plotly</a>
Expand Down
Loading