diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index a21a7c3b7f..8f52b289bc 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -11,6 +11,7 @@ import {
p5DataAddonURL,
p5URLTemplate
} from '../../../../common/p5URLs';
+import { preserveNoProtect } from '../utils/loopProtection';
export const majorVersion = (version) => version.split('.')[0];
@@ -46,7 +47,7 @@ export function P5VersionProvider(props) {
src = `${doctype}\n${src}`;
}
- return src;
+ return preserveNoProtect(indexSrc, src);
};
const usedP5Versions = [...dom.documentElement.querySelectorAll('script')]
diff --git a/client/modules/IDE/hooks/useP5Version.unit.test.jsx b/client/modules/IDE/hooks/useP5Version.unit.test.jsx
new file mode 100644
index 0000000000..2ad73628b5
--- /dev/null
+++ b/client/modules/IDE/hooks/useP5Version.unit.test.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { reduxRender } from '../../../test-utils';
+import { initialState as filesInitialState } from '../reducers/files';
+import { p5PreloadAddonURL } from '../../../../common/p5URLs';
+import { hasNoProtect } from '../utils/loopProtection';
+import { P5VersionProvider, useP5Version } from './useP5Version';
+
+const P5_SCRIPT = 'https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js';
+
+function indexHtml({ noprotect = false, sound = false } = {}) {
+ const soundTag = sound
+ ? '\n '
+ : '';
+ const html = `
+
+
+ ${soundTag}
+
+
+`;
+ return noprotect ? `\n${html}` : html;
+}
+
+let capturedVersionInfo = null;
+
+function VersionProbe() {
+ const { versionInfo } = useP5Version();
+ capturedVersionInfo = versionInfo;
+ return null;
+}
+
+function renderVersionInfo(indexContent) {
+ capturedVersionInfo = null;
+ reduxRender(
+
+
+ ,
+ {
+ initialState: {
+ files: filesInitialState().map((file) => ({
+ ...file,
+ ...(file.fileType === 'file' &&
+ file.name === 'index.html' &&
+ file.filePath === '' && { content: indexContent })
+ }))
+ }
+ }
+ );
+ return capturedVersionInfo;
+}
+
+describe('useP5Version HTML rewrites', () => {
+ afterEach(() => {
+ capturedVersionInfo = null;
+ });
+
+ it('keeps when replaceVersion rewrites index.html', () => {
+ const versionInfo = renderVersionInfo(indexHtml({ noprotect: true }));
+ const next = versionInfo.replaceVersion('2.3.3');
+
+ expect(hasNoProtect(next)).toBe(true);
+ expect(next).toContain('p5@2.3.3/lib/p5.js');
+ expect(next.match(//g)).toHaveLength(1);
+ });
+
+ it('does not insert when the original had none', () => {
+ const versionInfo = renderVersionInfo(indexHtml());
+ const next = versionInfo.replaceVersion('2.3.3');
+
+ expect(hasNoProtect(next)).toBe(false);
+ expect(next).toContain('p5@2.3.3/lib/p5.js');
+ });
+
+ it('keeps when addon setters rewrite index.html', () => {
+ const versionInfo = renderVersionInfo(
+ indexHtml({ noprotect: true, sound: true })
+ );
+
+ const afterSoundOff = versionInfo.setP5Sound(false);
+ expect(hasNoProtect(afterSoundOff)).toBe(true);
+ expect(afterSoundOff).not.toContain('p5.sound');
+
+ const afterPreload = versionInfo.setP5PreloadAddon(true);
+ expect(hasNoProtect(afterPreload)).toBe(true);
+ expect(afterPreload).toContain(p5PreloadAddonURL);
+ });
+});
diff --git a/client/modules/IDE/utils/loopProtection.js b/client/modules/IDE/utils/loopProtection.js
index 49e591496a..72af02872c 100644
--- a/client/modules/IDE/utils/loopProtection.js
+++ b/client/modules/IDE/utils/loopProtection.js
@@ -16,3 +16,11 @@ export function removeNoProtect(src = '') {
export function toggleLoopProtection(src = '', enabled) {
return enabled ? removeNoProtect(src) : addNoProtect(src);
}
+
+// Version/addon HTML rewrites serialize via documentElement.outerHTML, which
+// drops comments outside — including a leading .
+export function preserveNoProtect(originalSrc = '', serializedSrc = '') {
+ return hasNoProtect(originalSrc)
+ ? addNoProtect(serializedSrc)
+ : serializedSrc;
+}
diff --git a/client/modules/IDE/utils/loopProtection.unit.test.js b/client/modules/IDE/utils/loopProtection.unit.test.js
new file mode 100644
index 0000000000..d9483a775d
--- /dev/null
+++ b/client/modules/IDE/utils/loopProtection.unit.test.js
@@ -0,0 +1,64 @@
+import {
+ addNoProtect,
+ hasNoProtect,
+ preserveNoProtect,
+ removeNoProtect,
+ toggleLoopProtection
+} from './loopProtection';
+
+describe('loopProtection helpers', () => {
+ const html = `
+
+
+
+`;
+ const withNoProtect = `\n${html}`;
+
+ describe('hasNoProtect', () => {
+ it('is true when a leading noprotect comment exists', () => {
+ expect(hasNoProtect(withNoProtect)).toBe(true);
+ });
+
+ it('is false when the comment is absent', () => {
+ expect(hasNoProtect(html)).toBe(false);
+ });
+ });
+
+ describe('addNoProtect / removeNoProtect', () => {
+ it('prepends the comment once', () => {
+ expect(addNoProtect(html)).toBe(withNoProtect);
+ expect(addNoProtect(withNoProtect)).toBe(withNoProtect);
+ });
+
+ it('strips the comment', () => {
+ expect(removeNoProtect(withNoProtect)).toBe(html);
+ expect(removeNoProtect(html)).toBe(html);
+ });
+ });
+
+ describe('toggleLoopProtection', () => {
+ it('removes noprotect when protection is enabled', () => {
+ expect(toggleLoopProtection(withNoProtect, true)).toBe(html);
+ });
+
+ it('adds noprotect when protection is disabled', () => {
+ expect(toggleLoopProtection(html, false)).toBe(withNoProtect);
+ });
+ });
+
+ describe('preserveNoProtect', () => {
+ it('restores noprotect after a rewrite that dropped it', () => {
+ expect(preserveNoProtect(withNoProtect, html)).toBe(withNoProtect);
+ });
+
+ it('does not insert noprotect when the original had none', () => {
+ expect(preserveNoProtect(html, html)).toBe(html);
+ });
+
+ it('does not duplicate noprotect when the rewrite kept it', () => {
+ expect(preserveNoProtect(withNoProtect, withNoProtect)).toBe(
+ withNoProtect
+ );
+ });
+ });
+});