diff --git a/package-lock.json b/package-lock.json
index 96a28156d2..8faf4d39d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,14 @@
"version": "1.9.4",
"license": "LGPL-2.1",
"dependencies": {
+ "acorn": "^8.12.1",
+ "acorn-walk": "^8.3.4",
"colorjs.io": "^0.5.2",
"file-saver": "^1.3.8",
"gifenc": "^1.0.3",
"libtess": "^1.2.2",
"omggif": "^1.0.10",
- "opentype.js": "^1.3.1",
- "zod-validation-error": "^3.3.1"
+ "opentype.js": "^1.3.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
@@ -828,6 +829,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -2547,7 +2565,6 @@
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
- "dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2559,11 +2576,21 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
@@ -4563,6 +4590,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4603,17 +4647,27 @@
}
},
"node_modules/espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
- "dev": true,
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
+ "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
"dependencies": {
- "acorn": "^8.9.0",
+ "acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
+ "eslint-visitor-keys": "^4.1.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
+ "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -11606,21 +11660,11 @@
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/zod-validation-error": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz",
- "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==",
- "engines": {
- "node": ">=18.0.0"
- },
- "peerDependencies": {
- "zod": "^3.18.0"
- }
- },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
@@ -11632,4 +11676,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index ef709e7e47..37c0804100 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
},
"version": "1.9.4",
"dependencies": {
+ "acorn": "^8.12.1",
+ "acorn-walk": "^8.3.4",
"colorjs.io": "^0.5.2",
"file-saver": "^1.3.8",
"gifenc": "^1.0.3",
diff --git a/preview/index.html b/preview/index.html
index 847423dc4d..299cf2ccb0 100644
--- a/preview/index.html
+++ b/preview/index.html
@@ -1,5 +1,6 @@
+
P5 test
@@ -7,35 +8,37 @@
+
-
+ new p5(sketch);
+
+
\ No newline at end of file
diff --git a/src/core/friendly_errors/index.js b/src/core/friendly_errors/index.js
index 4cf7db60ba..8f1b0e56e0 100644
--- a/src/core/friendly_errors/index.js
+++ b/src/core/friendly_errors/index.js
@@ -1,5 +1,7 @@
import validateParams from './param_validator.js';
+import sketchVerifier from './sketch_verifier.js';
export default function (p5) {
p5.registerAddon(validateParams);
+ p5.registerAddon(sketchVerifier);
}
\ No newline at end of file
diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js
new file mode 100644
index 0000000000..f90eb4f204
--- /dev/null
+++ b/src/core/friendly_errors/sketch_verifier.js
@@ -0,0 +1,126 @@
+import * as acorn from 'acorn';
+import * as walk from 'acorn-walk';
+
+/**
+ * @for p5
+ * @requires core
+ */
+function sketchVerifier(p5, fn) {
+ /**
+ * Fetches the contents of a script element in the user's sketch.
+ *
+ * @method fetchScript
+ * @param {HTMLScriptElement} script
+ * @returns {Promise}
+ */
+ fn.fetchScript = async function (script) {
+ if (script.src) {
+ try {
+ const contents = await fetch(script.src).then((res) => res.text());
+ return contents;
+ } catch (error) {
+ // TODO: Handle CORS error here.
+ console.error('Error fetching script:', error);
+ return '';
+ }
+ } else {
+ return script.textContent;
+ }
+ }
+
+ /**
+ * Extracts the user's code from the script fetched. Note that this method
+ * assumes that the user's code is always the last script element in the
+ * sketch.
+ *
+ * @method getUserCode
+ * @returns {Promise} The user's code as a string.
+ */
+ fn.getUserCode = async function () {
+ // TODO: think of a more robust way to get the user's code. Refer to
+ // https://github.com/processing/p5.js/pull/7293.
+ const scripts = document.querySelectorAll('script');
+ const userCodeScript = scripts[scripts.length - 1];
+ const userCode = await fn.fetchScript(userCodeScript);
+
+ return userCode;
+ }
+
+ /**
+ * Extracts the user-defined variables and functions from the user code with
+ * the help of Espree parser.
+ *
+ * @method extractUserDefinedVariablesAndFuncs
+ * @param {string} code - The code to extract variables and functions from.
+ * @returns {Object} An object containing the user's defined variables and functions.
+ * @returns {Array<{name: string, line: number}>} [userDefinitions.variables] Array of user-defined variable names and their line numbers.
+ * @returns {Array<{name: string, line: number}>} [userDefinitions.functions] Array of user-defined function names and their line numbers.
+ */
+ fn.extractUserDefinedVariablesAndFuncs = function (code) {
+ const userDefinitions = {
+ variables: [],
+ functions: []
+ };
+ // The line numbers from the parser are consistently off by one, add
+ // `lineOffset` here to correct them.
+ const lineOffset = -1;
+
+ try {
+ const ast = acorn.parse(code, {
+ ecmaVersion: 2021,
+ sourceType: 'module',
+ locations: true // This helps us get the line number.
+ });
+
+ walk.simple(ast, {
+ VariableDeclarator(node) {
+ if (node.id.type === 'Identifier') {
+ const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type)
+ ? 'functions'
+ : 'variables';
+ userDefinitions[category].push({
+ name: node.id.name,
+ line: node.loc.start.line + lineOffset
+ });
+ }
+ },
+ FunctionDeclaration(node) {
+ if (node.id && node.id.type === 'Identifier') {
+ userDefinitions.functions.push({
+ name: node.id.name,
+ line: node.loc.start.line + lineOffset
+ });
+ }
+ },
+ // We consider class declarations to be a special form of variable
+ // declaration.
+ ClassDeclaration(node) {
+ if (node.id && node.id.type === 'Identifier') {
+ userDefinitions.variables.push({
+ name: node.id.name,
+ line: node.loc.start.line + lineOffset
+ });
+ }
+ }
+ });
+ } catch (error) {
+ // TODO: Replace this with a friendly error message.
+ console.error('Error parsing code:', error);
+ }
+
+ return userDefinitions;
+ }
+
+ fn.run = async function () {
+ const userCode = await fn.getUserCode();
+ const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode);
+
+ return userDefinedVariablesAndFuncs;
+ }
+}
+
+export default sketchVerifier;
+
+if (typeof p5 !== 'undefined') {
+ sketchVerifier(p5, p5.prototype);
+}
\ No newline at end of file
diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js
new file mode 100644
index 0000000000..cf16edbff7
--- /dev/null
+++ b/test/unit/core/sketch_overrides.js
@@ -0,0 +1,220 @@
+import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js';
+
+suite('Validate Params', function () {
+ const mockP5 = {
+ _validateParameters: vi.fn()
+ };
+ const mockP5Prototype = {};
+
+ beforeAll(function () {
+ sketchVerifier(mockP5, mockP5Prototype);
+ });
+
+ afterAll(function () {
+ });
+
+ suite('fetchScript()', function () {
+ const url = 'https://www.p5test.com/sketch.js';
+ const code = 'p.createCanvas(200, 200);';
+
+ test('Fetches script content from src', async function () {
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ text: () => Promise.resolve(code)
+ })
+ );
+ vi.stubGlobal('fetch', mockFetch);
+
+ const mockScript = { src: url };
+ const result = await mockP5Prototype.fetchScript(mockScript);
+
+ expect(mockFetch).toHaveBeenCalledWith(url);
+ expect(result).toBe(code);
+
+ vi.unstubAllGlobals();
+ });
+
+ test('Fetches code when there is no src attribute', async function () {
+ const mockScript = { textContent: code };
+ const result = await mockP5Prototype.fetchScript(mockScript);
+
+ expect(result).toBe(code);
+ });
+ });
+
+ suite('getUserCode()', function () {
+ const userCode = "let c = p5.Color(20, 20, 20);";
+
+ test('fetches the last script element', async function () {
+ document.body.innerHTML = `
+
+
+
+ `;
+
+ mockP5Prototype.fetchScript = vi.fn(() => Promise.resolve(userCode));
+
+ const result = await mockP5Prototype.getUserCode();
+
+ expect(mockP5Prototype.fetchScript).toHaveBeenCalledTimes(1);
+ expect(result).toBe(userCode);
+ });
+ });
+
+ suite('extractUserDefinedVariablesAndFuncs()', function () {
+ test('Extracts user-defined variables and functions', function () {
+ const code = `
+ let x = 5;
+ const y = 10;
+ var z = 15;
+ let v1, v2, v3
+ function foo() {}
+ const bar = () => {};
+ const baz = (x) => x * 2;
+ `;
+
+ const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code);
+ const expectedResult = {
+ "functions": [
+ {
+ "line": 5,
+ "name": "foo",
+ },
+ {
+ "line": 6,
+ "name": "bar",
+ },
+ {
+ "line": 7,
+ "name": "baz",
+ },
+ ],
+ "variables": [
+ {
+ "line": 1,
+ "name": "x",
+ },
+ {
+ "line": 2,
+ "name": "y",
+ },
+ {
+ "line": 3,
+ "name": "z",
+ },
+ {
+ "line": 4,
+ "name": "v1",
+ },
+ {
+ "line": 4,
+ "name": "v2",
+ },
+ {
+ "line": 4,
+ "name": "v3",
+ },
+ ],
+ };
+ expect(result).toEqual(expectedResult);
+ });
+
+ // Sketch verifier should ignore the following types of lines:
+ // - Comments (both single line and multi-line)
+ // - Function calls
+ // - Non-declaration code
+ test('Ignores other lines', function () {
+ const code = `
+ // This is a comment
+ let x = 5;
+ /* This is a multi-line comment.
+ * This is a multi-line comment.
+ */
+ const y = 10;
+ console.log("This is a statement");
+ foo(5);
+ p5.Math.random();
+ if (true) {
+ let z = 15;
+ }
+ for (let i = 0; i < 5; i++) {}
+ `;
+
+ const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code);
+ const expectedResult = {
+ "functions": [],
+ "variables": [
+ {
+ "line": 2,
+ "name": "x",
+ },
+ {
+ "line": 6,
+ "name": "y",
+ },
+ {
+ "line": 11,
+ "name": "z",
+ },
+ {
+ "line": 13,
+ "name": "i",
+ },
+ ],
+ };
+
+ expect(result).toEqual(expectedResult);
+ });
+
+ test('Handles parsing errors', function () {
+ const invalidCode = 'let x = ;';
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
+
+ const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(invalidCode);
+
+ expect(consoleSpy).toHaveBeenCalled();
+ expect(result).toEqual({ variables: [], functions: [] });
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ suite('run()', function () {
+ test('Returns extracted variables and functions', async function () {
+ const mockScript = `
+ let x = 5;
+ const y = 10;
+ function foo() {}
+ const bar = () => {};
+ `;
+ mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript));
+
+ const result = await mockP5Prototype.run();
+ const expectedResult = {
+ "functions": [
+ {
+ "line": 3,
+ "name": "foo",
+ },
+ {
+ "line": 4,
+ "name": "bar",
+ },
+ ],
+ "variables": [
+ {
+ "line": 1,
+ "name": "x",
+ },
+ {
+ "line": 2,
+ "name": "y",
+ },
+ ],
+ };
+
+ expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(expectedResult);
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/unit/spec.js b/test/unit/spec.js
index 995b152693..8df31317f2 100644
--- a/test/unit/spec.js
+++ b/test/unit/spec.js
@@ -13,6 +13,7 @@ var spec = {
'param_errors',
'preload',
'rendering',
+ 'sketch_overrides',
'structure',
'transform',
'version',