Skip to content
Merged
8 changes: 2 additions & 6 deletions client/modules/IDE/components/Preferences/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ import {
setLinewrap,
setPreferencesTab
} from '../../actions/preferences';
import {
majorVersion,
p5SoundURL,
p5URL,
useP5Version
} from '../../hooks/useP5Version';
import { majorVersion, p5URL, useP5Version } from '../../hooks/useP5Version';
import p5SoundURL from '../../../../../common/p5URLs';
import VersionPicker from '../VersionPicker';
import { updateFileContent } from '../../actions/files';
import { CmControllerContext } from '../../pages/IDEView';
Expand Down
20 changes: 9 additions & 11 deletions client/modules/IDE/hooks/useP5Version.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ import React, { useContext, useMemo } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { currentP5Version, p5Versions } from '../../../../common/p5Versions';
import {
p5SoundURLOldTemplate,
p5SoundURL,
p5PreloadAddonURL,
p5ShapesAddonURL,
p5DataAddonURL,
p5URLTemplate
} from '../../../../common/p5URLs';

export const majorVersion = (version) => version.split('.')[0];

export const p5SoundURLOldTemplate =
'https://cdnjs.cloudflare.com/ajax/libs/p5.js/$VERSION/addons/p5.sound.min.js';
export const p5SoundURLOld = p5SoundURLOldTemplate.replace(
'$VERSION',
currentP5Version
);
export const p5SoundURL =
'https://cdn.jsdelivr.net/npm/p5.sound@0.2.0/dist/p5.sound.min.js';
export const p5PreloadAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/preload.js';
export const p5ShapesAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/shapes.js';
export const p5DataAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/data.js';
export const p5URL = `https://cdn.jsdelivr.net/npm/p5@${currentP5Version}/lib/p5.js`;
export const p5URL = p5URLTemplate.replace('$VERSION', currentP5Version);

const P5VersionContext = React.createContext({});

Expand Down
4 changes: 3 additions & 1 deletion client/modules/IDE/reducers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -32,7 +34,7 @@ export const initialState = () => {
},
{
name: 'index.html',
content: defaultHTML,
content: defaultHTML(params),
id: b,
_id: b,
fileType: 'file',
Expand Down
87 changes: 87 additions & 0 deletions client/utils/parseURLParams.js
Original file line number Diff line number Diff line change
@@ -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)
};
}
51 changes: 51 additions & 0 deletions client/utils/parseURLParams.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
12 changes: 12 additions & 0 deletions common/p5URLs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const p5SoundURLOldTemplate =
'https://cdnjs.cloudflare.com/ajax/libs/p5.js/$VERSION/addons/p5.sound.min.js';
export const p5SoundURL =
'https://cdn.jsdelivr.net/npm/p5.sound@0.2.0/dist/p5.sound.min.js';
export const p5PreloadAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/preload.js';
export const p5ShapesAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/shapes.js';
export const p5DataAddonURL =
'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.2.0/src/data.js';
export const p5URLTemplate =
'https://cdn.jsdelivr.net/npm/p5@$VERSION/lib/p5.js';
40 changes: 36 additions & 4 deletions server/domain-objects/createDefaultFiles.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { currentP5Version } from '../../common/p5Versions';
import {
p5SoundURLOldTemplate,
p5SoundURL,
p5PreloadAddonURL,
p5ShapesAddonURL,
p5DataAddonURL,
p5URLTemplate
} from '../../common/p5URLs';

export const defaultSketch = `function setup() {
createCanvas(400, 400);
Expand All @@ -8,11 +16,34 @@ function draw() {
background(220);
}`;

export const defaultHTML = `<!DOCTYPE html>
const majorVersion = (version) => version.split('.')[0];

export function defaultHTML({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great change—it adds much more flexibility in creating the default files and the changes here are easy to follow!

version = currentP5Version,
sound = true,
preload = false,
shapes = false,
data = false
} = {}) {
const p5URL = p5URLTemplate.replace('$VERSION', version);

const soundURL =
majorVersion(version) === '2'
? p5SoundURL
: p5SoundURLOldTemplate.replace('$VERSION', version);

const libraries = [
`<script src="${p5URL}"></script>`,
sound ? `<script src="${soundURL}"></script>` : '',
preload ? `<script src="${p5PreloadAddonURL}"></script>` : '',
shapes ? `<script src="${p5ShapesAddonURL}"></script>` : '',
data ? `<script src="${p5DataAddonURL}"></script>` : ''
].join('\n ');

return `<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/p5@${currentP5Version}/lib/p5.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@${currentP5Version}/lib/addons/p5.sound.min.js"></script>
${libraries}
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />

Expand All @@ -24,6 +55,7 @@ export const defaultHTML = `<!DOCTYPE html>
</body>
</html>
`;
}

export const defaultCSS = `html, body {
margin: 0;
Expand All @@ -37,7 +69,7 @@ canvas {
export default function createDefaultFiles() {
return {
'index.html': {
content: defaultHTML
content: defaultHTML()
},
'style.css': {
content: defaultCSS
Expand Down
Loading