From 0f18deec05798418eeaea2d60ce84c5bfb431f3f Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 11:22:00 +0100 Subject: [PATCH 1/6] Create draw a cat sample project --- src/projects/draw-a-cat.json | 36 ++++++++++++++++++++++++++++++++++++ web-component.html | 1 + 2 files changed, 37 insertions(+) create mode 100644 src/projects/draw-a-cat.json diff --git a/src/projects/draw-a-cat.json b/src/projects/draw-a-cat.json new file mode 100644 index 000000000..7fabfaf77 --- /dev/null +++ b/src/projects/draw-a-cat.json @@ -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": "\n\n \n \n \n p5 Image Repro\n \n \n \n \n
\n

p5 image loading repro

\n

Waiting to load cat.png\u2026

\n
\n
\n \n \n\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": "https://editor-api.raspberrypi.org/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6ImUwZmQ2ZmVlLTk0ZDQtNDM5OC04NDllLTc5MWM2MmU4NGNlMyIsInB1ciI6ImJsb2JfaWQifX0=--98515ee7d550c47a1b458a151287740fdb865b8f/cat.png" + } + ], + "videos": [], + "audio": [] +} diff --git a/web-component.html b/web-component.html index 4d8756389..a15e8bbde 100644 --- a/web-component.html +++ b/web-component.html @@ -79,6 +79,7 @@ Load Sample Projects: blank-html-starter cool-html + draw-a-cat blank-python-starter cool-python python-plotly From 8f23a9708166ff6f59f7f6024a7500a2f969ceb9 Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 11:33:37 +0100 Subject: [PATCH 2/6] Substitute project media URLs in JS files, add tests --- .../Runners/HtmlRunner/HtmlRenderer.jsx | 24 ++++--- .../Runners/HtmlRunner/HtmlRenderer.test.jsx | 62 +++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx index 2291ed991..1dacd2131 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx @@ -17,15 +17,25 @@ 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 escapeForRegExp = (string) => + string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const substituteProjectMedia = (projectFile, projectMedia) => { let updatedProjectFile = { ...projectFile }; - if (projectFile.extension === "css") { + if (mediaSubstitutedExtensions.includes(projectFile.extension)) { projectMedia.forEach((media_file) => { - const find = new RegExp(`['"]${media_file.filename}['"]`, "g"); // prevent substring matches - const replace = `"${media_file.url}"`; + const find = new RegExp( + `['"]${escapeForRegExp(media_file.filename)}['"]`, + "g", + ); // prevent substring matches updatedProjectFile.content = updatedProjectFile.content.replaceAll( find, - replace, + () => `"${media_file.url}"`, // callback, so $-sequences in the URL are not expanded ); }); } @@ -54,7 +64,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); @@ -113,7 +123,7 @@ const replaceSrcNodes = ( src = projectMediaFile.url; } else if (!!projectTextFile) { src = getBlobURL( - projectTextFile.content, + substituteProjectMedia(projectTextFile, projectMedia).content, mimeTypes.lookup( `${projectTextFile.name}.${projectTextFile.extension}`, ), diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx index 9a7f2ca3e..7297f2a53 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx @@ -70,6 +70,28 @@ const mediaProject = { ], }; +const scriptMediaProject = { + components: [ + { + name: "index", + extension: "html", + content: '', + }, + { + name: "sketch", + extension: "js", + content: + 'let img;\nfunction preload() {\n img = loadImage("image.jpeg");\n}\n', + }, + ], + image_list: [ + { + filename: "image.jpeg", + url: "https://example.com/image.jpeg", + }, + ], +}; + const allowedExternalLink = { name: "allowed_external_link", extension: "html", @@ -285,4 +307,44 @@ describe("When run is triggered", () => { }); }); }); + + describe("When a JavaScript file references project media", () => { + beforeEach(() => { + global.Blob.mockClear(); + window.postMessage( + { + type: MSG_HTML_PROJECT_UPDATE, + code: scriptMediaProject.components, + media: scriptMediaProject.image_list, + current: scriptMediaProject.components[0].content, + }, + "*", + ); + }); + + test("Substitutes the media URL into the script contents", async () => { + await waitFor(() => { + const blobbedContents = global.Blob.mock.calls.map( + (call) => call[0][0], + ); + expect(blobbedContents).toContainEqual( + expect.stringContaining( + 'loadImage("https://example.com/image.jpeg")', + ), + ); + }); + }); + + test("Does not leave the bare filename in the script contents", async () => { + await waitFor(() => { + const blobbedContents = global.Blob.mock.calls.map( + (call) => call[0][0], + ); + expect(blobbedContents.length).toBeGreaterThan(0); + blobbedContents.forEach((contents) => { + expect(contents).not.toContain('"image.jpeg"'); + }); + }); + }); + }); }); From 2f8a4e478d4a0d41c818dda8668d1ebbe563d142 Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 11:41:13 +0100 Subject: [PATCH 3/6] Update var for consistency --- src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx index 1dacd2131..3de86549f 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx @@ -28,14 +28,14 @@ const escapeForRegExp = (string) => const substituteProjectMedia = (projectFile, projectMedia) => { let updatedProjectFile = { ...projectFile }; if (mediaSubstitutedExtensions.includes(projectFile.extension)) { - projectMedia.forEach((media_file) => { + projectMedia.forEach((mediaFile) => { const find = new RegExp( - `['"]${escapeForRegExp(media_file.filename)}['"]`, + `['"]${escapeForRegExp(mediaFile.filename)}['"]`, "g", ); // prevent substring matches updatedProjectFile.content = updatedProjectFile.content.replaceAll( find, - () => `"${media_file.url}"`, // callback, so $-sequences in the URL are not expanded + () => `"${mediaFile.url}"`, // callback, so $-sequences in the URL are not expanded ); }); } From 91fd7f9884bef3fe9342fcf54db505125460c518 Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 12:13:31 +0100 Subject: [PATCH 4/6] Update test --- .../Runners/HtmlRunner/HtmlRenderer.test.jsx | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx index 07a571dce..38d651ee9 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx @@ -309,8 +309,26 @@ 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(() => { - global.Blob.mockClear(); + 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, @@ -322,11 +340,13 @@ describe("When run is triggered", () => { ); }); + afterEach(() => { + global.Blob = originalBlob; + window.URL.createObjectURL = originalCreateObjectURL; + }); + test("Substitutes the media URL into the script contents", async () => { await waitFor(() => { - const blobbedContents = global.Blob.mock.calls.map( - (call) => call[0][0], - ); expect(blobbedContents).toContainEqual( expect.stringContaining( 'loadImage("https://example.com/image.jpeg")', @@ -337,9 +357,6 @@ describe("When run is triggered", () => { test("Does not leave the bare filename in the script contents", async () => { await waitFor(() => { - const blobbedContents = global.Blob.mock.calls.map( - (call) => call[0][0], - ); expect(blobbedContents.length).toBeGreaterThan(0); blobbedContents.forEach((contents) => { expect(contents).not.toContain('"image.jpeg"'); From 2350faea8e296e0c8ebe13f6291a7d10f29ecacd Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 12:15:01 +0100 Subject: [PATCH 5/6] Update test data to prevent cors fail --- src/projects/draw-a-cat.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/projects/draw-a-cat.json b/src/projects/draw-a-cat.json index 7fabfaf77..1a0e9fd63 100644 --- a/src/projects/draw-a-cat.json +++ b/src/projects/draw-a-cat.json @@ -28,7 +28,7 @@ "image_list": [ { "filename": "cat.png", - "url": "https://editor-api.raspberrypi.org/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6ImUwZmQ2ZmVlLTk0ZDQtNDM5OC04NDllLTc5MWM2MmU4NGNlMyIsInB1ciI6ImJsb2JfaWQifX0=--98515ee7d550c47a1b458a151287740fdb865b8f/cat.png" + "url": "data:image/png;base64,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" } ], "videos": [], From 95d53da0d699de5f99d6db40366aaf6f455f7d4a Mon Sep 17 00:00:00 2001 From: Jamie Benstead Date: Mon, 24 Aug 2026 14:55:48 +0100 Subject: [PATCH 6/6] Update to use replaceAll --- .../Editor/Runners/HtmlRunner/HtmlRenderer.jsx | 18 +++++++----------- .../Runners/HtmlRunner/HtmlRenderer.test.jsx | 14 +++++++++++++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx index 3de86549f..4272e80a6 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx @@ -22,21 +22,17 @@ const parentTag = (node, tag) => // swapped for the asset's real URL before the file is handed to the iframe. const mediaSubstitutedExtensions = ["css", "js"]; -const escapeForRegExp = (string) => - string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const substituteProjectMedia = (projectFile, projectMedia) => { let updatedProjectFile = { ...projectFile }; if (mediaSubstitutedExtensions.includes(projectFile.extension)) { projectMedia.forEach((mediaFile) => { - const find = new RegExp( - `['"]${escapeForRegExp(mediaFile.filename)}['"]`, - "g", - ); // prevent substring matches - updatedProjectFile.content = updatedProjectFile.content.replaceAll( - find, - () => `"${mediaFile.url}"`, // callback, so $-sequences in the URL are not expanded - ); + // 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; diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx index 38d651ee9..d9bf85493 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx @@ -81,7 +81,7 @@ const scriptMediaProject = { name: "sketch", extension: "js", content: - 'let img;\nfunction preload() {\n img = loadImage("image.jpeg");\n}\n', + "let img;\nfunction preload() {\n img = loadImage(\"image.jpeg\");\n bg = loadImage('cat (1)+x.jpeg');\n}\n", }, ], image_list: [ @@ -89,6 +89,10 @@ const scriptMediaProject = { filename: "image.jpeg", url: "https://example.com/image.jpeg", }, + { + filename: "cat (1)+x.jpeg", + url: "https://example.com/cat.jpeg", + }, ], }; @@ -355,6 +359,14 @@ describe("When run is triggered", () => { }); }); + 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);