diff --git a/index.html b/index.html index 4c7e052166..2e096d87c4 100644 --- a/index.html +++ b/index.html @@ -66,7 +66,12 @@ - + diff --git a/js/js-export/__tests__/ast2blocklist.test.js b/js/js-export/__tests__/ast2blocklist.test.js index 2c2e8b7aed..b9f3c282b0 100644 --- a/js/js-export/__tests__/ast2blocklist.test.js +++ b/js/js-export/__tests__/ast2blocklist.test.js @@ -22,8 +22,19 @@ const acorn = require("../../../lib/acorn.min"); const { AST2BlockList } = require("../ast2blocklist"); +const fs = require("fs"); +const path = require("path"); describe("AST2BlockList Class", () => { + let config; + + beforeAll(() => { + // Load the config file from parent directory + const configPath = path.join(__dirname, "..", "ast2blocks.json"); + const configContent = fs.readFileSync(configPath, "utf8"); + config = JSON.parse(configContent); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -44,7 +55,12 @@ describe("AST2BlockList Class", () => { MusicBlocks.run();`; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - expect(() => AST2BlockList.toTrees(AST)).toThrow("Unsupported AsyncCallExpression: mouse.setMusicInstrument"); + try { + AST2BlockList.toBlockList(AST, config); + } catch (e) { + //TODO: error message should isolate to smallest scope + expect(e.prefix).toEqual("Unsupported statement: "); + } }); // Test unsupported assignment expression should throw an error. @@ -65,7 +81,7 @@ describe("AST2BlockList Class", () => { const AST = acorn.parse(code, { ecmaVersion: 2020 }); try { - AST2BlockList.toTrees(AST); + AST2BlockList.toBlockList(AST, config); } catch (e) { expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported AssignmentExpression: box1 = box2 - 1"); } @@ -87,7 +103,11 @@ describe("AST2BlockList Class", () => { MusicBlocks.run();`; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - expect(() => AST2BlockList.toTrees(AST)).toThrow("Unsupported binary operator: <<"); + try { + AST2BlockList.toBlockList(AST, config); + } catch (e) { + expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported operator <<: 1 << 2"); + } }); // Test unsupported unary operator should throw an error. @@ -106,7 +126,11 @@ describe("AST2BlockList Class", () => { MusicBlocks.run();`; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - expect(() => AST2BlockList.toTrees(AST)).toThrow("Unsupported unary operator: ~"); + try { + AST2BlockList.toBlockList(AST, config); + } catch (e) { + expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported operator ~: ~2"); + } }); // Test calling unsupported function should throw an error. @@ -125,7 +149,11 @@ describe("AST2BlockList Class", () => { MusicBlocks.run();`; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - expect(() => AST2BlockList.toTrees(AST)).toThrow("Unsupported function call: Math.unsupported"); + try { + AST2BlockList.toBlockList(AST, config); + } catch (e) { + expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported operator unsupported: Math.unsupported(1)"); + } }); // Test unsupported argument type should throw an error. @@ -145,7 +173,7 @@ describe("AST2BlockList Class", () => { const AST = acorn.parse(code, { ecmaVersion: 2020 }); try { - AST2BlockList.toTrees(AST); + AST2BlockList.toBlockList(AST, config); } catch (e) { expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported argument type ArrayExpression: [1]"); } @@ -157,7 +185,7 @@ describe("AST2BlockList Class", () => { const AST = acorn.parse(code, { ecmaVersion: 2020 }); try { - AST2BlockList.toTrees(AST); + AST2BlockList.toBlockList(AST, config); } catch (e) { expect(e.prefix + code.substring(e.start, e.end)).toEqual("Unsupported statement: console.log('test');"); } @@ -198,8 +226,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -244,8 +271,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -290,8 +316,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -305,6 +330,7 @@ describe("AST2BlockList Class", () => { return mouse.ENDFLOW; }); box1 = box1 - 1; + box1 = box1 + 1; if (box1 > 0) { await playSol(mouse); } @@ -333,27 +359,29 @@ describe("AST2BlockList Class", () => { [9, ["number", { "value": 2 }], 0, 0, [7]], [10, "decrementOne", 0, 0, [2, 11, 12]], [11, ["namedbox", { "value": "box1" }], 0, 0, [10]], - [12, "if", 0, 0, [10, 13, 16, null]], - [13, "greater", 0, 0, [12, 14, 15]], - [14, ["namedbox", { "value": "box1" }], 0, 0, [13]], - [15, ["number", { "value": 0 }], 0, 0, [13]], - [16, ["nameddo", { "value": "playSol" }], 0, 0, [12, null]], - [17, "start", 500, 200, [null, 18, null]], - [18, ["storein2", { "value": "box1" }], 0, 0, [17, 19, 24]], - [19, "multiply", 0, 0, [18, 20, 23]], - [20, "abs", 0, 0, [19, 21]], - [21, "neg", 0, 0, [20, 22]], - [22, ["number", { "value": 2 }], 0, 0, [21]], - [23, ["number", { "value": 3 }], 0, 0, [19]], - [24, "vspace", 0, 0, [18, 25]], - [25, "settimbre", 0, 0, [24, 26, 27, null]], - [26, ["voicename", { "value": "electronic synth" }], 0, 0, [25]], - [27, ["nameddo", { "value": "playSol" }], 0, 0, [25, null]] + [12, "increment", 0, 0, [10, 13, 14, 15]], + [13, ["namedbox", { "value": "box1" }], 0, 0, [12]], + [14, ["number", { "value": 1 }], 0, 0, [12]], + [15, "if", 0, 0, [12, 16, 19, null]], + [16, "greater", 0, 0, [15, 17, 18]], + [17, ["namedbox", { "value": "box1" }], 0, 0, [16]], + [18, ["number", { "value": 0 }], 0, 0, [16]], + [19, ["nameddo", { "value": "playSol" }], 0, 0, [15, null]], + [20, "start", 500, 200, [null, 21, null]], + [21, ["storein2", { "value": "box1" }], 0, 0, [20, 22, 27]], + [22, "multiply", 0, 0, [21, 23, 26]], + [23, "abs", 0, 0, [22, 24]], + [24, "neg", 0, 0, [23, 25]], + [25, ["number", { "value": 2 }], 0, 0, [24]], + [26, ["number", { "value": 3 }], 0, 0, [22]], + [27, "vspace", 0, 0, [21, 28]], + [28, "settimbre", 0, 0, [27, 29, 30, null]], + [29, ["voicename", { "value": "electronic synth" }], 0, 0, [28]], + [30, ["nameddo", { "value": "playSol" }], 0, 0, [28, null]] ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -607,8 +635,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -712,8 +739,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -902,8 +928,7 @@ describe("AST2BlockList Class", () => { ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); @@ -1025,7 +1050,7 @@ describe("AST2BlockList Class", () => { [49, "pitch", 0, 0, [48, 50, 51, null]], [50, ["solfege", { "value": "sol" }], 0, 0, [49]], [51, ["number", { "value": 4 }], 0, 0, [49]], - [52, "break", 0, 0, [44, null, null, null]], + [52, "break", 0, 0, [44, null]], [53, "forever", 0, 0, [43, 54, 62]], [54, "newnote", 0, 0, [53, 55, 58, null]], [55, "divide", 0, 0, [54, 56, 57]], @@ -1035,46 +1060,44 @@ describe("AST2BlockList Class", () => { [59, "pitch", 0, 0, [58, 60, 61, null]], [60, ["solfege", { "value": "sol" }], 0, 0, [59]], [61, ["number", { "value": 4 }], 0, 0, [59]], - [62, "until", 0, 0, [53, 63, 64, 73]], + [62, "until", 0, 0, [53, 63, 64, 72]], [63, ["boolean", { "value": true }], 0, 0, [62]], - [64, "vspace", 0, 0, [62, 65]], - [65, "newnote", 0, 0, [64, 66, 69, null]], - [66, "divide", 0, 0, [65, 67, 68]], - [67, ["number", { "value": 1 }], 0, 0, [66]], - [68, ["number", { "value": 4 }], 0, 0, [66]], - [69, "vspace", 0, 0, [65, 70]], - [70, "pitch", 0, 0, [69, 71, 72, null]], - [71, ["solfege", { "value": "sol" }], 0, 0, [70]], - [72, ["number", { "value": 4 }], 0, 0, [70]], - [73, "switch", 0, 0, [62, 74, 75, null]], - [74, ["number", { "value": 1 }], 0, 0, [73]], - [75, "case", 0, 0, [73, 76, 77, 88]], - [76, ["number", { "value": 1 }], 0, 0, [75]], - [77, "newnote", 0, 0, [75, 78, 81, 85]], - [78, "divide", 0, 0, [77, 79, 80]], - [79, ["number", { "value": 1 }], 0, 0, [78]], - [80, ["number", { "value": 4 }], 0, 0, [78]], - [81, "vspace", 0, 0, [77, 82]], - [82, "pitch", 0, 0, [81, 83, 84, null]], - [83, ["solfege", { "value": "sol" }], 0, 0, [82]], - [84, ["number", { "value": 4 }], 0, 0, [82]], - [85, "break", 0, 0, [77, 86, null, null]], - [86, "break", 0, 0, [85, 87, null, null]], - [87, "break", 0, 0, [86, null, null, null]], - [88, "defaultcase", 0, 0, [75, 89, null, null]], - [89, "newnote", 0, 0, [88, 90, 93, null]], - [90, "divide", 0, 0, [89, 91, 92]], - [91, ["number", { "value": 1 }], 0, 0, [90]], - [92, ["number", { "value": 4 }], 0, 0, [90]], - [93, "vspace", 0, 0, [89, 94]], - [94, "pitch", 0, 0, [93, 95, 96, null]], - [95, ["solfege", { "value": "5" }], 0, 0, [94]], - [96, ["number", { "value": 4 }], 0, 0, [94]] + [64, "newnote", 0, 0, [62, 65, 68, null]], + [65, "divide", 0, 0, [64, 66, 67]], + [66, ["number", { "value": 1 }], 0, 0, [65]], + [67, ["number", { "value": 4 }], 0, 0, [65]], + [68, "vspace", 0, 0, [64, 69]], + [69, "pitch", 0, 0, [68, 70, 71, null]], + [70, ["solfege", { "value": "sol" }], 0, 0, [69]], + [71, ["number", { "value": 4 }], 0, 0, [69]], + [72, "switch", 0, 0, [62, 73, 74, null]], + [73, ["number", { "value": 1 }], 0, 0, [72]], + [74, "case", 0, 0, [72, 75, 76, 87]], + [75, ["number", { "value": 1 }], 0, 0, [74]], + [76, "newnote", 0, 0, [74, 77, 80, 84]], + [77, "divide", 0, 0, [76, 78, 79]], + [78, ["number", { "value": 1 }], 0, 0, [77]], + [79, ["number", { "value": 4 }], 0, 0, [77]], + [80, "vspace", 0, 0, [76, 81]], + [81, "pitch", 0, 0, [80, 82, 83, null]], + [82, ["solfege", { "value": "sol" }], 0, 0, [81]], + [83, ["number", { "value": 4 }], 0, 0, [81]], + [84, "break", 0, 0, [76, 85]], + [85, "break", 0, 0, [84, 86]], + [86, "break", 0, 0, [85, null]], + [87, "defaultcase", 0, 0, [74, 88, null]], + [88, "newnote", 0, 0, [87, 89, 92, null]], + [89, "divide", 0, 0, [88, 90, 91]], + [90, ["number", { "value": 1 }], 0, 0, [89]], + [91, ["number", { "value": 4 }], 0, 0, [89]], + [92, "vspace", 0, 0, [88, 93]], + [93, "pitch", 0, 0, [92, 94, 95, null]], + [94, ["solfege", { "value": "5" }], 0, 0, [93]], + [95, ["number", { "value": 4 }], 0, 0, [93]] ]; const AST = acorn.parse(code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(AST); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(AST, config); expect(blockList).toEqual(expectedBlockList); }); }); \ No newline at end of file diff --git a/js/js-export/ast2blocklist.js b/js/js-export/ast2blocklist.js index a9e0c3bd9e..f4a864a6cf 100644 --- a/js/js-export/ast2blocklist.js +++ b/js/js-export/ast2blocklist.js @@ -26,1098 +26,591 @@ * which can be loaded into the musicblocks UI by calling `blocks.loadNewBlocks(blockList)`. * * Usage: - * let trees = AST2BlockList.toTrees(AST); - * let blockList = AST2BlockList.toBlockList(trees); + * let blockList = AST2BlockList.toBlockList(AST, config); */ class AST2BlockList { - /** - * Given a musicblocks AST ("type": "Program"), return an array of trees. - * Each AST contains one to multiple top level blocks, each to be converted to a tree. - * For example: - * { - * name: "start", - * children: [ { - * name: "settimbre", - * args: ["guitar"], - * children: [ - * child1, - * child2 ] - * } - * ] - * } - * Each tree node contains up to three properties: name, args, and children. Each child - * is another tree node. For example, child1 could be something like: - * { - * name: "newnote" - * args: [1] - * children: [ { - * name: "pitch" - * args: ["sol", 3] - * } ] - * } - * The entire block is to play a whole note Sol on guitar in the third octave. - * - * @param {Object} AST - AST generated from JavaScript code - * @returns {Array} trees, see example above - */ - static toTrees(AST) { - // An array of predicate and visitor pairs - use the visitor to get information from - // a body AST node if the predicate evaluates to true. Each visitor defines three getters: - // getName finds the name in the AST node of the statement and returns it - // getArguments returns an array of AST nodes that contain the arguments of the statement - // getChildren returns an array of AST nodes that contain the children of the statement - // The AST structures for different statement types are different. Therefore, a new entry - // needs to be added to the array in order to support a new statement type. - const _bodyVisitors = [ - // Action Palette, Start block - { - pred: (bodyAST) => { - return bodyAST.type == "ExpressionStatement" && - bodyAST.expression.type == "NewExpression"; - }, - visitor: { - getName: () => { return "start"; }, - getArguments: () => { return []; }, - getChildren: (bodyAST) => { - for (const arg of bodyAST.expression.arguments) { - if (arg.type == "ArrowFunctionExpression") { - return arg.body.body; - } - } - return []; - }, - } - }, + static toBlockList(AST, config) { + let trees = _astToTree(AST, config); + return _treeToBlockList(trees, config); - // Action Palette, Action block (Action Declaration) - { - pred: (bodyAST) => { - return bodyAST.type == "VariableDeclaration" && - bodyAST.declarations[0].init.type == "ArrowFunctionExpression"; - }, - visitor: { - getName: () => { return "action"; }, - getArguments: (bodyAST) => { return [bodyAST.declarations[0].id]; }, - getChildren: (bodyAST) => { return bodyAST.declarations[0].init.body.body; }, + /** + * Given a musicblocks AST ("type": "Program"), return an array of trees. + * Each AST contains one to multiple top level blocks, each to be converted to a tree. + * For example: + * { + * name: "start", + * children: [ { + * name: "settimbre", + * args: ["guitar"], + * children: [ + * child1, + * child2 ] + * } + * ] + * } + * Each tree node contains up to three properties: name, args, and children. Each child + * is another tree node. For example, child1 could be something like: + * { + * name: "newnote" + * args: [1] + * children: [ { + * name: "pitch" + * args: ["sol", 3] + * } ] + * } + * The entire block is to play a whole note Sol on guitar in the third octave. + * + * @param {Object} AST - AST generated from JavaScript code + * @param {Array} config - JSON config that maps AST to corresponding blocks + * @returns {Array} trees, see example above + */ + function _astToTree(AST, config) { + // Load argument properties configuration + const argConfigs = config.argument_blocks; + + // Implementation of toTrees(AST). + let root = {}; + for (let body of AST.body) { + _createNodeAndAddToTree(body, root); + } + return root["children"]; + + // + // Helper functions + // + + function _getPropertyValue(obj, path) { + const steps = path.split("."); + let current = obj; + for (let step of steps) { + // Regex matching to handle array case such as arguments[0] + const matchStep = step.match(/(\w+)\[(\d+)\]/); + if (matchStep) { + // Following the example above, the output of matchStep will have + // 'arguments' at index 1 and the index (0) will be at index 2 + current = current[matchStep[1]]; + current = current[matchStep[2]]; + } else { + current = current[step]; + } } - }, + return current; + } - // Action Palette, Async call: await action(...) - { - pred: (bodyAST) => { - return bodyAST.type == "ExpressionStatement" && - bodyAST.expression.type == "AwaitExpression" && - bodyAST.expression.argument.type == "CallExpression" && - bodyAST.expression.argument.callee.type == "Identifier"; - }, - visitor: { - getName: (bodyAST) => { return { nameddo: bodyAST.expression.argument.callee.name }; }, - getArguments: () => { return []; }, - getChildren: () => { return []; }, - } - }, + function _matchBody(bodyAST) { + for (const entry of config.body_blocks) { + if (!("ast" in entry)) continue; - // Async call: await mouse.playNote(...) - { - pred: (bodyAST) => { - return bodyAST.type == "ExpressionStatement" && - bodyAST.expression.type == "AwaitExpression" && - bodyAST.expression.argument.type == "CallExpression" && - bodyAST.expression.argument.callee.type == "MemberExpression"; - }, - visitor: { - getName: (bodyAST) => { - let obj = bodyAST.expression.argument.callee.object.name; - let member = bodyAST.expression.argument.callee.property.name; - let numArgs = bodyAST.expression.argument.arguments.length; - if (obj in _memberLookup && member in _memberLookup[obj]) { - let name = _memberLookup[obj][member]; - if (typeof name === "object") { - if (!(numArgs in name)) { - throw { - //TODO - }; + // Group identifiers by property path + const propertyGroups = {}; + for (const identifier of entry.ast.identifiers) { + if (!propertyGroups[identifier.property]) { + propertyGroups[identifier.property] = []; + } + propertyGroups[identifier.property].push(identifier); + } + // Check if all property groups match + let matched = true; + for (const [property, identifiers] of Object.entries(propertyGroups)) { + const value = _getPropertyValue(bodyAST, property); + let groupMatched = false; + + // Check if any identifier in this group matches + for (const identifier of identifiers) { + if ("value" in identifier) { + if (value === identifier.value) { + groupMatched = true; + break; } - name = name[numArgs]; + } else if ("has_value" in identifier && ((!identifier.has_value && value == null) || + (identifier.has_value && value != null))) { + groupMatched = true; + break; + } else if (!("has_value" in identifier) && identifier.size === value.length) { + groupMatched = true; + break; } - return name; } - throw { - message: `Unsupported AsyncCallExpression: ${obj}.${member}`, - start: bodyAST.expression.argument.callee.start, - end: bodyAST.expression.argument.callee.end - }; - }, - getArguments: (bodyAST) => { return bodyAST.expression.argument.arguments; }, - getChildren: (bodyAST) => { - for (const arg of bodyAST.expression.argument.arguments) { - if (arg.type == "ArrowFunctionExpression") { - return arg.body.body; - } + if (!groupMatched) { + matched = false; + break; } - return []; - }, - } - }, - - // Flow Palette, Repeat block - { - pred: (bodyAST) => { - return bodyAST.type == "ForStatement"; - }, - visitor: { - getName: () => { return "repeat"; }, - getArguments: (bodyAST) => { return [bodyAST.test.right]; }, - getChildren: (bodyAST) => { return bodyAST.body.body; }, + } + if (matched) { + return entry; + } } - }, + return null; + } - // Flow Palette, While and Forever block - { - pred: (bodyAST) => { - return bodyAST.type == "WhileStatement"; - }, - visitor: { - getName: (bodyAST) => { return bodyAST.test.value ? "forever" : "while"; }, - getArguments: (bodyAST) => { return bodyAST.test.value ? [] : [bodyAST.test]; }, - getChildren: (bodyAST) => { return bodyAST.body.body; }, + function _matchArgument(arg) { + for (const entry of argConfigs) { + let arg_type = entry.ast; + let matched = true; + for (const identifier of arg_type.identifiers) { + let value = _getPropertyValue(arg, identifier.property); + if ("size" in identifier) { + if (value.length !== identifier.size) { + matched = false; + break; + } + } else if (value !== identifier.value) { + matched = false; + break; + } + } + if (matched) { + return entry; + } } - }, + return null; + } - // Flow Palette, DoWhile (until) block - { - pred: (bodyAST) => { - return bodyAST.type == "DoWhileStatement"; - }, - visitor: { - getName: () => { return "until"; }, - getArguments: (bodyAST) => { return [bodyAST.test]; }, - getChildren: (bodyAST) => { return bodyAST.body.body; }, + function _createNodeAndAddToTree(bodyAST, parent) { + let pair = _matchBody(bodyAST); + if (pair === null) { + throw { + prefix: "Unsupported statement: ", + start: bodyAST.start, + end: bodyAST.end + }; } - }, - - // Flow Palette, If/Ifelse block - { - pred: (bodyAST) => { - return bodyAST.type == "IfStatement"; - }, - visitor: { - getName: (bodyAST) => { return bodyAST.alternate ? "ifthenelse" : "if"; }, - getArguments: (bodyAST) => { return [bodyAST.test]; }, - getChildren: (bodyAST) => { - if (bodyAST.alternate) { - return bodyAST.consequent.body.concat([{ "type": null, }], bodyAST.alternate.body); - } else { - return bodyAST.consequent.body; - } - }, + if (!("name" in pair)) { + return; } - }, - // Special case for else section of ifelse block - { - pred: (bodyAST) => { - return bodyAST.type == null; - }, - visitor: { - getName: () => { return "else"; }, - getArguments: () => { return []; }, - getChildren: () => { return []; }, + let node = {}; + // Set block name + if ("name_property" in pair.ast) { + node["name"] = { [pair.name]: _getPropertyValue(bodyAST, pair.ast.name_property) }; + } else { + node["name"] = pair.name; } - }, - // Flow Palette, Switch block - { - pred: (bodyAST) => { - return bodyAST.type == "SwitchStatement"; - }, - visitor: { - getName: () => { return "switch"; }, - getArguments: (bodyAST) => { return [bodyAST.discriminant]; }, - getChildren: (bodyAST) => { return bodyAST.cases; }, + // Set arguments + if (pair.arguments !== undefined) { + let argArray = []; + for (const argPath of pair.ast.argument_properties) { + argArray.push(_getPropertyValue(bodyAST, argPath)); + } + let args = _createArgNode(argArray); + if (args.length > 0) { + node["arguments"] = args; + } } - }, - - // Flow Palette, Case/Default block - { - pred: (bodyAST) => { - return bodyAST.type == "SwitchCase"; - }, - visitor: { - getName: (bodyAST) => { return bodyAST.test != null ? "case" : "defaultcase"; }, - getArguments: (bodyAST) => { return bodyAST.test != null ? [bodyAST.test] : []; }, - getChildren: (bodyAST) => { return bodyAST.consequent; }, + // Set children + if (pair.ast.children_properties !== undefined) { + for (const child of _getPropertyValue(bodyAST, pair.ast.children_properties[0])) { + if (child.type != "ReturnStatement") { + _createNodeAndAddToTree(child, node); + } + } + if (pair.ast.children_properties.length > 1) { + node["children"].push({ "name": "else" }); + for (const child of _getPropertyValue(bodyAST, pair.ast.children_properties[1])) { + if (child.type != "ReturnStatement") { + _createNodeAndAddToTree(child, node); + } + } + } } - }, - - // Flow Palette, break statement - { - pred: (bodyAST) => { - return bodyAST.type == "BreakStatement"; - }, - visitor: { - getName: (bodyAST) => { return "break"; }, - getArguments: (bodyAST) => { return []; }, - getChildren: (bodyAST) => { return []; }, + // Add the node to the children list of the parent. + if (parent["children"] === undefined) { + parent["children"] = []; } - }, + parent["children"].push(node); + } - // Boxes Palette, Store in box block (Variable Declaration) - { - pred: (bodyAST) => { - return bodyAST.type == "VariableDeclaration" && - (bodyAST.declarations[0].init.type == "Literal" || // var v = 6; - bodyAST.declarations[0].init.type == "BinaryExpression" || // var v = 2 * 3; - bodyAST.declarations[0].init.type == "CallExpression" || // var v = Math.abs(-6); - bodyAST.declarations[0].init.type == "UnaryExpression"); // var v = -6; - }, - visitor: { - getName: (bodyAST) => { return { storein2: bodyAST.declarations[0].id.name }; }, - getArguments: (bodyAST) => { return [bodyAST.declarations[0].init]; }, - getChildren: () => { return []; }, - } - }, + function _createArgNode(argASTNodes) { + let argNodes = []; + for (const arg of argASTNodes) { + let argNode = null; - // Boxes Palette, Add / Subtract block (Assignment) - { - pred: (bodyAST) => { - return bodyAST.type == "ExpressionStatement" && - bodyAST.expression.type == "AssignmentExpression"; - }, - visitor: { - getName: (bodyAST) => { - if (bodyAST.expression.right.type == "BinaryExpression" && - bodyAST.expression.left.name == bodyAST.expression.right.left.name) { - // box1 = box1 - 1; - if (bodyAST.expression.right.operator == "-" && - bodyAST.expression.right.right.value == 1) { - return "decrementOne"; - } - // box1 = box1 + 3; or - // box1 = box1 + -3; - if (bodyAST.expression.right.operator == "+") { - return "increment"; - } - } + // Find matching configuration for this argument type + let argConfig = _matchArgument(arg); + if (!argConfig) { throw { - prefix: "Unsupported AssignmentExpression: ", - start: bodyAST.expression.start, - end: bodyAST.expression.end + prefix: `Unsupported argument type ${arg.type}: `, + start: arg.start, + end: arg.end }; - }, - getArguments: (bodyAST) => { - if (bodyAST.expression.right.type == "BinaryExpression" && - bodyAST.expression.left.name == bodyAST.expression.right.left.name) { - if (bodyAST.expression.right.operator == "-" && - bodyAST.expression.right.right.value == 1) { - return [bodyAST.expression.left]; + } + + if ("value_property" in argConfig.ast) { + argNode = _getPropertyValue(arg, argConfig.ast.value_property); + } else if (argConfig.ast.identifier_property) { + argNode = { "identifier": _getPropertyValue(arg, argConfig.ast.identifier_property) }; + } else { + const name = _getPropertyValue(arg, argConfig.ast.name_property); + if (name in argConfig.name_map) { + let blockName = argConfig.name_map[name]; + let args = []; + if ("arguments_property" in argConfig.ast) { + args = _getPropertyValue(arg, argConfig.ast.arguments_property); + } else { + for (const property of argConfig.ast.argument_properties) { + args.push(_getPropertyValue(arg, property)); + } + } + if (typeof blockName === "object") { + blockName = blockName[args.length]; } - if (bodyAST.expression.right.operator == "+") { - return [bodyAST.expression.left, bodyAST.expression.right.right]; + if (blockName === undefined) { + throw { + prefix: `Unsupported argument count for ${name}: `, + start: arg.start, + end: arg.end + }; } + argNode = { + "name": blockName, + "arguments": _createArgNode(args) + }; + } else { + throw { + prefix: `Unsupported operator ${name}: `, + start: arg.start, + end: arg.end + }; } - throw { - prefix: "Unsupported AssignmentExpression: ", - start: bodyAST.expression.start, - end: bodyAST.expression.end - }; - }, - getChildren: () => { return []; }, - } - }, - - // Ignore MusicBlocks.run() - { - pred: (bodyAST) => { - return bodyAST.type == "ExpressionStatement" && - bodyAST.expression.type == "CallExpression" && - bodyAST.expression.callee.type == "MemberExpression" && - bodyAST.expression.callee.object.name == "MusicBlocks" && - bodyAST.expression.callee.property.name == "run"; - }, - visitor: null - }, - ]; + } - // A map from argument AST node types to builders that build tree nodes for - // argument AST nodes with the corresponding types. - // The AST structures for different argument types are different. Therefore, a new entry - // needs to be added to the map in order to support a new argument type. - const _argNodeBuilders = { - "Identifier": (argAST) => { - return { identifier: argAST.name }; - }, - - "Literal": (argAST) => { - return argAST.value; - }, - - "BinaryExpression": (argAST) => { - if (!(argAST.operator in _binaryOperatorLookup)) { - throw { - message: `Unsupported binary operator: ${argAST.operator}`, - start: argAST.start, - end: argAST.end - }; + if (argNode !== null) { + argNodes.push(argNode); + } } - return { - "name": _binaryOperatorLookup[argAST.operator], - "args": _createArgNode([argAST.left, argAST.right]) - }; - }, - - "CallExpression": (argAST) => { - let obj = argAST.callee.object.name; - let member = argAST.callee.property.name; - let numArgs = argAST.arguments.length; - if (!(obj in _memberLookup && member in _memberLookup[obj])) { - throw { - message: `Unsupported function call: ${obj}.${member}`, - start: argAST.callee.start, - end: argAST.callee.end - }; - } - let name = _memberLookup[obj][member]; - if (typeof name === "object") { - name = name[numArgs]; - } - let args = argAST.arguments; - if (name == "getDict") { - // For getValue(Key, Dictionary Name), - // make sure the order is [Dictionary Name, Key] - args = [argAST.arguments[1], argAST.arguments[0]]; - } - return { - "name": name, - "args": _createArgNode(args) - }; - }, - - "UnaryExpression": (argAST) => { - if (!(argAST.operator in _unaryOperatorLookup)) { - throw { - message: `Unsupported unary operator: ${argAST.operator}`, - start: argAST.start, - end: argAST.end - }; - } - return { - "name": _unaryOperatorLookup[argAST.operator], - "args": _createArgNode([argAST.argument]) - }; - }, - - "AwaitExpression": (argAST) => { - return _createArgNode([argAST.argument])[0]; - }, - - "ArrowFunctionExpression": () => { - // Ignore the type, it's for children - return null; - }, - }; + return argNodes; + } + } - // A map from member functions in AST to block names. - // Member functions are function names used in JavaScript, while block names are - // recognizable by musicblocks. - const _memberLookup = { - "mouse": { - "setInstrument": "settimbre", - "playNote": "newnote", - "playPitch": "pitch", - "playRest": "rest2", - "dot": "rhythmicdot2", - "tie": "tie", - "multiplyNoteValue": "multiplybeatfactor", - "swing": "newswing2", - "playNoteMillis": "osctime", - "playHertz": "hertz", - // Handle function overloading with different number of arguments - "setValue": { 3: "setDict", 2: "setDict2" }, - "getValue": { 2: "getDict", 1: "getDict2" }, - }, - "Math": { - "abs": "abs", - "floor": "int", - "pow": "power", - "sqrt": "sqrt", - }, - "MathUtility": { - "doCalculateDistance": "distance", - "doOneOf": "oneOf", - "doRandom": "random", - }, - }; + /** + * @param {Object} trees - trees generated from JavaScript AST by toTrees(AST) + * @param {Array} config - JSON config that maps AST to corresponding blocks + * @returns {Array} a blockList that can loaded by musicblocks by calling `blocks.loadNewBlocks(blockList)` + */ + function _treeToBlockList(trees, config) { + // [1,"settimbre",0,0,[0,2,3,null]] or + // [21,["nameddo",{"value":"action"}],421,82,[20]] + function _propertyOf(block) { + const block_name = Array.isArray(block[1]) ? block[1][0] : block[1]; + for (const entry of config.body_blocks) { + if (!("blocklist_connections" in entry)) continue; + if ("name" in entry && entry.name === block_name) { + const connections = { + count: entry.blocklist_connections.length + }; - // A map from binary operators in JavaScript to operators recognizable by musicblocks. - const _binaryOperatorLookup = { - "+": "plus", - "-": "minus", - "*": "multiply", - "/": "divide", - "%": "mod", - "==": "equal", - "!=": "not_equal_to", - "<": "less", - ">": "greater", - "<=": "less_than_or_equal_to", - ">=": "greater_than_or_equal_to", - "|": "or", - "&": "and", - "^": "xor" - }; + // Only add connection indices that exist + const prevIndex = entry.blocklist_connections.indexOf("parent_or_previous_sibling"); + if (prevIndex !== -1) connections.prev = prevIndex; - // A map from unary operators in JavaScript to operators recognizable by musicblocks. - const _unaryOperatorLookup = { - "-": "neg", - "!": "not" - }; + const childIndex = entry.blocklist_connections.indexOf("first_child"); + if (childIndex !== -1) connections.child = childIndex; - // Implementation of toTrees(AST). - let root = {}; - for (let body of AST.body) { - _createNodeAndAddToTree(body, root); - } - return root["children"]; + const nextIndex = entry.blocklist_connections.indexOf("next_sibling"); + if (nextIndex !== -1) connections.next = nextIndex; - // - // Helper functions - // + const secondChildIndex = entry.blocklist_connections.indexOf("second_child"); + if (secondChildIndex !== -1) connections.second_child = secondChildIndex; - /** - * Create a tree node starting at the give AST node and add it to parent, which is also a tree node. - * A tree node is an associated array with one to three elements, for example: - * {name: "settimbre", args: ["guitar"], children: [child1, child2]}. - * - * @param {Object} bodyAST - an element in a 'body' array in the AST - * @param {Object} parent - parent node for the new node created by this method - * @returns {Void} - */ - function _createNodeAndAddToTree(bodyAST, parent) { - let visitor = undefined; - for (const entry of _bodyVisitors) { - if (entry.pred(bodyAST)) { - visitor = entry.visitor; - break; + return "body" in entry.default_vspaces ? { + type: "block", + connections: connections, + vspaces: entry.default_vspaces.body + } : { + type: "block", + connections: connections, + argument_v_spaces: entry.default_vspaces.argument + }; + } } - } - if (visitor === undefined) { - throw { - prefix: "Unsupported statement: ", - start: bodyAST.start, - end: bodyAST.end + // doesn't match means it is a vspace block + return { + type: "block", + connections: { + count: 2, + prev: 0, + next: 1 + }, + vspaces: 1, + argument_v_spaces: 0 }; } - if (visitor === null) { - return; - } - let node = {}; - // Set block name - node["name"] = visitor.getName(bodyAST); - // Set arguments - let args = _createArgNode(visitor.getArguments(bodyAST)); - if (args.length > 0) { - node["args"] = args; + // Implementation of toBlockList(trees). + let blockList = []; + let x = 200; + for (let tree of trees) { + let blockNumber = _createBlockAndAddToList(tree, blockList)["blockNumber"]; + // Set (x, y) for the top level blocks. + blockList[blockNumber][2] = x; + blockList[blockNumber][3] = 200; + x += 300; } - // Set children - for (const child of visitor.getChildren(bodyAST)) { - if (child.type != "ReturnStatement") { - _createNodeAndAddToTree(child, node); + return blockList; + + /** + * Create a block for the tree node and add the block to the blockList. + * Each block is [block number, block descriptor, x position, y position, [connections]], where + * the actual number of connections and the meaning of each connection varies from block type to block type. + * For example, for a settimbre block, connection 0 is the parent or the previous sibling of the node, + * connection 1 is the arguments of the node, connection 2 is the first child of the node, and connection 3 + * is the next sibling of the node. + * For a pitch block, connection 0 is the parent or the previous sibling of the node, connection 1 is the + * first argument of the node - solfege, connection 2 is the second argument of the node - octave, and + * connection 3 is the next sibling of the node. + * For a number block (always as an argument), such as a divide block, connection 0 is the node that this + * divide is its argument (e.g. a newnote block), connection 1 is numerator, and connection 2 is denominator. + * + * @param {Object} node - the tree node for which a new block is to be created + * @param {Array} blockList - where the new block is going to be added to + * @returns {Number} the number (index in blockList) of the newly created block + */ + + function _createBlockAndAddToList(node, blockList) { + let block = []; + let blockNumber = blockList.length; + block.push(blockNumber); + blockList.push(block); + if ((typeof node.name) === "object") { + let blockName = Object.keys(node.name)[0]; + block.push([blockName, { "value": node.name[blockName] }]); + } else if (node.name !== "else") { + block.push(node.name); } - } - - // Add the node to the children list of the parent. - if (parent["children"] === undefined) { - parent["children"] = []; - } - parent["children"].push(node); - } - - /** - * Create and return an array for the given array of AST nodes for arguments. - * For exmple, two Literal AST nodes with value 1 and 2 will result in an array [1, 2]; - * One binary AST node "1 / 2" will result in an array [{"name": "divide", "args": [1, 2]}]. - * - * @param {Array} argASTNodes - an arry of AST nodes for arguments - * @returns {Array} an array of all the arguments, note that an argument can be another node - */ - function _createArgNode(argASTNodes) { - let argNodes = []; - for (const arg of argASTNodes) { - const argNodeBuilder = _argNodeBuilders[arg.type]; - if (argNodeBuilder === undefined) { - throw { - prefix: `Unsupported argument type ${arg.type}: `, - start: arg.start, - end: arg.end - }; - } - const argNode = argNodeBuilder(arg); - if (argNode !== null) { - argNodes.push(argNode); - } - } - return argNodes; - } - } - - /** - * @param {Object} trees - trees generated from JavaScript AST by toTrees(AST) - * @returns {Array} a blockList that can loaded by musicblocks by calling `blocks.loadNewBlocks(blockList)` - */ - static toBlockList(trees) { - // A map from block name to its argument handling function. - // Keys do not include the blocks that do not take arguments (e.g., start block or action call block) - // or can only be used as arguments - any block with a connector on its left side. - // Each value is a function to add blocks for the arguments and return the number of vertical spaces - // that the argument blocks will take. For example, an action definition with name for the action will - // take 1 vertical space for its argument. playPitch(1/4, ...) will have its argument blocks take 2 - // vertical spaces. With this information, the code can decide how many vspacers to add. - const _argHandlers = { - // Action Palette, Action block (Action Declaration) takes a string literal for action name - // Example: let chunk1 = async mouse => {...} - // args: [{"identifier": "chunk1"}] => - // [2,["text",{"value":"chunck1"}],0,0,[1]] - "action": (args, blockList, parentBlockNumber) => { - return _addNthArgToBlockList(["text", { "value": args[0].identifier }], 1, blockList, parentBlockNumber); - }, - - // Tone Palette, set instrument block takes a string literal for instrument name - // Example: mouse.setInstrument("guitar", async () => {...}); - // args: ["guitar"] => - // [2,["voicename",{"value":"guitar"}],0,0,[1]] - "settimbre": (args, blockList, parentBlockNumber) => { - return _addNthArgToBlockList(["voicename", { "value": args[0] }], 1, blockList, parentBlockNumber); - }, - - // Pitch Palette, pitch block takes a note name and a number expression for octave - // Example: mouse.playPitch("sol", 4); - // args: ["sol", 4] => - // [10,["solfege",{"value":"sol"}],0,0,[9]], - // [11,["number",{"value":4}],0,0,[9]] - // args: ["G", 4] => - // [10,["notename",{"value":"G"}],0,0,[9]], - // [11,["number",{"value":4}],0,0,[9]] - "pitch": (args, blockList, parentBlockNumber) => { - const notes = new Set(["A", "B", "C", "D", "E", "F", "G"]); - // Add the 1st argument - note name - let vspaces = _addNthArgToBlockList( - [notes.has(args[0].charAt(0)) ? "notename" : "solfege", { "value": args[0] }], - 1, blockList, parentBlockNumber); - // Add the 2nd argument - a number expression for octave - vspaces += _addNthValueArgToBlockList(args[1], 2, blockList, parentBlockNumber); - return vspaces; - }, - - // Rhythm Palette, note block takes a number expression that evaluates to 1, 1/2, 1/4, etc. - // Example: mouse.playNote(1 / 4, async () => {...}); - // args: [{"name": "divide"}, "args": [1, 4]] => - // [8, "divide", 0, 0, [7, 9, 10]], - // [9, ["number", { "value": 1 }], 0, 0, [8]], - // [10, ["number", { "value": 4 }], 0, 0, [8]], - "newnote": _addValueArgsToBlockList, - - // Dot block takes in a whole number - // Example: mouse.dot(3, async () => {...}); - // args: [3] - "rhythmicdot2": _addValueArgsToBlockList, - - // Multiply note value block takes in a number expression that evaluates to 1, 1/2, 1/4, etc. - // Example: mouse.multiplyNoteValue(1 / 4, async () => {...}); - // args: [{"name": "divide"}, "args": [1, 4]] => - // [7, "divide", 0, 0, [6, 8, 9]], - // [8, ["number", { "value": 1 }], 0, 0, [7]], - // [9, ["number", { "value": 4 }], 0, 0, [7]], - "multiplybeatfactor": _addValueArgsToBlockList, - - // Swing block takes two args, a number expression as swing value, and another expression as note value - // Example: mouse.swing(1 / 24, 1 / 8, async () => {...}); - "newswing2": _addValueArgsToBlockList, - - // Milliseconds note block takes in a number expression that evaluates to 1, 1/2, 1/4, etc. - // Example: mouse.playNoteMillis(1000 / (3 / 2), async () => {...}); - "osctime": _addValueArgsToBlockList, - - // Hertz block takes in a whole number - // Example: mouse.playHertz(392); - "hertz": _addValueArgsToBlockList, - - // Boxes Palette, subtract 1 from block takes a string identifier for variable - // Example: box1 = box1 - 1; - // args: [{"identifier": "box1"}] - // [2,["namedbox",{"value":"box1"}],0,0,[1]] - "decrementOne": (args, blockList, parentBlockNumber) => { - return _addNthArgToBlockList(["namedbox", { "value": args[0].identifier }], 1, blockList, parentBlockNumber); - }, - - // Boxes Palette, add block takes a string identifier for variable and a number expression - // Example: box1 = box1 + 2; - // args: [{"identifier": "box1"}, 2] => - // [10,["namedbox",{"value":"box1"}],0,0,[9]], - // [11,["number",{"value":2}],0,0,[9]] - "increment": (args, blockList, parentBlockNumber) => { - // Add the 1st argument - variable name - let vspaces = _addNthArgToBlockList(["namedbox", { "value": args[0].identifier }], 1, blockList, parentBlockNumber); - // Add the 2nd argument - a number expression - vspaces += _addNthValueArgToBlockList(args[1], 2, blockList, parentBlockNumber); - return vspaces; - }, - - // Boxes Palette, store in box block takes a number or boolean expression - // Example: var box1 = 2 * 5; - // args: [{"name": "multiply"}, "args": [2, 5]] => - // [8, "multiply", 0, 0, [7, 9, 10]], - // [9, ["number", { "value": 2 }], 0, 0, [8]], - // [10, ["number", { "value": 5 }], 0, 0, [8]], - "storein2": _addValueArgsToBlockList, - - // Dictionary Palette, set value block takes a string for the name of the dictionary, a string for the key, and a value - // Example: mouse.setValue("times", 3, "dict"); - // args: ["times", 3, "dict"] => - // [8,["text",{"value":"dict"}],0,0,[7]], - // [9,["text",{"value":"times"}],0,0,[7]], - // [10,["number",{"value":3}],0,0,[7]] - "setDict": (args, blockList, parentBlockNumber) => { - // Add the 1st argument - Dictionary name - let vspaces = _addNthArgToBlockList(["text", { "value": args[2] }], 1, blockList, parentBlockNumber); - // Add the 2nd argument - Key - vspaces += _addNthArgToBlockList(["text", { "value": args[0] }], 2, blockList, parentBlockNumber); - // Add the 3rd argument - Value - vspaces += _addNthValueArgToBlockList(args[1], 3, blockList, parentBlockNumber); - return vspaces; - }, - - // Dictionary Palette, set value block takes a string for the key, and a value - // Example: mouse.setValue("times", 3); - // args: ["times", 3] => - // [8,["text",{"value":"times"}],0,0,[7]], - // [9,["number",{"value":3}],0,0,[7]] - "setDict2": (args, blockList, parentBlockNumber) => { - // Add the 1st argument - Key - let vspaces = _addNthArgToBlockList(["text", { "value": args[0] }], 1, blockList, parentBlockNumber); - // Add the 2nd argument - Value - vspaces += _addNthValueArgToBlockList(args[1], 2, blockList, parentBlockNumber); - return vspaces; - }, - - // Flow Palette, repeat block takes a number expression - "repeat": _addValueArgsToBlockList, - - // Flow Palette, while block takes a condition - "while": _addValueArgsToBlockList, - - // Flow Palette, until block takes a condition - "until": _addValueArgsToBlockList, - - // Flow Palette, if block takes a boolean expression - "if": _addValueArgsToBlockList, + block.push(0); // x + block.push(0); // y - // Flow Palette, if block takes a boolean expression - "ifthenelse": _addValueArgsToBlockList, + let property = _propertyOf(block); + let connections = new Array(property.connections.count).fill(null); + block.push(connections); - // Flow Palette, switch block takes a numerical expression - "switch": _addValueArgsToBlockList, + // Process arguments + let argVSpaces = _createArgBlockAndAddToList(node, blockList, blockNumber); + let vspaces = Math.max(1, argVSpaces); // A node takes at least 1 vertical space - // Flow Palette, case block takes a numerical expression - "case": _addValueArgsToBlockList, - }; + // Process children + if (node["children"] !== undefined && node["children"].length > 0) { + let elseIndex = node.children.findIndex(child => child.name === "else"); - // A map from block name to its properties including: - // 1. An object that describes its connections such as the number of connections, - // the indices of its first child block, next sibling block, etc. - // 2. For blocks that may have children, the vertical spaces allowed for its arguments - // before extra v-spacers are needed in order to prevent its argument blocks from - // covering its child blocks. - // 3. For blocks that don't have children, the vertical spaces allowed for its arguments - // before extra v-spacers are needed in order to prevent its argument blocks from - // covering its sibling blocks. - // Keys do not include the blocks that can only be used as arguments - any block with - // a connector on its left side. - const _blockProperties = { - "start": { - // null, child, null - connections: { count: 3, "child": 1 }, - argVSpaces: 1, - }, - "action": { - // Action declaration - // null, arg (action name), child, null - connections: { count: 4, "child": 2 }, - argVSpaces: 1, - }, - "nameddo": { - // Action call - // prev, next - connections: { count: 2, "prev": 0, "next": 1 }, - vspaces: 1, - }, - "settimbre": { - // prev, arg (instrument name), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "pitch": { - // prev, arg1 (solfege), arg2 (octave), next - connections: { count: 4, "prev": 0, "next": 3 }, - vspaces: 2, - }, - "rest2": { - // prev, next - connections: { count: 2, "prev": 0, "next": 1 }, - vspaces: 1, - }, - "rhythmicdot2": { - // prev, arg (duration), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "tie": { - // prev, child, next - connections: { count: 3, "prev": 0, "child": 1, "next": 2 }, - argVSpaces: 0, - }, - "multiplybeatfactor": { - // prev, arg (duration), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "newswing2": { - // prev, arg1 (swing), arg2 (note), child, next - connections: { count: 5, "prev": 0, "child": 3, "next": 4 }, - argVSpaces: 2, - }, - "osctime": { - // prev, arg (duration), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "hertz": { - // prev, child, next - connections: { count: 4, "prev": 0, "child": 1, "next": 2 }, - argVSpaces: 1, - }, - "newnote": { - // prev, arg (note), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "decrementOne": { - // prev, arg (variable name), next - connections: { count: 3, "prev": 0, "next": 2 }, - vspaces: 1, - }, - "increment": { - // prev, arg1 (variable name), arg2 (value), next - connections: { count: 4, "prev": 0, "next": 3 }, - vspaces: 2, - }, - "storein2": { - // prev, arg (variable name), next - connections: { count: 3, "prev": 0, "next": 2 }, - vspaces: 1, - }, - "setDict": { - // prev, arg1 (dictionary name), arg2 (key), arg3 (value), next - connections: { count: 5, "prev": 0, "next": 4 }, - vspaces: 3, - }, - "setDict2": { - // prev, arg1 (key), arg2 (value), next - connections: { count: 4, "prev": 0, "next": 3 }, - vspaces: 2, - }, - "repeat": { - // prev, arg (repeat counts), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "while": { - // prev, arg (condition), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 2, - }, - "forever": { - // prev, child, next - connections: { count: 3, "prev": 0, "child": 1, "next": 2 }, - argVSpaces: 0, - }, - "until": { - // prev, arg (condition), child, next - connections: { count: 3, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 0, - }, - "if": { - // prev, arg (condition), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 2, - }, - "ifthenelse": { - // prev, arg (condition), child (if and else cases), next - connections: { count: 5, "prev": 0, "child": 2, "next": 4 }, - argVSpaces: 2, - }, - "switch": { - // prev, arg (variable), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "case": { - // prev, arg (condition), child, next - connections: { count: 4, "prev": 0, "child": 2, "next": 3 }, - argVSpaces: 1, - }, - "defaultcase": { - // prev, child, next - connections: { count: 4, "prev": 0, "child": 1, "next": 2 }, - argVSpaces: 0, - }, - "break": { - // prev, next - connections: { count: 4, "prev": 0, "next": 1 }, - vspaces: 1, - }, - "vspace": { - // prev, next - connections: { count: 2, "prev": 0, "next": 1 } - }, - }; + // Split children into if/else groups if applies (most cases first group will contain all children) + let firstGroup = (elseIndex !== -1) ? node.children.slice(0, elseIndex) : node.children; + let secondGroup = (elseIndex !== -1) ? node.children.slice(elseIndex + 1) : []; - // [1,"settimbre",0,0,[0,2,3,null]] or - // [21,["nameddo",{"value":"action"}],421,82,[20]] - function _propertyOf(block) { - return _blockProperties[Array.isArray(block[1]) ? block[1][0] : block[1]]; - } - - // Implementation of toBlockList(trees). - let blockList = []; - let x = 200; - for (let tree of trees) { - let blockNumber = _createBlockAndAddToList(tree, blockList)["blockNumber"]; - // Set (x, y) for the top level blocks. - blockList[blockNumber][2] = x; - blockList[blockNumber][3] = 200; - x += 300; - } - return blockList; + // Process first children group + let ret = _processChildren(firstGroup, argVSpaces - property.argument_v_spaces, blockList); + vspaces += ret.vspaces; - /** - * Create a block for the tree node and add the block to the blockList. - * Each block is [block number, block descriptor, x position, y position, [connections]], where - * the actual number of connections and the meaning of each connection varies from block type to block type. - * For example, for a settimbre block, connection 0 is the parent or the previous sibling of the node, - * connection 1 is the arguments of the node, connection 2 is the first child of the node, and connection 3 - * is the next sibling of the node. - * For a pitch block, connection 0 is the parent or the previous sibling of the node, connection 1 is the - * first argument of the node - solfege, connection 2 is the second argument of the node - octave, and - * connection 3 is the next sibling of the node. - * For a number block (always as an argument), such as a divide block, connection 0 is the node that this - * divide is its argument (e.g. a newnote block), connection 1 is numerator, and connection 2 is denominator. - * - * @param {Object} node - the tree node for which a new block is to be created - * @param {Array} blockList - where the new block is going to be added to - * @returns {Number} the number (index in blockList) of the newly created block - */ + // Set child-parent connection for first group + if (property.connections.child !== undefined) { + connections[property.connections.child] = ret.firstChildBlockNumber; + let childBlock = blockList[ret.firstChildBlockNumber]; + let childProperty = _propertyOf(childBlock); + if (childProperty.connections.prev !== undefined) { + childBlock[4][childProperty.connections.prev] = blockNumber; + } + } + + // Process second children group (else case in ifelse block) + if (elseIndex !== -1) { + let ret = _processChildren(secondGroup, 0, blockList); + vspaces += ret.vspaces; + // Set child-parent connection for second group + if (property.connections.second_child !== undefined) { + connections[property.connections.second_child] = ret.firstChildBlockNumber; + let childBlock = blockList[ret.firstChildBlockNumber]; + let childProperty = _propertyOf(childBlock); + if (childProperty.connections.prev !== undefined) { + childBlock[4][childProperty.connections.prev] = blockNumber; + } + } + } + } - function _createBlockAndAddToList(node, blockList) { - let block = []; - let blockNumber = blockList.length; - block.push(blockNumber); - blockList.push(block); - if ((typeof node.name) === "object") { - let blockName = Object.keys(node.name)[0]; - block.push([blockName, { "value": node.name[blockName] }]); - } else if (node.name !== "else") { - block.push(node.name); + return { "blockNumber": blockNumber, "vspaces": vspaces }; } - block.push(0); // x - block.push(0); // y - - let property = _propertyOf(block); - let connections = new Array(property.connections.count).fill(null); - block.push(connections); - // Process arguments - let argVSpaces = _createArgBlockAndAddToList(node, blockList, blockNumber); - let vspaces = Math.max(1, argVSpaces); // A node takes at least 1 vertical space + // Helper to process a group of children (create and establish connections between them) + function _processChildren(children, padding, blockList) { + let childBlockNumbers = []; + let vspaces = 0; - // Process children - if (node["children"] !== undefined && node["children"].length > 0) { - let elseIndex = node.children.findIndex(child => child.name === "else"); - - // Split children into if/else groups if applies (most cases first group will contain all children) - let firstGroup = (elseIndex !== -1) ? node.children.slice(0, elseIndex) : node.children; - let secondGroup = (elseIndex !== -1) ? node.children.slice(elseIndex + 1) : []; + // Add vertical spacers if the arguments take too much vertical spaces + for (let i = 0; i < padding; i++) { + childBlockNumbers.push(_addVSpacer(blockList)); + vspaces++; + } - // Process first children group - let ret = _processChildren(firstGroup, argVSpaces - property.argVSpaces, blockList); - vspaces += ret.vspaces; + // Add the children + for (const child of children) { + let ret = _createBlockAndAddToList(child, blockList); + childBlockNumbers.push(ret.blockNumber); + vspaces += ret.vspaces; - // Set child-parent connection for first group - connections[property.connections["child"]] = ret.firstChildBlockNumber; - let childBlock = blockList[ret.firstChildBlockNumber]; - property = _propertyOf(childBlock); - childBlock[4][property.connections["prev"]] = blockNumber; + let childProperty = _propertyOf(blockList[ret.blockNumber]); + for (let i = 0; i < ret.vspaces - childProperty.vspaces; i++) { + childBlockNumbers.push(_addVSpacer(blockList)); + } + } - // Process second children group (else case in ifelse block) - if (elseIndex !== -1) { - let ret = _processChildren(secondGroup, 0, blockList); - vspaces += ret.vspaces; - // Set child-parent connection for second group - connections[property.connections["child"] + 1] = ret.firstChildBlockNumber; - let childBlock = blockList[ret.firstChildBlockNumber]; - property = _propertyOf(childBlock); - childBlock[4][property.connections["prev"]] = blockNumber; + // Establish connections between children + // Parent of children is their previous sibling, except the first one + for (let i = 1; i < childBlockNumbers.length; i++) { + let childBlock = blockList[childBlockNumbers[i]]; + let property = _propertyOf(childBlock); + if (property.connections.prev !== undefined) { + childBlock[4][property.connections.prev] = childBlockNumbers[i - 1]; + } } - // For blocks with children, add 1 to vspaces for the end of the clamp. - vspaces += 1; + // Set the next sibling block number for the children, except the last one + for (let i = 0; i < childBlockNumbers.length - 1; i++) { + let childBlock = blockList[childBlockNumbers[i]]; + let property = _propertyOf(childBlock); + if (property.connections.next !== undefined) { + childBlock[4][property.connections.next] = childBlockNumbers[i + 1]; + } + } + return { "firstChildBlockNumber": childBlockNumbers[0], "vspaces": vspaces }; } - return { "blockNumber": blockNumber, "vspaces": vspaces }; - } - - // Helper to process a group of children (create and establish connections between them) - function _processChildren(children, padding, blockList) { - let childBlockNumbers = []; - let vspaces = 0; - - // Add vertical spacers if the arguments take too much vertical spaces - for (let i = 0; i < padding; i++) { - childBlockNumbers.push(_addVSpacer(blockList)); - vspaces++; + function _addVSpacer(blockList) { + let block = []; // A block for the vertical spacer + let blockNumber = blockList.length; + block.push(blockNumber); + blockList.push(block); + block.push("vspace"); + block.push(0); // x + block.push(0); // y + block.push([null, null]); // connections, prev and next + return blockNumber; } - // Add the children - for (const child of children) { - let ret = _createBlockAndAddToList(child, blockList); - childBlockNumbers.push(ret.blockNumber); - vspaces += ret.vspaces; - - let childProperty = _propertyOf(blockList[ret.blockNumber]); - for (let i = 0; i < ret.vspaces - childProperty.vspaces; i++) { - childBlockNumbers.push(_addVSpacer(blockList)); + function _createArgBlockAndAddToList(node, blockList, parentBlockNumber) { + if (node.arguments === undefined || node.arguments.length == 0) { + return 0; } - } - // Establish connections between children - // Parent of children is their previous sibling, except the first one - for (let i = 1; i < childBlockNumbers.length; i++) { - let childBlock = blockList[childBlockNumbers[i]]; - let property = _propertyOf(childBlock); - childBlock[4][property.connections["prev"]] = childBlockNumbers[i - 1]; - } - // Set the next sibling block number for the children, except the last one - for (let i = 0; i < childBlockNumbers.length - 1; i++) { - let childBlock = blockList[childBlockNumbers[i]]; - let property = _propertyOf(childBlock); - childBlock[4][property.connections["next"]] = childBlockNumbers[i + 1]; - } - return { "firstChildBlockNumber": childBlockNumbers[0], "vspaces": vspaces }; - } + let block = blockList[parentBlockNumber]; + let property = _propertyOf(block); + let vspaces = 0; + + // Find the block configuration + const block_name = Array.isArray(block[1]) ? block[1][0] : block[1]; + let blockConfig = null; + for (const entry of config.body_blocks) { + if (entry.name === block_name) { + blockConfig = entry; + break; + } + } - function _addVSpacer(blockList) { - let block = []; // A block for the vertical spacer - let blockNumber = blockList.length; - block.push(blockNumber); - blockList.push(block); - block.push("vspace"); - block.push(0); // x - block.push(0); // y - block.push([null, null]); // connections, prev and next - return blockNumber; - } + if (!blockConfig || !blockConfig.arguments) { + throw new Error(`Cannot find argument configuration for: ${block_name}`); + } - function _createArgBlockAndAddToList(node, blockList, parentBlockNumber) { - if (node.args === undefined || node.args.length == 0) { - return 0; - } - let argHandlerKey = (typeof node.name) === "object" ? Object.keys(node.name)[0] : node.name; - let argHandler = _argHandlers[argHandlerKey]; - if (argHandler === undefined) { - throw new Error(`Cannot find argument handler for: ${argHandlerKey}`); + // Process each argument with its corresponding configuration + for (let i = 0; i < node.arguments.length; i++) { + const arg = node.arguments[i]; + const argConfig = blockConfig.arguments[i]; + if (argConfig.type === "note_or_solfege") { + // Handle pitch notes (solfege or note names) + const notes = new Set(["A", "B", "C", "D", "E", "F", "G"]); + vspaces += _addNthArgToBlockList( + [notes.has(arg.charAt(0)) ? "notename" : "solfege", { "value": arg }], + i + 1, blockList, parentBlockNumber); + } else if (argConfig.type === "ValueExpression") { + // Handle value expressions (like storein2) + vspaces += _addNthValueArgToBlockList(arg, i + 1, blockList, parentBlockNumber); + } else if (argConfig.type === "NumberExpression" || argConfig.type === "BooleanExpression") { + // Handle number/boolean expressions + vspaces += _addNthValueArgToBlockList(arg, i + 1, blockList, parentBlockNumber); + } else { + vspaces += _addNthArgToBlockList([argConfig.type, { "value": typeof arg === "object" ? arg.identifier : arg }], i + 1, blockList, parentBlockNumber); + } + } + return vspaces; } - return argHandler(node.args, blockList, parentBlockNumber); - } - - // Add a new block to the blockList for the nth argument (1-indexed) of the parent block. - function _addNthArgToBlockList(arg, nth, blockList, parentBlockNumber) { - let block = []; // A block for the argument - let blockNumber = blockList.length; - block.push(blockNumber); - blockList.push(block); - block.push(arg); - block.push(0); // x - block.push(0); // y - block.push([parentBlockNumber]); // connections - let parentConnections = blockList[parentBlockNumber][4]; - parentConnections[nth] = blockNumber; - return 1; // vspaces - } - /** - * Examples: - * - * args: [2] => - * [5,["number",{"value":2}],0,0,[4]], - * - * args: [{"name": "divide", - * "args": [1,4]}] => - * [5,"divide",0,0,[4,6,7]], - * [6,["number",{"value":1}],0,0,[5]], - * [7,["number",{"value":4}],0,0,[5]] - * - * args: [{"name": "abs", - * "args": [{"name": "neg", - * "args": [1]}]}] => - * [5,"abs",0,0,[4,6,7]], - * [6,["neg",0,0,[5,7]], - * [7,["number",{"value":1}],0,0,[6]] - * - * @param {Object} args - the args property of a tree node - * @param {Array} blockList - the blockList to which the new argument blocks will be added - * @param {Number} parentBlockNumber - the number of the parent block of the new argument blocks - */ - function _addValueArgsToBlockList(args, blockList, parentBlockNumber) { - let vspaces = 0; - for (let i = 0; i < args.length; i++) { - vspaces += _addNthValueArgToBlockList(args[i], i + 1, blockList, parentBlockNumber); + // Add a new block to the blockList for the nth argument (1-indexed) of the parent block. + function _addNthArgToBlockList(arg, nth, blockList, parentBlockNumber) { + let block = []; // A block for the argument + let blockNumber = blockList.length; + block.push(blockNumber); + blockList.push(block); + block.push(arg); + block.push(0); // x + block.push(0); // y + block.push([parentBlockNumber]); // connections + let parentConnections = blockList[parentBlockNumber][4]; + parentConnections[nth] = blockNumber; + return 1; // vspaces } - return vspaces; - } - function _addNthValueArgToBlockList(arg, nth, blockList, parentBlockNumber) { - let vspaces = 0; - let block = []; - let blockNumber = blockList.length; - block.push(blockNumber); - blockList.push(block); - let type = typeof arg; - if (type === "string") { - type = "text"; + /** + * Examples: + * + * args: [2] => + * [5,["number",{"value":2}],0,0,[4]], + * + * args: [{"name": "divide", + * "args": [1,4]}] => + * [5,"divide",0,0,[4,6,7]], + * [6,["number",{"value":1}],0,0,[5]], + * [7,["number",{"value":4}],0,0,[5]] + * + * args: [{"name": "abs", + * "args": [{"name": "neg", + * "args": [1]}]}] => + * [5,"abs",0,0,[4,6,7]], + * [6,["neg",0,0,[5,7]], + * [7,["number",{"value":1}],0,0,[6]] + * + * @param {Object} args - the args property of a tree node + * @param {Array} blockList - the blockList to which the new argument blocks will be added + * @param {Number} parentBlockNumber - the number of the parent block of the new argument blocks + */ + function _addValueArgsToBlockList(args, blockList, parentBlockNumber) { + let vspaces = 0; + for (let i = 0; i < args.length; i++) { + vspaces += _addNthValueArgToBlockList(args[i], i + 1, blockList, parentBlockNumber); + } + return vspaces; } - if (type === "number" || type === "boolean" || type === "text" || - (type === "object" && arg.identifier !== undefined)) { - // variables can be in number or boolean expressions - block.push(type === "object" ? ["namedbox", { "value": arg.identifier }] : [type, { "value": arg }]); - block.push(0); // x - block.push(0); // y - // Initialize connections with just the parent. - block.push([parentBlockNumber]); - vspaces = 1; - } else if (type === "object") { - block.push(arg.name); - block.push(0); // x - block.push(0); // y - let connections = new Array(1 + arg.args.length).fill(null); - connections[0] = parentBlockNumber; - block.push(connections); - vspaces = _addValueArgsToBlockList(arg.args, blockList, blockNumber); - } else { - throw new Error(`Unsupported value argument: ${arg}`); + + function _addNthValueArgToBlockList(arg, nth, blockList, parentBlockNumber) { + let vspaces = 0; + let block = []; + let blockNumber = blockList.length; + block.push(blockNumber); + blockList.push(block); + let type = typeof arg; + if (type === "string") { + type = "text"; + } + if (type === "number" || type === "boolean" || type === "text" || + (type === "object" && arg.identifier !== undefined)) { + // variables can be in number or boolean expressions + block.push(type === "object" ? ["namedbox", { "value": arg.identifier }] : [type, { "value": arg }]); + block.push(0); // x + block.push(0); // y + // Initialize connections with just the parent. + block.push([parentBlockNumber]); + vspaces = 1; + } else if (type === "object") { + block.push(arg.name); + block.push(0); // x + block.push(0); // y + let connections = new Array(1 + arg.arguments.length).fill(null); + connections[0] = parentBlockNumber; + block.push(connections); + vspaces = _addValueArgsToBlockList(arg.arguments, blockList, blockNumber); + } else { + throw new Error(`Unsupported value argument: ${arg}`); + } + let parentConnections = blockList[parentBlockNumber][4]; + parentConnections[nth] = blockNumber; + return vspaces; } - let parentConnections = blockList[parentBlockNumber][4]; - parentConnections[nth] = blockNumber; - return vspaces; } } } diff --git a/js/js-export/ast2blocks.json b/js/js-export/ast2blocks.json new file mode 100644 index 0000000000..db5e6c4f2d --- /dev/null +++ b/js/js-export/ast2blocks.json @@ -0,0 +1,1409 @@ +{ + "argument_blocks": [ + { + "comment": "Variable name like 'box1' in the Boxes palette or action name like 'action' in the Action palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "Identifier" + } + ], + "identifier_property": "name" + } + }, + { + "comment": "Number expression like '1 / 4' in the Number palette or boolean expressions like 'box1 >= 0' in the Boolean palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "BinaryExpression" + } + ], + "name_property": "operator", + "argument_properties": [ + "left", + "right" + ] + }, + "name_map": { + "+": "plus", + "-": "minus", + "*": "multiply", + "/": "divide", + "%": "mod", + "==": "equal", + "!=": "not_equal_to", + "<": "less", + ">": "greater", + "<=": "less_than_or_equal_to", + ">=": "greater_than_or_equal_to", + "|": "or", + "&": "and", + "^": "xor" + } + }, + { + "comment": "Unary expressions such as ! in boolean palette or - in number palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "UnaryExpression" + } + ], + "name_property": "operator", + "argument_properties": [ + "argument" + ] + }, + "name_map": { + "-": "neg", + "!": "not" + } + }, + { + "comment": "Literals such as numbers or booleans or strings", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "Literal" + } + ], + "value_property": "value" + } + }, + { + "comment": "Dictionary get function for 2 arguments", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "AwaitExpression" + }, + { + "property": "argument.type", + "value": "CallExpression" + }, + { + "property": "argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "argument.callee.property.name", + "value": "getValue" + }, + { + "property": "argument.arguments", + "size": 2 + } + ], + "name_property": "argument.callee.property.name", + "argument_properties": [ + "argument.arguments[1]", + "argument.arguments[0]" + ] + }, + "name_map": { + "getValue": "getDict" + } + }, + { + "comment": "Dictionary get function for 1 argument", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "AwaitExpression" + }, + { + "property": "argument.type", + "value": "CallExpression" + }, + { + "property": "argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "argument.callee.property.name", + "value": "getValue" + }, + { + "property": "argument.arguments", + "size": 1 + } + ], + "name_property": "argument.callee.property.name", + "argument_properties": [ + "argument.arguments[0]" + ] + }, + "name_map": { + "getValue": "getDict2" + } + }, + { + "comment": "Skip, this is for children", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ArrowFunctionExpression" + } + ] + } + }, + { + "comment": "Math operators such as absolute value or square", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "CallExpression" + }, + { + "property": "callee.type", + "value": "MemberExpression" + }, + { + "property": "callee.object.type", + "value": "Identifier" + }, + { + "property": "callee.object.name", + "value": "Math" + } + ], + "name_property": "callee.property.name", + "arguments_property": "arguments" + }, + "name_map": { + "abs": "abs", + "floor": "int", + "pow": "power", + "sqrt": "sqrt" + } + }, + { + "comment": "Math utility operators such as distance or random", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "CallExpression" + }, + { + "property": "callee.type", + "value": "MemberExpression" + }, + { + "property": "callee.object.type", + "value": "Identifier" + }, + { + "property": "callee.object.name", + "value": "MathUtility" + } + ], + "name_property": "callee.property.name", + "arguments_property": "arguments" + }, + "name_map": { + "doCalculateDistance": "distance", + "doOneOf": "oneOf", + "doRandom": "random" + } + } + ], + "body_blocks": [ + { + "comments": "run is ignored", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "CallExpression" + }, + { + "property": "expression.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.callee.object.name", + "value": "MusicBlocks" + }, + { + "property": "expression.callee.property.name", + "value": "run" + } + ] + } + }, + { + "name": "storein2", + "comments": "Variable assignment in the Boxes palette like 'var box1 = 2 * 5;'", + "arguments": [ + { + "type": "ValueExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "VariableDeclaration" + }, + { + "property": "declarations[0].init.type", + "value": "Literal" + }, + { + "property": "declarations[0].init.type", + "value": "BinaryExpression" + }, + { + "property": "declarations[0].init.type", + "value": "UnaryExpression" + }, + { + "property": "declarations[0].init.type", + "value": "CallExpression" + } + ], + "name_property": "declarations[0].id.name", + "argument_properties": [ + "declarations[0].init" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "decrementOne", + "comments": "Decrement by one block in the Boxes palette 'box1 = box1 - 1;'", + "arguments": [ + { + "type": "namedbox" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AssignmentExpression" + }, + { + "property": "expression.right.type", + "value": "BinaryExpression" + }, + { + "property": "expression.right.operator", + "value": "-" + }, + { + "property": "expression.right.right.value", + "value": 1 + } + ], + "argument_properties": [ + "expression.left" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "increment", + "comments": "Increment block in the Boxes palette 'box1 = box1 + 3;'", + "arguments": [ + { + "type": "namedbox" + }, + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AssignmentExpression" + }, + { + "property": "expression.right.type", + "value": "BinaryExpression" + }, + { + "property": "expression.right.operator", + "value": "+" + } + ], + "argument_properties": [ + "expression.left", + "expression.right.right" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 2 + } + }, + { + "name": "settimbre", + "comments": "Set instrument block", + "arguments": [ + { + "type": "voicename" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "setInstrument" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "newnote", + "comments": "Note block in the Rhythm palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "playNote" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "rhythmicdot2", + "comments": "Dot block in the Rhythm palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "dot" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "tie", + "comments": "Tie block in the Rhythm palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "tie" + } + ], + "children_properties": [ + "expression.argument.arguments[0].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 0 + } + }, + { + "name": "multiplybeatfactor", + "comments": "Multiply note value block in the Rhythm palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "multiplyNoteValue" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "newswing2", + "comments": "Swing block in the Rhythm palette", + "arguments": [ + { + "type": "NumberExpression" + }, + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "swing" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]", + "expression.argument.arguments[1]" + ], + "children_properties": [ + "expression.argument.arguments[2].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 2 + } + }, + { + "name": "osctime", + "comments": "Millisecond note block in the Rhythm palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "playNoteMillis" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "pitch", + "comments": "Pitch block", + "arguments": [ + { + "type": "note_or_solfege" + }, + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "playPitch" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]", + "expression.argument.arguments[1]" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 2 + } + }, + { + "name": "hertz", + "comments": "Hertz block", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "playHertz" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "rest2", + "comments": "Rest block", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "playRest" + } + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "nameddo", + "comments": "Action palette, async block", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "Identifier" + } + ], + "name_property": "expression.argument.callee.name" + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "start", + "comments": "Start block in the Flow palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "NewExpression" + } + ], + "children_properties": [ + "expression.arguments[0].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "action", + "comments": "Action block in the Action palette", + "arguments": [ + { + "type": "text" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "VariableDeclaration" + }, + { + "property": "declarations[0].init.type", + "value": "ArrowFunctionExpression" + } + ], + "argument_properties": [ + "declarations[0].id" + ], + "children_properties": [ + "declarations[0].init.body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "repeat", + "comments": "Repeat block in the Flow palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ForStatement" + } + ], + "argument_properties": [ + "test.right" + ], + "children_properties": [ + "body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "forever", + "comments": "Forever block in the Flow palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "WhileStatement" + }, + { + "property": "test.raw", + "value": "1000" + } + ], + "children_properties": [ + "body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "while", + "comments": "While block in the Flow palette", + "arguments": [ + { + "type": "BooleanExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "WhileStatement" + } + ], + "argument_properties": [ + "test" + ], + "children_properties": [ + "body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 2 + } + }, + { + "name": "until", + "comments": "Do While block in the Flow palette", + "arguments": [ + { + "type": "BooleanExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "DoWhileStatement" + } + ], + "argument_properties": [ + "test" + ], + "children_properties": [ + "body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 2 + } + }, + { + "name": "switch", + "comments": "Switch block in the Flow palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "SwitchStatement" + } + ], + "argument_properties": [ + "discriminant" + ], + "children_properties": [ + "cases" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "defaultcase", + "comments": "Default case block in the Flow palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "SwitchCase" + }, + { + "property": "test", + "has_value": false + } + ], + "children_properties": [ + "consequent" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "case", + "comments": "Case block in the Flow palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "SwitchCase" + }, + { + "property": "test.type", + "value": "Literal" + } + ], + "argument_properties": [ + "test" + ], + "children_properties": [ + "consequent" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "break", + "comments": "Break block in the Flow palette", + "ast": { + "identifiers": [ + { + "property": "type", + "value": "BreakStatement" + } + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "next_sibling" + ], + "default_vspaces": { + "body": 1 + } + }, + { + "name": "if", + "comments": "Basic if block in the Flow palette", + "arguments": [ + { + "type": "BooleanExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "IfStatement" + }, + { + "property": "alternate", + "has_value": false + } + ], + "argument_properties": [ + "test" + ], + "children_properties": [ + "consequent.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 2 + } + }, + { + "name": "ifthenelse", + "comments": "If-then-else block in the Flow palette", + "arguments": [ + { + "type": "BooleanExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "IfStatement" + }, + { + "property": "alternate", + "has_value": true + } + ], + "argument_properties": [ + "test" + ], + "children_properties": [ + "consequent.body", + "alternate.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "second_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 2 + } + }, + { + "name": "crescendo", + "comments": "Crescendo block in the Volume palette", + "arguments": [ + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "doCrescendo" + } + ], + "argument_properties": [ + "expression.argument.arguments[0]" + ], + "children_properties": [ + "expression.argument.arguments[1].body.body" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "first_child", + "next_sibling" + ], + "default_vspaces": { + "argument": 1 + } + }, + { + "name": "setDict", + "comment": "Set value block for dictionary, with specific dictionary", + "arguments": [ + { + "type": "text" + }, + { + "type": "text" + }, + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "setValue" + }, + { + "property": "expression.argument.arguments", + "size": 3 + } + ], + "argument_properties": [ + "expression.argument.arguments[2]", + "expression.argument.arguments[0]", + "expression.argument.arguments[1]" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 3 + } + }, + { + "name": "setDict2", + "comment": "Dictionary blocks, the set functions", + "arguments": [ + { + "type": "text" + }, + { + "type": "NumberExpression" + } + ], + "ast": { + "identifiers": [ + { + "property": "type", + "value": "ExpressionStatement" + }, + { + "property": "expression.type", + "value": "AwaitExpression" + }, + { + "property": "expression.argument.type", + "value": "CallExpression" + }, + { + "property": "expression.argument.callee.type", + "value": "MemberExpression" + }, + { + "property": "expression.argument.callee.property.name", + "value": "setValue" + }, + { + "property": "expression.argument.arguments", + "size": 2 + } + ], + "argument_properties": [ + "expression.argument.arguments[0]", + "expression.argument.arguments[1]" + ] + }, + "blocklist_connections": [ + "parent_or_previous_sibling", + "argument", + "argument", + "next_sibling" + ], + "default_vspaces": { + "body": 2 + } + } + ] +} \ No newline at end of file diff --git a/js/widgets/jseditor.js b/js/widgets/jseditor.js index 6a2f6a4a28..255fc84f81 100644 --- a/js/widgets/jseditor.js +++ b/js/widgets/jseditor.js @@ -453,8 +453,7 @@ class JSEditor { try { let ast = acorn.parse(this._code, { ecmaVersion: 2020 }); - let trees = AST2BlockList.toTrees(ast); - let blockList = AST2BlockList.toBlockList(trees); + let blockList = AST2BlockList.toBlockList(ast, ast2blocklist_config); const activity = this.activity; // Wait for the old blocks to be removed, then load new blocks. const __listener = (event) => {