/// <summary> /// Update the generator code. /// </summary> /// <param name="block"> Rendered block in preview workspace.</param> public static void updateGenerator(Blockly.Block block) { var language = ((HTMLSelectElement)Document.GetElementById("language")).Value; var generatorStub = FactoryUtils.getGeneratorStub(block, language); FactoryUtils.injectCode(generatorStub, "generatorPre"); }
/// <summary> /// Check to see if more than one input has this name. /// Highly inefficient (On^2), but n is small. /// </summary> /// <param name="referenceBlock">Block to check.</param> public static void inputNameCheck(Blockly.Block referenceBlock) { if (referenceBlock.workspace == null) { // Block has been deleted. return; } var name = referenceBlock.getFieldValue("INPUTNAME").ToLowerCase(); var count = 0; var blocks = referenceBlock.workspace.getAllBlocks(); for (var i = 0; i < blocks.Length; i++) { var block = blocks[i]; var otherName = block.getFieldValue("INPUTNAME"); if (!block.disabled && !block.getInheritedDisabled() && otherName != null && otherName.ToLowerCase() == name) { count++; } } var msg = (count > 1) ? "There are " + count + " input blocks\n with this name." : null; referenceBlock.setWarningText(msg); }
/// <summary> /// Checks if a block has a variable field. Blocks with variable fields cannot /// be shadow blocks. /// </summary> /// <param name="block"> The block to check if a variable field exists.</param> /// <returns>True if the block has a variable field, false otherwise.</returns> public static bool hasVariableField(Blockly.Block block) { if (block == null) { return(false); } return(block.getVars().Length > 0); }
/// <summary> /// Checks if a block is a procedures block. If procedures block names are /// ever updated or expanded, this function should be updated as well (no /// other known markers for procedure blocks beyond name). /// </summary> /// <param name="block"> The block to check.</param> /// <returns>True if the block is a procedure block, false otherwise.</returns> public static bool isProcedureBlock(Blockly.Block block) { return(block != null && (block.type == "procedures_defnoreturn" || block.type == "procedures_defreturn" || block.type == "procedures_callnoreturn" || block.type == "procedures_callreturn" || block.type == "procedures_ifreturn")); }
/// <summary> /// Get block definition code for the current block. /// </summary> /// <param name="blockType"></param> Type of block. /// <param name="rootBlock"> RootBlock from main workspace in which /// user uses Block Factory Blocks to create a custom block.</param> /// <param name="format">"JSON" or "JavaScript".</param> /// <param name="workspace"> Where the root block lives.</param> /// <returns>Block definition.</returns> public static string getBlockDefinition(string blockType, Blockly.Block rootBlock, string format, Blockly.Workspace workspace) { string code = null; blockType = blockType.Replace(new Regex(@"\W", RegexOptions.Multiline), "_").Replace(new Regex(@"^(\d)"), "_\\1"); switch (format) { case "JSON": code = FactoryUtils.formatJson_(blockType, rootBlock); break; case "JavaScript": code = FactoryUtils.formatJavaScript_(blockType, rootBlock, workspace); break; } return(code); }
/// <summary> /// Fetch the type(s) defined in the given input. /// </summary> /// <param name="block"> Block with input.</param> /// <param name="name">Name of the input.</param> /// <returns>List of types.</returns> private static JsArray <string> getTypesFrom_(Blockly.Block block, string name) { var typeBlock = block.getInputTargetBlock(name); JsArray <string> types; if (typeBlock == null || typeBlock.disabled) { types = new JsArray <string>(); } else if (typeBlock.type == "type_other") { types = new JsArray <string> { FactoryUtils.escapeString(typeBlock.getFieldValue("TYPE")) }; } else if (typeBlock.type == "type_group") { types = new JsArray <string>(); for (var n = 0; n < ((TypeGroup)typeBlock).typeCount_; n++) { types = types.Concat(FactoryUtils.getTypesFrom_(typeBlock, "TYPE" + n)); } // Remove duplicates. var hash = new Dictionary <string, object>(); for (var n = types.Length - 1; n >= 0; n--) { if (hash.ContainsKey(types[n])) { types.Splice(n, 1); } hash[types[n]] = true; } } else { var fi = typeBlock.GetType().GetField("valueType", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); types = new JsArray <string> { FactoryUtils.escapeString((string)fi.GetValue(null)) }; } return(types); }
/// <summary> /// Fetch the type(s) defined in the given input. /// Format as a string for appending to the generated code. /// </summary> /// <param name="block">Block with input.</param> /// <param name="name"> Name of the input.</param> /// <returns>String defining the types.</returns> public static string getOptTypesFrom(Blockly.Block block, string name) { var types = FactoryUtils.getTypesFrom_(block, name); if (types.Length == 0) { return(null); } else if (types.IndexOf("null") != -1) { return("null"); } else if (types.Length == 1) { return(types[0]); } else { return("[" + types.Join(", ") + "]"); } }
/// <summary> /// Get the generator code for a given block. /// </summary> /// <param name="block"> Rendered block in preview workspace.</param> /// <param name="generatorLanguage">"JavaScript", "Python", "PHP", "Lua", /// "Dart".</param> /// <returns>Generator code for multiple blocks.</returns> public static string getGeneratorStub(Blockly.Block block, string generatorLanguage) { var makeVar = new Func <string, string, string>((root, name) => { name = name.ToLowerCase().Replace(new Regex(@"\W", RegexOptions.Multiline), "_"); return(" var " + root + "_" + name); }); // The makevar function lives in the original update generator. var language = generatorLanguage; var code = new JsArray <string>(); code.Push("Blockly." + language + "[\"" + block.type + "\"] = (block) => {"); // Generate getters for any fields or inputs. for (var i = 0; i < block.inputList.Length; i++) { string name; var input = block.inputList[i]; for (var j = 0; j < input.fieldRow.Length; j++) { var field = input.fieldRow[j]; name = field.name; if (String.IsNullOrEmpty(name)) { continue; } if (typeof(Blockly.FieldVariable).IsInstanceOfType(field)) { // Subclass of Blockly.FieldDropdown, must test first. code.Push(makeVar("variable", name) + " = Blockly." + language + ".variableDB_.getName(block.getFieldValue(\"" + name + "\"), Blockly.Variables.NAME_TYPE);"); } else if (typeof(Blockly.FieldAngle).IsInstanceOfType(field)) { // Subclass of Blockly.FieldTextInput, must test first. code.Push(makeVar("angle", name) + " = block.getFieldValue(\"" + name + "\");"); } else if (typeof(Blockly.FieldDate).IsInstanceOfType(field)) { // Blockly.FieldDate may not be compiled into Blockly. code.Push(makeVar("date", name) + " = block.getFieldValue(\"" + name + "\");"); } else if (typeof(Blockly.FieldColour).IsInstanceOfType(field)) { code.Push(makeVar("colour", name) + " = block.getFieldValue(\"" + name + "\");"); } else if (typeof(Blockly.FieldCheckbox).IsInstanceOfType(field)) { code.Push(makeVar("checkbox", name) + " = block.getFieldValue(\"" + name + "\") == \"TRUE\";"); } else if (typeof(Blockly.FieldDropdown).IsInstanceOfType(field)) { code.Push(makeVar("dropdown", name) + " = block.getFieldValue(\"" + name + "\");"); } else if (typeof(Blockly.FieldNumber).IsInstanceOfType(field)) { code.Push(makeVar("number", name) + " = block.getFieldValue(\"" + name + "\");"); } else if (typeof(Blockly.FieldTextInput).IsInstanceOfType(field)) { code.Push(makeVar("text", name) + " = block.getFieldValue(\"" + name + "\");"); } } name = input.name; if (!String.IsNullOrEmpty(name)) { if (input.type == Blockly.Core.INPUT_VALUE) { code.Push(makeVar("value", name) + " = Blockly." + language + ".valueToCode(block, \"" + name + "\", Blockly." + language + ".ORDER_ATOMIC);"); } else if (input.type == Blockly.Core.NEXT_STATEMENT) { code.Push(makeVar("statements", name) + " = Blockly." + language + ".statementToCode(block, \"" + name + "\");"); } } } // Most languages end lines with a semicolon. Python does not. var lineEnd = new Dictionary <string, string> { { "JavaScript", ";" }, { "Python", "" }, { "PHP", ";" }, { "Dart", ";" } }; code.Push(" // TODO: Assemble " + language + " into code variable."); if (block.outputConnection != null) { code.Push(" var code = \"...\";"); code.Push(" // TODO: Change ORDER_NONE to the correct strength."); code.Push(" return [code, Blockly." + language + ".ORDER_NONE];"); } else { code.Push(" var code = \"...\" + (lineEnd[language] || \"\") + \"\\n\";"); code.Push(" return code;"); } code.Push("};"); return(code.Join("\n")); }
/// <summary> /// Returns field strings and any config. /// </summary> /// <param name="block"></param> Input block. /// <returns></returns> Array of static text and field configs. private static JsArray <object> getFieldsJson_(Blockly.Block block) { var fields = new JsArray <object>(); while (block != null) { if (!block.disabled && !block.getInheritedDisabled()) { switch (block.type) { case "field_static": // Result: "hello" fields.Push(block.getFieldValue("TEXT")); break; case "field_input": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), text = block.getFieldValue("TEXT") }); break; case "field_number": var obj = new Dictionary <string, object>(); obj["type"] = block.type; obj["name"] = block.getFieldValue("FIELDNAME"); obj["value"] = Script.ParseFloat(block.getFieldValue("VALUE")); var min = Script.ParseFloat(block.getFieldValue("MIN")); if (min > Double.NegativeInfinity) { obj["min"] = min; } var max = Script.ParseFloat(block.getFieldValue("MAX")); if (max < Double.PositiveInfinity) { obj["max"] = max; } var precision = block.getFieldValue("PRECISION"); if (!String.IsNullOrEmpty(precision)) { obj["precision"] = Script.ParseFloat(precision); } fields.Push(obj); break; case "field_angle": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), angle = Convert.ToDouble(block.getFieldValue("ANGLE")) }); break; case "field_checkbox": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), @checked = block.getFieldValue("CHECKED") == "TRUE" }); break; case "field_colour": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), colour = block.getFieldValue("COLOUR") }); break; case "field_date": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), date = block.getFieldValue("DATE") }); break; case "field_variable": fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), variable = block.getFieldValue("TEXT") ?? null }); break; case "field_dropdown": var count = ((FieldDropdown)block).optionCount_; var options = new JsArray <DropdownItemInfo>(count); for (var i = 0; i < count; i++) { options[i] = new DropdownItemInfo( block.getFieldValue("USER" + i), block.getFieldValue("CPU" + i) ); } if (options.Length >= 0) { fields.Push(new { type = block.type, name = block.getFieldValue("FIELDNAME"), options = options }); } break; case "field_image": fields.Push(new { type = block.type, src = block.getFieldValue("SRC"), width = Convert.ToDouble(block.getFieldValue("WIDTH")), height = Convert.ToDouble(block.getFieldValue("HEIGHT")), alt = block.getFieldValue("ALT") }); break; } } block = block.nextConnection == null ? null : block.nextConnection.targetBlock(); } return(fields); }
/// <summary> /// Returns field strings and any config. /// </summary> /// <param name="block">Input block.</param> /// <returns>Field strings.</returns> private static JsArray <string> getFieldsJs_(Blockly.Block block) { var fields = new JsArray <string>(); while (block != null) { if (!block.disabled && !block.getInheritedDisabled()) { switch (block.type) { case "field_static": // Result: "hello" fields.Push(FactoryUtils.escapeString(block.getFieldValue("TEXT"))); break; case "field_input": // Result: new Blockly.FieldTextInput("Hello"), "GREET" fields.Push("new Blockly.FieldTextInput(" + FactoryUtils.escapeString(block.getFieldValue("TEXT")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_number": // Result: new Blockly.FieldNumber(10, 0, 100, 1), "NUMBER" var args = new JsArray <double> { Script.ParseFloat(block.getFieldValue("VALUE")), Script.ParseFloat(block.getFieldValue("MIN")), Script.ParseFloat(block.getFieldValue("MAX")), Script.ParseFloat(block.getFieldValue("PRECISION")) }; // Remove any trailing arguments that aren't needed. if (args[3] == 0) { args.Pop(); if (args[2] == Double.PositiveInfinity) { args.Pop(); if (args[1] == Double.NegativeInfinity) { args.Pop(); } } } fields.Push("new Blockly.FieldNumber(" + args.Join(", ") + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_angle": // Result: new Blockly.FieldAngle(90), "ANGLE" fields.Push("new Blockly.FieldAngle(" + Script.ParseFloat(block.getFieldValue("ANGLE")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_checkbox": // Result: new Blockly.FieldCheckbox("TRUE"), "CHECK" fields.Push("new Blockly.FieldCheckbox(" + FactoryUtils.escapeString(block.getFieldValue("CHECKED")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_colour": // Result: new Blockly.FieldColour("#ff0000"), "COLOUR" fields.Push("new Blockly.FieldColour(" + FactoryUtils.escapeString(block.getFieldValue("COLOUR")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_date": // Result: new Blockly.FieldDate("2015-02-04"), "DATE" fields.Push("new Blockly.FieldDate(" + FactoryUtils.escapeString(block.getFieldValue("DATE")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_variable": // Result: new Blockly.FieldVariable("item"), "VAR" var varname = FactoryUtils.escapeString(block.getFieldValue("TEXT") ?? null); fields.Push("new Blockly.FieldVariable(" + varname + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_dropdown": // Result: // new Blockly.FieldDropdown([["yes", "1"], ["no", "0"]]), "TOGGLE" var options = new JsArray <string>(); for (var i = 0; i < ((FieldDropdown)block).optionCount_; i++) { options[i] = "[" + FactoryUtils.escapeString(block.getFieldValue("USER" + i)) + ", " + FactoryUtils.escapeString(block.getFieldValue("CPU" + i)) + "]"; } if (options.Length > 0) { fields.Push("new Blockly.FieldDropdown([" + options.Join(", ") + "]), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); } break; case "field_image": // Result: new Blockly.FieldImage("http://...", 80, 60) var src = FactoryUtils.escapeString(block.getFieldValue("SRC")); var width = Convert.ToDouble(block.getFieldValue("WIDTH")); var height = Convert.ToDouble(block.getFieldValue("HEIGHT")); var alt = FactoryUtils.escapeString(block.getFieldValue("ALT")); fields.Push("new Blockly.FieldImage(" + src + ", " + width + ", " + height + ", " + alt + ")"); break; } } block = block.nextConnection == null ? null : block.nextConnection.targetBlock(); } return(fields); }
/// <summary> /// Update the language code as JavaScript. /// </summary> /// <param name="blockType"> Name of block.</param> /// <param name="rootBlock"> Factory_base block.</param> /// <param name="workspace"></param> Where the root block lives. /// <returns>Generated language code.</returns> private static string formatJavaScript_(string blockType, Blockly.Block rootBlock, Blockly.Workspace workspace) { var code = new JsArray <string>(); code.Push("Blockly.Core.Blocks[\"" + blockType + "\"] = {"); code.Push(" init: () => {"); // Generate inputs. var TYPES = new Dictionary <string, string>() { { "input_value", "appendValueInput" }, { "input_statement", "appendStatementInput" }, { "input_dummy", "appendDummyInput" } }; var contentsBlock = rootBlock.getInputTargetBlock("INPUTS"); while (contentsBlock != null) { if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { var name = ""; // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type != "input_dummy") { name = FactoryUtils.escapeString(contentsBlock.getFieldValue("INPUTNAME")); } code.Push(" this." + TYPES[contentsBlock.type] + "(" + name + ")"); var check = FactoryUtils.getOptTypesFrom(contentsBlock, "TYPE"); if (!String.IsNullOrEmpty(check)) { code.Push(" .setCheck(" + check + ")"); } var align = contentsBlock.getFieldValue("ALIGN"); if (align != "LEFT") { code.Push(" .setAlign(Blockly.ALIGN_" + align + ")"); } var fields = FactoryUtils.getFieldsJs_( contentsBlock.getInputTargetBlock("FIELDS")); for (var i = 0; i < fields.Length; i++) { code.Push(" .appendField(" + fields[i] + ")"); } // Add semicolon to last line to finish the statement. code[code.Length - 1] += ";"; } contentsBlock = contentsBlock.nextConnection == null ? null : contentsBlock.nextConnection.targetBlock(); } // Generate inline/external switch. if (rootBlock.getFieldValue("INLINE") == "EXT") { code.Push(" this.setInputsInline(false);"); } else if (rootBlock.getFieldValue("INLINE") == "INT") { code.Push(" this.setInputsInline(true);"); } // Generate output, or next/previous connections. switch (rootBlock.getFieldValue("CONNECTIONS")) { case "LEFT": code.Push(FactoryUtils.connectionLineJs_("setOutput", "OUTPUTTYPE", workspace)); break; case "BOTH": code.Push( FactoryUtils.connectionLineJs_("setPreviousStatement", "TOPTYPE", workspace)); code.Push( FactoryUtils.connectionLineJs_("setNextStatement", "BOTTOMTYPE", workspace)); break; case "TOP": code.Push( FactoryUtils.connectionLineJs_("setPreviousStatement", "TOPTYPE", workspace)); break; case "BOTTOM": code.Push( FactoryUtils.connectionLineJs_("setNextStatement", "BOTTOMTYPE", workspace)); break; } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock("COLOUR"); if (colourBlock != null && !colourBlock.disabled) { var hue = Script.ParseFloat(colourBlock.getFieldValue("HUE")); if (!Double.IsNaN(hue)) { code.Push(" this.setColour(" + hue + ");"); } } code.Push(" this.setTooltip(\"\");"); code.Push(" this.setHelpUrl(\"http://www.example.com/\");"); code.Push(" }"); code.Push("};"); return(code.Join("\n")); }
/// <summary> /// Update the language code as JSON. /// </summary> /// <param name="blockType"> Name of block.</param> /// <param name="rootBlock">Factory_base block.</param> /// <returns>Generanted language code.</returns> private static string formatJson_(string blockType, Blockly.Block rootBlock) { var JS = new Dictionary <string, object>(); // Type is not used by Blockly, but may be used by a loader. JS["type"] = blockType; // Generate inputs. var message = new JsArray <string>(); var args = new JsArray <object>(); var contentsBlock = rootBlock.getInputTargetBlock("INPUTS"); Blockly.Block lastInput = null; while (contentsBlock != null) { if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { var fields = FactoryUtils.getFieldsJson_( contentsBlock.getInputTargetBlock("FIELDS")); for (var i = 0; i < fields.Length; i++) { if (fields[i] is string str) { message.Push(str.Replace(new Regex("%", RegexOptions.Multiline), "%%")); } else { args.Push(fields[i]); message.Push("%" + args.Length); } } var input = new Dictionary <string, object>(); input.Add("type", contentsBlock.type); // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type != "input_dummy") { input["name"] = contentsBlock.getFieldValue("INPUTNAME"); } var check = JSON.Parse( FactoryUtils.getOptTypesFrom(contentsBlock, "TYPE") ?? "null"); if (check != null) { input["check"] = check; } var align = contentsBlock.getFieldValue("ALIGN"); if (align != "LEFT") { input["align"] = align; } args.Push(input); message.Push("%" + args.Length); lastInput = contentsBlock; } contentsBlock = contentsBlock.nextConnection == null ? null : contentsBlock.nextConnection.targetBlock(); } // Remove last input if dummy and not empty. if (lastInput != null && lastInput.type == "input_dummy") { var fields = lastInput.getInputTargetBlock("FIELDS"); if (fields != null && FactoryUtils.getFieldsJson_(fields).Join("").Trim() != "") { var align = lastInput.getFieldValue("ALIGN"); if (align != "LEFT") { JS["lastDummyAlign0"] = align; } args.Pop(); message.Pop(); } } JS["message0"] = message.Join(" "); if (args.Length > 0) { JS["args0"] = args; } // Generate inline/external switch. if (rootBlock.getFieldValue("INLINE") == "EXT") { JS["inputsInline"] = false; } else if (rootBlock.getFieldValue("INLINE") == "INT") { JS["inputsInline"] = true; } // Generate output, or next/previous connections. switch (rootBlock.getFieldValue("CONNECTIONS")) { case "LEFT": JS["output"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "OUTPUTTYPE") ?? "null"); break; case "BOTH": JS["previousStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "TOPTYPE") ?? "null"); JS["nextStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "BOTTOMTYPE") ?? "null"); break; case "TOP": JS["previousStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "TOPTYPE") ?? "null"); break; case "BOTTOM": JS["nextStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "BOTTOMTYPE") ?? "null"); break; } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock("COLOUR"); if (colourBlock != null && !colourBlock.disabled) { var hue = Script.ParseFloat(colourBlock.getFieldValue("HUE")); JS["colour"] = hue; } JS["tooltip"] = ""; JS["helpUrl"] = "http://www.example.com/"; return(JSON.Stringify(JS, null, " ")); }