diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index de1b9b1aa7..6650f28d53 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -5,12 +5,14 @@ import { defaultCSS, defaultHTML } from '../../../../server/domain-objects/createDefaultFiles'; +import { parseUrlParams } from '../../../utils/parseURLParams'; export const initialState = () => { const a = objectID().toHexString(); const b = objectID().toHexString(); const c = objectID().toHexString(); const r = objectID().toHexString(); + const params = parseUrlParams(window.location.href); return [ { name: 'root', @@ -32,7 +34,7 @@ export const initialState = () => { }, { name: 'index.html', - content: defaultHTML, + content: defaultHTML(params), id: b, _id: b, fileType: 'file', diff --git a/client/utils/parseURLParams.js b/client/utils/parseURLParams.js new file mode 100644 index 0000000000..3770c918e3 --- /dev/null +++ b/client/utils/parseURLParams.js @@ -0,0 +1,87 @@ +import { p5Versions, currentP5Version } from '../../common/p5Versions'; + +const DEFAULTS = { + sound: true, + preload: false, + shapes: false, + data: false +}; + +/** + * Sorts version strings in descending order and returns the highest version + * @param {string[]} versions - Array of version strings (e.g., ['1.11.2', '1.11.1']) + * @returns {string} The highest version from the array + */ +function getNewestVersion(versions) { + return versions.sort((a, b) => { + const pa = a.split('.').map((n) => parseInt(n, 10)); + const pb = b.split('.').map((n) => parseInt(n, 10)); + for (let i = 0; i < 3; i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na !== nb) return nb - na; + } + return 0; + })[0]; +} + +function validateVersion(version) { + if (!version) return currentP5Version; + + const ver = String(version).trim(); + + if (p5Versions.includes(ver)) return ver; + + // if only major.minor provided like "1.11" + const majorMinorMatch = /^(\d+)\.(\d+)$/.exec(ver); + if (majorMinorMatch) { + const [, major, minor] = majorMinorMatch; + const matches = p5Versions.filter((v) => { + const parts = v.split('.'); + return parts[0] === major && parts[1] === minor; + }); + if (matches.length) { + return getNewestVersion(matches); + } + } + + // if only major provided like "1" + const majorOnlyMatch = /^(\d+)$/.exec(ver); + if (majorOnlyMatch) { + const [, major] = majorOnlyMatch; + const matches = p5Versions.filter((v) => v.split('.')[0] === major); + if (matches.length) { + return getNewestVersion(matches); + } + } + + return currentP5Version; +} + +function validateBool(value, defaultValue) { + if (!value) return defaultValue; + + const v = String(value).trim().toLowerCase(); + + const TRUTHY = new Set(['on', 'true', '1']); + const FALSY = new Set(['off', 'false', '0']); + + if (TRUTHY.has(v)) return true; + if (FALSY.has(v)) return false; + + return defaultValue; +} + +export function parseUrlParams(url) { + const params = new URLSearchParams( + new URL(url, 'https://dummy.origin').search + ); + + return { + version: validateVersion(params.get('version')), + sound: validateBool(params.get('sound'), DEFAULTS.sound), + preload: validateBool(params.get('preload'), DEFAULTS.preload), + shapes: validateBool(params.get('shapes'), DEFAULTS.shapes), + data: validateBool(params.get('data'), DEFAULTS.data) + }; +} diff --git a/client/utils/parseURLParams.test.js b/client/utils/parseURLParams.test.js new file mode 100644 index 0000000000..56e0b52611 --- /dev/null +++ b/client/utils/parseURLParams.test.js @@ -0,0 +1,51 @@ +import { parseUrlParams } from './parseURLParams'; +import { currentP5Version } from '../../common/p5Versions'; + +describe('parseUrlParams', () => { + test('returns defaults when no params are provided', () => { + const url = 'https://example.com'; + const result = parseUrlParams(url); + + expect(result).toEqual({ + version: currentP5Version, + sound: true, + preload: false, + shapes: false, + data: false + }); + }); + + test('parses a valid p5 version and falls back for invalid versions', () => { + const good = parseUrlParams('https://example.com?version=1.4.0'); + expect(good.version).toBe('1.4.0'); + + const bad = parseUrlParams('https://example.com?version=9.9.9'); + expect(bad.version).toBe(currentP5Version); + }); + + test('parses boolean-like params for sound/preload/shapes/data (true variants)', () => { + const trueVariants = ['on', 'true', '1', 'ON', 'True']; + + trueVariants.forEach((v) => { + const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; + const result = parseUrlParams(url); + expect(result.sound).toBe(true); + expect(result.preload).toBe(true); + expect(result.shapes).toBe(true); + expect(result.data).toBe(true); + }); + }); + + test('parses boolean-like params for sound/preload/shapes/data (false variants)', () => { + const falseVariants = ['off', 'false', '0', 'OFF', 'False']; + + falseVariants.forEach((v) => { + const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; + const result = parseUrlParams(url); + expect(result.sound).toBe(false); + expect(result.preload).toBe(false); + expect(result.shapes).toBe(false); + expect(result.data).toBe(false); + }); + }); +}); diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js index 70eddee3ed..f71545ce81 100644 --- a/server/domain-objects/createDefaultFiles.js +++ b/server/domain-objects/createDefaultFiles.js @@ -8,11 +8,35 @@ function draw() { background(220); }`; -export const defaultHTML = ` +export function defaultHTML({ + version = currentP5Version, + sound = true, + preload = false, + shapes = false, + data = false +} = {}) { + const soundURL = version.startsWith('2.') + ? `https://cdn.jsdelivr.net/npm/p5.sound@0.2.0/dist/p5.sound.min.js` + : `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${version}/addons/p5.sound.min.js`; + + const libraries = [ + ``, + sound ? `` : '', + preload + ? `` + : '', + shapes + ? `` + : '', + data + ? `` + : '' + ].join('\n '); + + return `
- - + ${libraries} @@ -24,6 +48,7 @@ export const defaultHTML = ` `; +} export const defaultCSS = `html, body { margin: 0; @@ -37,7 +62,7 @@ canvas { export default function createDefaultFiles() { return { 'index.html': { - content: defaultHTML + content: defaultHTML() }, 'style.css': { content: defaultCSS