Skip to content

Commit 5b74aa6

Browse files
authored
Merge pull request #8353 from limzykenneth/decorator-api
Implement public decorator API
2 parents 2ef7704 + 172b598 commit 5b74aa6

File tree

4 files changed

+143
-45
lines changed

4 files changed

+143
-45
lines changed

src/core/environment.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,14 +751,18 @@ function environment(p5, fn, lifecycles){
751751
};
752752

753753
Object.defineProperty(fn, 'width', {
754+
configurable: true,
755+
enumerable: true,
754756
get(){
755-
return this._renderer.width;
757+
return this._renderer?.width;
756758
}
757759
});
758760

759761
Object.defineProperty(fn, 'height', {
762+
configurable: true,
763+
enumerable: true,
760764
get(){
761-
return this._renderer.height;
765+
return this._renderer?.height;
762766
}
763767
});
764768

src/core/friendly_errors/param_validator.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -573,15 +573,19 @@ function validateParams(p5, fn, lifecycles) {
573573

574574
fn._validate = validate; // TEMP: For unit tests
575575

576-
p5.decorateHelper(
577-
/^(?!_).+$/,
578-
function(target, { name }){
579-
return function(...args){
580-
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
581-
validate(name, args);
582-
}
583-
return target.apply(this, args);
584-
};
576+
p5.registerDecorator(
577+
({ path }) => {
578+
return path.startsWith('p5.prototype');
579+
},
580+
function(target, { kind, name }){
581+
if(kind === 'method'){
582+
return function(...args){
583+
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
584+
validate(name, args);
585+
}
586+
return target.apply(this, args);
587+
};
588+
}
585589
}
586590
);
587591

src/core/main.js

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,30 +50,7 @@ class p5 {
5050
constructor(sketch, node) {
5151
// Apply addon defined decorations
5252
if(p5.decorations.size > 0){
53-
for (const [patternArray, decoration] of p5.decorations) {
54-
for(const member in p5.prototype) {
55-
// Member must be a function
56-
if (typeof p5.prototype[member] !== 'function') continue;
57-
58-
if (!patternArray.some(pattern => {
59-
if (typeof pattern === 'string') {
60-
return pattern === member;
61-
} else if (pattern instanceof RegExp) {
62-
return pattern.test(member);
63-
}
64-
})) continue;
65-
66-
p5.prototype[member] = decoration(p5.prototype[member], {
67-
kind: 'method',
68-
name: member,
69-
access: {},
70-
static: false,
71-
private: false,
72-
addInitializer(initializer){}
73-
});
74-
}
75-
}
76-
53+
decorateClass(p5, p5.decorations);
7754
p5.decorations.clear();
7855
}
7956

@@ -165,11 +142,11 @@ class p5 {
165142
}
166143

167144
get pixels(){
168-
return this._renderer.pixels;
145+
return this._renderer?.pixels;
169146
}
170147

171148
get drawingContext(){
172-
return this._renderer.drawingContext;
149+
return this._renderer?.drawingContext;
173150
}
174151

175152
static _registeredAddons = new Set();
@@ -193,10 +170,20 @@ class p5 {
193170
}
194171

195172
static decorations = new Map();
196-
static decorateHelper(pattern, decoration){
197-
let patternArray = pattern;
198-
if (!Array.isArray(pattern)) patternArray = [pattern];
199-
p5.decorations.set(patternArray, decoration);
173+
static registerDecorator(pattern, decoration){
174+
if(typeof pattern === 'string'){
175+
const patternStr = pattern;
176+
pattern = ({ path }) => patternStr === path;
177+
}else if(
178+
Array.isArray(pattern) &&
179+
pattern.every(value => typeof value === 'string')
180+
){
181+
const patternArray = pattern;
182+
pattern = ({ path }) => patternArray.includes(path);
183+
}else if(typeof pattern !== 'function'){
184+
throw new Error('Decorator matching pattern must be a function, a string, or an array of strings');
185+
}
186+
p5.decorations.set(pattern, decoration);
200187
}
201188

202189
#customActions = {};
@@ -437,6 +424,11 @@ class p5 {
437424
}
438425
}
439426

427+
// Attach constants to p5 prototype
428+
for (const k in constants) {
429+
p5.prototype[k] = constants[k];
430+
}
431+
440432
// Global helper function for binding properties to window in global mode
441433
function createBindGlobal(instance) {
442434
return function bindGlobal(property) {
@@ -527,9 +519,107 @@ function createBindGlobal(instance) {
527519
};
528520
}
529521

530-
// Attach constants to p5 prototype
531-
for (const k in constants) {
532-
p5.prototype[k] = constants[k];
522+
// Generic function to decorate classes
523+
function decorateClass(Target, decorations, path){
524+
path ??= Target.name;
525+
// Static properties
526+
for(const key in Target){
527+
if(!key.startsWith('_')){
528+
for (const [pattern, decorator] of decorations) {
529+
if(pattern({ path: `${path}.${key}` })){
530+
// Check if method or accessor
531+
if(typeof Target[key] === 'function'){
532+
const result = decorator(Target[key], {
533+
kind: 'method',
534+
name: key,
535+
static: true
536+
});
537+
if(result){
538+
Object.defineProperty(Target, key, {
539+
enumerable: true,
540+
writable: true,
541+
value: result
542+
});
543+
}
544+
}else{
545+
const result = decorator(undefined, {
546+
kind: 'field',
547+
name: key,
548+
static: true
549+
});
550+
if(result && typeof result === 'function'){
551+
Target[key] = result(Target[key]);
552+
}
553+
}
554+
}
555+
}
556+
557+
if(typeof Target[key] === 'function' && Target[key].prototype){
558+
decorateClass(Target[key], decorations, `${path}.${key}`);
559+
}
560+
}
561+
}
562+
563+
// Member properties
564+
for(const member of Object.getOwnPropertyNames(Target.prototype)){
565+
if(member !== 'constructor' && !member.startsWith('_')){
566+
for (const [pattern, decorator] of decorations) {
567+
if(pattern({ path: `${path}.prototype.${member}` })){
568+
// Check if method or accessor
569+
if(typeof Target.prototype[member] === 'function'){
570+
const result = decorator(Target.prototype[member], {
571+
kind: 'method',
572+
name: member,
573+
static: false
574+
});
575+
if(result) {
576+
Object.defineProperty(Target.prototype, member, {
577+
enumerable: true,
578+
writable: true,
579+
value: result
580+
});
581+
}
582+
}else{
583+
const descriptor = Object.getOwnPropertyDescriptor(
584+
Target.prototype,
585+
member
586+
);
587+
if(descriptor.hasOwnProperty('value')){
588+
const result = decorator(undefined, {
589+
kind: 'field',
590+
name: member,
591+
static: false
592+
});
593+
Object.defineProperty(Target.prototype, member, {
594+
enumerable: true,
595+
writable: true,
596+
value: result && typeof result === 'function' ?
597+
result(Target.prototype[member]) :
598+
Target.prototype[member]
599+
});
600+
}else{
601+
const { get, set } = descriptor;
602+
const getterResult = decorator(get, {
603+
kind: 'getter',
604+
name: member,
605+
static: false
606+
});
607+
const setterResult = decorator(set, {
608+
kind: 'setter',
609+
name: member,
610+
static: false
611+
});
612+
Object.defineProperty(Target.prototype, member, {
613+
enumerable: true,
614+
get: getterResult ?? get,
615+
set: setterResult ?? set
616+
});
617+
}
618+
}
619+
}
620+
}
621+
}
622+
}
533623
}
534624

535625
import transform from './transform';

test/unit/core/param_errors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ suite('Validate Params', function () {
2929
return 'mock p5.Graphics';
3030
},
3131
_error: () => {},
32-
decorateHelper: () => {},
32+
registerDecorator: () => {},
3333
};
3434
const mockP5Prototype = {};
3535

0 commit comments

Comments
 (0)