Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 82 additions & 6 deletions js/js-export/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,54 @@
* Internal functions' names are in PascalCase.
*/

/* global JSEditor, last, importMembers, Singer, JSInterface, globalActivity */
/* global JSEditor, last, importMembers, Singer, JSInterface, globalActivity, Painter, Turtle */

// Static registry for known API action objects.
// Resolvers must return stable objects (not factories).
// This replaces eval-based static lookups without changing behavior.
const STATIC_API_CLASS_REGISTRY = {
"Singer.RhythmActions": () => Singer.RhythmActions,
"Singer.MeterActions": () => Singer.MeterActions,
"Singer.PitchActions": () => Singer.PitchActions,
"Singer.IntervalsActions": () => Singer.IntervalsActions,
"Singer.ToneActions": () => Singer.ToneActions,
"Singer.OrnamentActions": () => Singer.OrnamentActions,
"Singer.VolumeActions": () => Singer.VolumeActions,
"Singer.DrumActions": () => Singer.DrumActions,
"Turtle.DictActions": () => Turtle.DictActions
};

function getStaticApiClassRegistry() {
return STATIC_API_CLASS_REGISTRY;
}

function resolveGlobalByPath(path) {
if (typeof path !== "string") return undefined;
if (!/^[A-Za-z0-9_.]+$/.test(path)) return undefined;

const parts = path.split(".");
let current = globalThis;
for (const part of parts) {
if (!part) return undefined;
current = current?.[part];
}
return current;
}

/**
* Resolve an API class name to its value using the static registry first,
* then fall back to resolving a global by path. This preserves the safe
* registry-based lookup while retaining backward compatibility with
* test/legacy globals that may be injected at runtime.
*/
function resolveApiClass(className) {
const registry = getStaticApiClassRegistry();
const getApiClass = registry[className];
if (getApiClass) {
return getApiClass();
}
return resolveGlobalByPath(className);
}

/**
* @class
Expand Down Expand Up @@ -143,6 +190,7 @@ class MusicBlocks {
* @returns {void}
*/
function CreateAPIMethodList() {
const apiClassRegistry = getStaticApiClassRegistry();
[
"Painter",
// "Painter.GraphicsActions",
Expand All @@ -160,16 +208,21 @@ class MusicBlocks {
MusicBlocks._methodList[className] = [];

if (className === "Painter") {
for (const methodName of Object.getOwnPropertyNames(
eval(className + ".prototype")
)) {
for (const methodName of Object.getOwnPropertyNames(Painter.prototype)) {
if (methodName !== "constructor" && !methodName.startsWith("_"))
MusicBlocks._methodList[className].push(methodName);
}
return;
}

for (const methodName of Object.getOwnPropertyNames(eval(className))) {
// Use the same safe resolution strategy used by dispatch so
// enumeration remains compatible with injected globals.
const apiClass = resolveApiClass(className);
if (!apiClass) {
throw new Error(`Unknown API class for method enumeration: ${className}`);
}

for (const methodName of Object.getOwnPropertyNames(apiClass)) {
if (methodName !== "length" && methodName !== "prototype")
MusicBlocks._methodList[className].push(methodName);
}
Expand Down Expand Up @@ -246,7 +299,30 @@ class MusicBlocks {
}
}

cname = cname === "Painter" ? this.turtle.painter : eval(cname);
if (cname === "Painter") {
cname = this.turtle.painter;
} else {
const apiClassRegistry = getStaticApiClassRegistry();
const getApiClass = apiClassRegistry[cname];
let apiClass;

if (getApiClass) {
apiClass = getApiClass();
} else {
// Backward-compatible fallback for test harnesses or other non-user-controlled
// environments that inject additional API action classes.
apiClass = resolveGlobalByPath(cname);
}

if (
!apiClass ||
(typeof apiClass !== "object" && typeof apiClass !== "function")
) {
throw new ReferenceError(`${cname} did not resolve to a valid API object`);
}

cname = apiClass;
}

returnVal =
args === undefined || (Array.isArray(args) && args.length === 0)
Expand Down
26 changes: 22 additions & 4 deletions js/palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
i18nSolfege, NUMBERBLOCKDEFAULT, TEXTWIDTH, STRINGLEN,
DEFAULTBLOCKSCALE, SVG, DISABLEDFILLCOLOR, DISABLEDSTROKECOLOR,
PALETTEFILLCOLORS, PALETTESTROKECOLORS, last, getTextWidth,
STANDARDBLOCKHEIGHT, CLOSEICON, BUILTINPALETTES,
STANDARDBLOCKHEIGHT, CLOSEICON, BUILTINPALETTES,
safeSVG, blockIsMacro, getMacroExpansion

cameraPALETTE, videoPALETTE, mediaPALETTE
*/

/* exported Palettes, initPalettes */
Expand Down Expand Up @@ -53,6 +55,14 @@ const makePaletteIcons = (data, width, height) => {
return img;
};

// Registry for built-in palette images defined in artwork.js.
// Using lazy getters preserves script load order expectations (same behavior as prior eval).
const BUILTIN_IMAGE_PALETTE_REGISTRY = {
media: () => mediaPALETTE,
camera: () => cameraPALETTE,
video: () => videoPALETTE
};

class Palettes {
constructor(activity) {
this.activity = activity;
Expand Down Expand Up @@ -997,7 +1007,15 @@ class Palette {
if (["media", "camera", "video"].includes(b.blkname)) {
// Use artwork.js strings as images for:
// cameraPALETTE, videoPALETTE, mediaPALETTE
img = makePaletteIcons(eval(b.blkname + "PALETTE"));
const getPaletteImage = BUILTIN_IMAGE_PALETTE_REGISTRY[b.blkname];
if (getPaletteImage) {
img = makePaletteIcons(getPaletteImage());
} else {
// Fallback: preserve previous behavior and warn so missing registry
// entries are visible during development/runtime.
console.warn(`Missing built-in palette image for ${b.blkname}`);
img = makePaletteIcons(this.activity.pluginsImages[b.blkname]);
}
} else {
// or use the plugin image...
img = makePaletteIcons(this.activity.pluginsImages[b.blkname]);
Expand Down Expand Up @@ -1430,7 +1448,7 @@ class Palette {
// Add variables first
for (let i = 0; i < foundVariables.length; i++) {
const [blockId, blockType] = foundVariables[i];
const block = activity.blocks.blockList[blockId];
const block = this.activity.blocks.blockList[blockId];
const isLastVar = i === foundVariables.length - 1;
const hasBoxes = boxBlocks.length > 0;

Expand Down Expand Up @@ -1461,7 +1479,7 @@ class Palette {
// Then add box blocks
for (let i = 0; i < boxBlocks.length; i++) {
const boxBlockId = boxBlocks[i];
const boxBlock = activity.blocks.blockList[boxBlockId];
const boxBlock = this.activity.blocks.blockList[boxBlockId];

statusBlocks.push([
lastBlockIndex + 1,
Expand Down
Loading