From b72c38721807745b6eb0b130d4166916074e0a42 Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Thu, 30 Oct 2025 02:17:03 +0000 Subject: [PATCH 1/5] Added unfinished URL parser and helper functions Helper functions will ensure URL parameters are normalized before being used. --- client/utils/parseURLParams.js | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 client/utils/parseURLParams.js diff --git a/client/utils/parseURLParams.js b/client/utils/parseURLParams.js new file mode 100644 index 0000000000..a1d558d3f1 --- /dev/null +++ b/client/utils/parseURLParams.js @@ -0,0 +1,62 @@ +import { p5Versions, currentP5Version } from "../../common/p5Versions"; + +// One centralized parser +export function parseUrlParams(url) { + const params = new URLSearchParams(new URL(url, 'https://dummy.origin').search); + + return { + version: validateVersion(params.get('version')), // string + sound: validateSound(params.get('sound')), // bool + preload: validatePreload(params.get('preload')), // bool + shapes: validateShapes(params.get('shapes')), // bool + data: validateData(params.get('data')) // bool + // Easy to add more params here + }; +} + +function validateVersion(version) { + if (!version) { + return currentP5Version; + } + + // if valid return version + + return currentP5Version; +} +function validateSound(sound) { + return; + + // on, true, 1 == on + // off, false, 0 == off + + // default if none triggered + +} +function validatePreload(preload) { + return; + + // on, true, 1 == on + // off, false, 0 == off + + // default if none triggered + +} +function validateShapes(shapes) { + return; + + // on, true, 1 == on + // off, false, 0 == off + + // default if none triggered + + +} +function validateData(data) { + return; + + // on, true, 1 == on + // off, false, 0 == off + + // default if none triggered + +} \ No newline at end of file From 07c9c71214e11563621ea2f7d61eaf0f41e6a69a Mon Sep 17 00:00:00 2001 From: Enoch Owoade Date: Sat, 1 Nov 2025 01:43:27 +0000 Subject: [PATCH 2/5] Added test file: client/utils/parseURLParams.test.js I have added a test file for the URL parameter parsing utility located at client/utils/parseURLParams.js. This test file includes various test cases to ensure the correct functionality of the parseURLParams function, covering edge cases and typical usage scenarios. For the add-on flags I allowed 'on', 'true', '1', 'ON', 'True', and 'TRUE' to be interpreted as true, while 'off', 'false', '0', 'OFF', 'False', and 'FALSE' are interpreted as false. This should enhance the robustness of the URL parameter parsing in our application. --- client/utils/parseURLParams.test.js | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 client/utils/parseURLParams.test.js diff --git a/client/utils/parseURLParams.test.js b/client/utils/parseURLParams.test.js new file mode 100644 index 0000000000..bf750516bb --- /dev/null +++ b/client/utils/parseURLParams.test.js @@ -0,0 +1,51 @@ +import { parseUrlParams } from './parseURLParams.js'; +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: undefined, + preload: undefined, + shapes: undefined, + data: undefined + }); + }); + + 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); + }); + }); +}); \ No newline at end of file From f53f7b0aa2f74f29da4fd3fc5993a21a5e8be182 Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Fri, 31 Oct 2025 06:22:36 +0000 Subject: [PATCH 3/5] Refactor defaultHTML to accept options object Convert defaultHTML from a constant string to a function that accepts an options object for version and add-on library configuration. This enables URL parameter parsing for library selection. Maintains backward compatibility - calling defaultHTML() with no arguments produces the same output as before. --- client/modules/IDE/reducers/files.js | 2 +- server/domain-objects/createDefaultFiles.js | 33 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index de1b9b1aa7..03f1adf3fe 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -32,7 +32,7 @@ export const initialState = () => { }, { name: 'index.html', - content: defaultHTML, + content: defaultHTML(), id: b, _id: b, fileType: 'file', 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 From a9076f440221ca121c8214067639de42db657c65 Mon Sep 17 00:00:00 2001 From: GoodKimchi <224943594+GoodKimchi@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:04:58 +0000 Subject: [PATCH 4/5] added functions, defaults, --- client/utils/parseURLParams.js | 98 ++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/client/utils/parseURLParams.js b/client/utils/parseURLParams.js index a1d558d3f1..c4afd2cb96 100644 --- a/client/utils/parseURLParams.js +++ b/client/utils/parseURLParams.js @@ -1,62 +1,80 @@ -import { p5Versions, currentP5Version } from "../../common/p5Versions"; +import { p5Versions, currentP5Version } from '../../common/p5Versions'; + +const DEFAULTS = { + sound: true, + preload: false, + shapes: false, + data: false +}; // One centralized parser export function parseUrlParams(url) { - const params = new URLSearchParams(new URL(url, 'https://dummy.origin').search); + const params = new URLSearchParams( + new URL(url, 'https://dummy.origin').search + ); return { version: validateVersion(params.get('version')), // string - sound: validateSound(params.get('sound')), // bool - preload: validatePreload(params.get('preload')), // bool - shapes: validateShapes(params.get('shapes')), // bool - data: validateData(params.get('data')) // bool + sound: validateBool(params.get('sound'), DEFAULTS.sound), // bool + preload: validateBool(params.get('preload'), DEFAULTS.preload), // bool + shapes: validateBool(params.get('shapes'), DEFAULTS.shapes), // bool + data: validateBool(params.get('data'), DEFAULTS.data) // bool // Easy to add more params here }; } function validateVersion(version) { - if (!version) { - return currentP5Version; - } - - // if valid return version - + if (!version) { return currentP5Version; -} -function validateSound(sound) { - return; + } + const v = String(version).trim(); + if (v.toLowerCase() === 'latest') { + const newest = getNewestVersion(p5Versions); + return newest ?? currentP5Version; //The ?? operator means: “if newest is null or undefined, use currentP5Version + } + if (v.toLowerCase() === 'current') return currentP5Version; + if (p5Versions.includes(v)) return v; + const normalized = v.replace(/^v/i, ''); + if (p5Versions.includes(normalized)) return normalized; + //This line strips that leading v using a regular expression ^v (meaning “v at the start”) and then rechecks. - // on, true, 1 == on - // off, false, 0 == off - - // default if none triggered + // if valid return version + return currentP5Version; +} +//picks highest version number from array +function getNewestVersion(list) { + // Defensive copy + semver sort (major.minor.patch) + const parts = (s) => s.split('.').map((n) => parseInt(n, 10) || 0); + return [...list].sort((a, b) => { + const [am, an, ap] = parts(a); + const [bm, bn, bp] = parts(b); + if (am !== bm) return bm - am; + if (an !== bn) return bn - an; + return bp - ap; + })[0]; } -function validatePreload(preload) { - return; - // on, true, 1 == on - // off, false, 0 == off - - // default if none triggered +function validateBool(value, defaultValue) { + if (value) return defaultValue; // param absent + //if (value === '') return true; // bare flag: ?flag -} -function validateShapes(shapes) { - return; + const v = String(value).trim().toLowerCase(); - // on, true, 1 == on - // off, false, 0 == off - - // default if none triggered + const TRUTHY = new Set(['on', 'true', '1', 'yes', 'y', 'enable', 'enabled']); + const FALSY = new Set([ + 'off', + 'false', + '0', + //'no', + //'n', + //'disable', + //'disabled' + ]); + if (TRUTHY.has(v)) return true; + if (FALSY.has(v)) return false; + return defaultValue; // unrecognized → fall back safely } -function validateData(data) { - return; - - // on, true, 1 == on - // off, false, 0 == off - - // default if none triggered -} \ No newline at end of file From e61bc35fc28ac0c8f30086172b838a82533e7edf Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Mon, 3 Nov 2025 05:13:57 +0000 Subject: [PATCH 5/5] Finish validator functions for URL parser Add validation functions to ensure URL parameters for p5.js versioning is correctly parsed and handled. Falls back to default functionality for invalid/missing parameters. Use npx prettier --write to format all modified files. Change test fallbacks to be consistent with default behavior. Add URL parser to client/modules/IDE/reducers/file.js Co-authored-by: Oscar Bedolla Co-authored-by: Enoch Owoade --- client/modules/IDE/reducers/files.js | 4 +- client/utils/parseURLParams.js | 111 ++++++++++++++------------- client/utils/parseURLParams.test.js | 90 +++++++++++----------- 3 files changed, 107 insertions(+), 98 deletions(-) diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 03f1adf3fe..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 index c4afd2cb96..3770c918e3 100644 --- a/client/utils/parseURLParams.js +++ b/client/utils/parseURLParams.js @@ -7,74 +7,81 @@ const DEFAULTS = { data: false }; -// One centralized parser -export function parseUrlParams(url) { - const params = new URLSearchParams( - new URL(url, 'https://dummy.origin').search - ); - - return { - version: validateVersion(params.get('version')), // string - sound: validateBool(params.get('sound'), DEFAULTS.sound), // bool - preload: validateBool(params.get('preload'), DEFAULTS.preload), // bool - shapes: validateBool(params.get('shapes'), DEFAULTS.shapes), // bool - data: validateBool(params.get('data'), DEFAULTS.data) // bool - // Easy to add more params here - }; +/** + * 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; + 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); + } } - const v = String(version).trim(); - if (v.toLowerCase() === 'latest') { - const newest = getNewestVersion(p5Versions); - return newest ?? currentP5Version; //The ?? operator means: “if newest is null or undefined, use currentP5Version + + // 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); + } } - if (v.toLowerCase() === 'current') return currentP5Version; - if (p5Versions.includes(v)) return v; - const normalized = v.replace(/^v/i, ''); - if (p5Versions.includes(normalized)) return normalized; - //This line strips that leading v using a regular expression ^v (meaning “v at the start”) and then rechecks. - // if valid return version return currentP5Version; } -//picks highest version number from array -function getNewestVersion(list) { - // Defensive copy + semver sort (major.minor.patch) - const parts = (s) => s.split('.').map((n) => parseInt(n, 10) || 0); - return [...list].sort((a, b) => { - const [am, an, ap] = parts(a); - const [bm, bn, bp] = parts(b); - if (am !== bm) return bm - am; - if (an !== bn) return bn - an; - return bp - ap; - })[0]; -} - function validateBool(value, defaultValue) { - if (value) return defaultValue; // param absent - //if (value === '') return true; // bare flag: ?flag + if (!value) return defaultValue; const v = String(value).trim().toLowerCase(); - const TRUTHY = new Set(['on', 'true', '1', 'yes', 'y', 'enable', 'enabled']); - const FALSY = new Set([ - 'off', - 'false', - '0', - //'no', - //'n', - //'disable', - //'disabled' - ]); + 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; // unrecognized → fall back safely + 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 index bf750516bb..56e0b52611 100644 --- a/client/utils/parseURLParams.test.js +++ b/client/utils/parseURLParams.test.js @@ -1,51 +1,51 @@ -import { parseUrlParams } from './parseURLParams.js'; +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: undefined, - preload: undefined, - shapes: undefined, - data: undefined - }); + 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 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 (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); - }); + }); + + 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); }); -}); \ No newline at end of file + }); +});