Files
NeoIDE/src/blocks/functions.js
2026-01-19 23:44:32 -06:00

812 lines
22 KiB
JavaScript

import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
const ARG_BLOCK_TYPE = "FunctionsArgumentBlock";
class CustomChecker extends Blockly.ConnectionChecker {
canConnect(a, b, isDragging, opt_distance) {
if (!isDragging) {
return super.canConnect(a, b, isDragging, opt_distance);
}
const existing = b.targetConnection && b.targetConnection.getSourceBlock();
if (
existing &&
existing.type === "functions_argument_block" &&
existing.isShadow()
) {
return false;
}
return super.canConnect(a, b, isDragging, opt_distance);
}
}
Blockly.registry.register(
Blockly.registry.Type.CONNECTION_CHECKER,
"CustomChecker",
CustomChecker,
true
);
class DuplicateOnDrag {
constructor(block) {
this.block = block;
}
isMovable() {
return true;
}
startDrag(e) {
const ws = this.block.workspace;
let typeToCreate = this.block.type;
if (this.block.argType_ === "statement") {
typeToCreate = "functions_statement_argument_block";
}
let data = this.block.toCopyData();
if (data?.blockState) {
data.blockState.type = typeToCreate;
} else {
data.blockState = { type: typeToCreate };
}
if (this.block.mutationToDom) {
const mutation = this.block.mutationToDom();
if (mutation) {
data.blockState.extraState = mutation.outerHTML;
}
}
this.copy = Blockly.clipboard.paste(data, ws);
this.baseStrat = new Blockly.dragging.BlockDragStrategy(this.copy);
this.copy.setDragStrategy(this.baseStrat);
this.baseStrat.startDrag(e);
}
drag(e) {
this.block.workspace
.getGesture(e)
.getCurrentDragger()
.setDraggable(this.copy);
this.baseStrat.drag(e);
}
endDrag(e) {
this.baseStrat?.endDrag(e);
}
revertDrag(e) {
this.copy?.dispose();
}
}
function typeToBlocklyCheck(type) {
return (
{
string: "String",
number: "Number",
boolean: "Boolean",
array: "Array",
object: "Object",
}[type] || null
);
}
function findDuplicateArgNames(types, names) {
const used = {};
const duplicates = [];
for (let i = 0; i < types.length; i++) {
const key = types[i] + ":" + names[i];
if (!names[i]) continue;
if (used[key]) duplicates.push(i);
else used[key] = true;
}
return duplicates;
}
function isValidIdentifier(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
Blockly.Blocks["functions_argument_block"] = {
init() {
if (!this.argType_) this.argType_ = "string";
if (!this.argName_) this.argName_ = "arg";
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField(
new Blockly.FieldLabel(this.argName_),
"ARG_NAME"
);
this.setOutput(true, null);
this.setMovable(true);
this.setDeletable(true);
setTimeout(() => {
if (this.setDragStrategy && this.isShadow()) {
this.setDragStrategy(new DuplicateOnDrag(this));
}
});
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("type", this.argType_ || "string");
container.setAttribute("name", this.argName_ || "arg");
return container;
},
domToMutation: function (xmlElement) {
const type = xmlElement.getAttribute("type") || "string";
const name = xmlElement.getAttribute("name") || "arg";
this.updateType_(type);
this.updateName_(name);
},
updateType_: function (type) {
this.argType_ = type;
if (type === "statement") {
this.setOutputShape(3);
this.setOutput(true, ARG_BLOCK_TYPE);
} else {
const outputType = typeToBlocklyCheck(type) || "String";
this.setOutput(true, [outputType, ARG_BLOCK_TYPE]);
}
},
updateName_: function (name) {
this.argName_ = name;
if (this.getField("ARG_NAME")) {
this.setFieldValue(name, "ARG_NAME");
} else {
this.appendDummyInput().appendField(
new Blockly.FieldLabel(name),
"ARG_NAME"
);
}
},
};
Blockly.Blocks["functions_statement_argument_block"] = {
init() {
if (!this.argName_) this.argName_ = "arg";
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField(
new Blockly.FieldLabel(this.argName_),
"ARG_NAME"
);
this.setNextStatement(true, "default");
this.setPreviousStatement(true, "default");
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("name", this.argName_ || "arg");
return container;
},
domToMutation: function (xmlElement) {
const name = xmlElement.getAttribute("name") || "arg";
this.updateName_(name);
},
updateName_: function (name) {
this.argName_ = name;
if (this.getField("ARG_NAME")) {
this.setFieldValue(name, "ARG_NAME");
} else {
this.appendDummyInput().appendField(
new Blockly.FieldLabel(name),
"ARG_NAME"
);
}
},
};
Blockly.Blocks["functions_definition"] = {
init: function () {
this.setStyle("procedure_blocks");
this.setTooltip("Function definition with a variable number of inputs.");
this.setInputsInline(true);
this.functionId_ = Blockly.utils.idGenerator.genUid();
this.itemCount_ = 0;
this.argTypes_ = [];
this.argNames_ = [];
this.blockShape_ = "statement";
this.returnTypes_ = [];
this.updateShape_();
this.setMutator(
new Blockly.icons.MutatorIcon(["functions_args_generic"], this)
);
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("functionid", this.functionId_);
container.setAttribute("items", String(this.itemCount_));
container.setAttribute("shape", this.blockShape_ || "statement");
for (let i = 0; i < this.itemCount_; i++) {
const item = Blockly.utils.xml.createElement("item");
item.setAttribute("type", this.argTypes_[i]);
item.setAttribute("name", this.argNames_[i]);
container.appendChild(item);
}
return container;
},
domToMutation: function (xmlElement) {
const items = xmlElement.getAttribute("items");
this.itemCount_ = items ? parseInt(items, 10) : 0;
this.argTypes_ = [];
this.argNames_ = [];
const children = [...xmlElement.children].filter(
n => n.tagName.toLowerCase() === "item"
);
for (let i = 0; i < children.length; i++) {
this.argTypes_[i] = children[i].getAttribute("type");
this.argNames_[i] = children[i].getAttribute("name");
}
while (this.argTypes_.length < this.itemCount_)
this.argTypes_.push("label");
while (this.argNames_.length < this.itemCount_) this.argNames_.push("text");
this.functionId_ =
xmlElement.getAttribute("functionid") ||
Blockly.utils.idGenerator.genUid();
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
this.updateShape_();
},
saveExtraState: function () {
return {
functionId: this.functionId_,
itemCount: this.itemCount_,
argTypes: this.argTypes_,
argNames: this.argNames_,
shape: this.blockShape_,
returnTypes: this.returnTypes_,
};
},
loadExtraState: function (state) {
this.functionId_ = state.functionId || Blockly.utils.idGenerator.genUid();
this.itemCount_ = state.itemCount || 0;
this.argTypes_ = state.argTypes || [];
this.argNames_ = state.argNames || [];
this.blockShape_ = state.shape || "statement";
this.returnTypes_ = state.returnTypes || [];
this.updateShape_();
},
createDefaultArgBlock_: function (type, name = "arg") {
Blockly.Events.disable();
let block;
try {
const ws = this.workspace;
block = ws.newBlock("functions_argument_block");
block.setShadow(true);
block.setEditable(false);
block.updateType_(type);
block.updateName_(name);
if (ws?.rendered) {
block.initSvg();
block.render();
}
} catch (_) {}
Blockly.Events.enable();
return block;
},
updateShape_: function () {
let savedBody = null;
const bodyInput = this.getInput("BODY");
if (bodyInput && bodyInput.connection?.targetConnection) {
savedBody = bodyInput.connection.targetConnection;
}
if (bodyInput) this.removeInput("BODY");
if (this.getInput("EMPTY")) this.removeInput("EMPTY");
if (this.getInput("SHAPE")) this.removeInput("SHAPE");
[...this.inputList].forEach(input => {
const connection = input.connection?.targetConnection;
if (connection) connection.getSourceBlock()?.dispose(false);
this.removeInput(input.name);
});
let firstArgAdded = this.argTypes_[0] === "label";
for (let i = 0; i < this.itemCount_; i++) {
const type = this.argTypes_[i];
const name = this.argNames_[i];
if (type === "label") {
this.appendDummyInput().appendField(new Blockly.FieldLabel(name));
} else {
const input = this.appendValueInput(name).setCheck(
typeToBlocklyCheck(type)
);
if (!firstArgAdded) {
input.appendField("my block with");
firstArgAdded = true;
}
const reporter = this.createDefaultArgBlock_(type, name);
reporter.setFieldValue(name, "ARG_NAME");
try {
reporter.outputConnection.connect(input.connection);
} catch (e) {}
}
}
if (this.itemCount_ === 0) {
this.appendDummyInput("EMPTY").appendField("my block");
}
const newBody = this.appendStatementInput("BODY").setCheck("default");
if (savedBody) {
try {
newBody.connection.connect(savedBody);
} catch (e) {}
}
},
decompose: function (workspace) {
const containerBlock = workspace.newBlock("functions_args_container");
if (workspace.rendered) containerBlock.initSvg();
let connection = containerBlock.getInput("STACK").connection;
for (let i = 0; i < this.itemCount_; i++) {
const type = this.argTypes_[i] || "label";
const name = this.argNames_[i] || "text";
const itemBlock = workspace.newBlock("functions_args_generic");
itemBlock.setFieldValue(type, "ARG_TYPE");
itemBlock.setFieldValue(name, "ARG_NAME");
if (workspace.rendered) itemBlock.initSvg();
itemBlock.valueConnection_ = null;
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
containerBlock.setFieldValue(this.blockShape_, "SHAPEMENU");
return containerBlock;
},
compose: function (containerBlock) {
const newTypes = [];
const newNames = [];
let itemBlock = containerBlock.getInputTargetBlock("STACK");
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
const type = itemBlock.getFieldValue("ARG_TYPE");
const name = itemBlock.getFieldValue("ARG_NAME");
newTypes.push(type);
newNames.push(name);
}
itemBlock = itemBlock.getNextBlock();
}
const dups = findDuplicateArgNames(newTypes, newNames);
const invalid = [];
for (let i = 0; i < newTypes.length; i++) {
const type = newTypes[i];
const name = newNames[i];
if (type !== "label") {
if (!isValidIdentifier(name)) {
invalid.push(i);
}
}
}
itemBlock = containerBlock.getInputTargetBlock("STACK");
let index = 0;
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
if (dups.includes(index)) {
itemBlock.setWarningText(
"This argument name is already used for this type."
);
} else if (invalid.includes(index)) {
itemBlock.setWarningText("This argument name is not a valid.");
} else {
itemBlock.setWarningText(null);
}
index++;
}
itemBlock = itemBlock.getNextBlock();
}
const newBlockShape =
containerBlock.getFieldValue("SHAPEMENU") || "statement";
if (dups.length > 0 || invalid.length > 0) return;
this.itemCount_ = newTypes.length;
this.argTypes_ = newTypes;
this.argNames_ = newNames;
this.blockShape_ = newBlockShape;
this.updateShape_();
},
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock("STACK");
let i = 0;
while (itemBlock) {
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
const key = this.argTypes_[i] + "_" + this.argNames_[i];
const input = this.getInput(key);
itemBlock.valueConnection_ =
input && input.connection && input.connection.targetConnection;
i++;
}
itemBlock = itemBlock.getNextBlock();
}
},
updateReturnState_: function () {
const body = this.getInputTargetBlock("BODY");
const types = new Set();
function walk(block) {
if (!block) return;
if (block?.childBlocks_?.length > 0) block?.childBlocks_.forEach(walk);
if (block.type === "functions_return") {
const val = block.getInputTargetBlock("VALUE");
const checks = val?.outputConnection?.check;
if (checks !== undefined) {
(Array.isArray(checks) ? checks : [checks]).forEach(t =>
types.add(t)
);
}
}
walk(block.getNextBlock());
}
walk(body);
if (types.size === 0) this.returnTypes_ = [];
else this.returnTypes_ = [...types];
},
};
Blockly.Blocks["functions_args_container"] = {
init: function () {
this.setStyle("procedure_blocks");
this.appendDummyInput().appendField("arguments");
this.appendStatementInput("STACK");
this.appendDummyInput()
.appendField("shape")
.appendField(
new Blockly.FieldDropdown([
[
{
src: "icons/statement.svg",
width: 98 * 0.6,
height: 57 * 0.6,
alt: "A block with top and bottom connections",
},
"statement",
],
[
{
src: "icons/terminal.svg",
width: 98 * 0.6,
height: 48 * 0.6,
alt: "A block with only a top connection",
},
"terminal",
],
]),
"SHAPEMENU"
);
this.contextMenu = false;
},
};
Blockly.Blocks["functions_args_generic"] = {
init() {
this.setStyle("procedure_blocks");
this.appendDummyInput()
.appendField("argument")
.appendField(
new Blockly.FieldDropdown([
["label", "label"],
["string", "string"],
["number", "number"],
["boolean", "boolean"],
["array", "array"],
["object", "object"],
["statement", "statement"],
]),
"ARG_TYPE"
)
.appendField(new Blockly.FieldTextInput("arg"), "ARG_NAME");
this.setPreviousStatement(true);
this.setNextStatement(true);
this.contextMenu = false;
this.valueConnection_ = null;
},
};
Blockly.Blocks["functions_call"] = {
init: function () {
this.setStyle("procedure_blocks");
this.setInputsInline(true);
this.functionId_ = null;
this.blockShape_ = null;
this.argTypes_ = [];
this.argNames_ = [];
this.previousArgTypes_ = [];
this.previousArgNames_ = [];
this.returnTypes_ = [];
this.updateShape_();
},
mutationToDom: function () {
const container = Blockly.utils.xml.createElement("mutation");
container.setAttribute("functionid", this.functionId_);
container.setAttribute("items", this.argTypes_.length);
container.setAttribute("shape", this.blockShape_ || "statement");
container.setAttribute(
"returntypes",
JSON.stringify(this.returnTypes_ || [])
);
for (let i = 0; i < this.argTypes_.length; i++) {
const item = Blockly.utils.xml.createElement("item");
item.setAttribute("type", this.argTypes_[i]);
item.setAttribute("name", this.argNames_[i]);
container.appendChild(item);
}
return container;
},
domToMutation: function (xmlElement) {
this.functionId_ = xmlElement.getAttribute("functionid");
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
this.previousArgTypes_ = [...this.argTypes_];
this.previousArgNames_ = [...this.argNames_];
this.argTypes_ = [];
this.argNames_ = [];
this.returnTypes_;
try {
this.returnTypes_ = JSON.parse(
xmlElement.getAttribute("returntypes") || "[]"
);
} catch {
this.returnTypes_ = [];
}
const items = parseInt(xmlElement.getAttribute("items") || "0", 10);
for (let i = 0; i < items; i++) {
const item = xmlElement.children[i];
this.argTypes_[i] = item.getAttribute("type");
this.argNames_[i] = item.getAttribute("name");
}
this.updateShape_();
},
matchDefinition: function (defBlock) {
this.functionId_ = defBlock.functionId_;
this.previousArgTypes_ = [...this.argTypes_];
this.previousArgNames_ = [...this.argNames_];
this.argTypes_ = [...defBlock.argTypes_];
this.argNames_ = [...defBlock.argNames_];
this.blockShape_ = defBlock.blockShape_;
this.returnTypes_ = [...defBlock.returnTypes_];
this.updateShape_();
if (defBlock.workspace.rendered) this.render();
},
updateShape_: function () {
const oldConnections = {};
[...this.inputList].forEach(input => {
if (input.connection && input.connection.targetBlock()) {
oldConnections[input.name] = input.connection.targetConnection;
}
this.removeInput(input.name);
});
const shape = this.blockShape_ || "statement";
const nextConn = this.nextConnection;
const prevConn = this.previousConnection;
const outputConn = this.outputConnection;
const returnTypes = this.returnTypes_ || [];
if (returnTypes?.length > 0) {
if (prevConn && prevConn.isConnected()) {
const blockAbove = prevConn.targetBlock();
blockAbove.unplug(true);
}
if (nextConn && nextConn.isConnected()) {
const blockBelow = nextConn.targetBlock();
blockBelow.unplug(true);
}
this.setPreviousStatement(false);
this.setNextStatement(false);
this.setOutput(true, returnTypes);
} else {
if (outputConn && outputConn.isConnected()) {
outputConn.disconnect();
}
if (shape === "statement") {
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setOutput(false);
} else if (shape === "terminal") {
if (nextConn && nextConn.isConnected()) {
nextConn.targetBlock().unplug(true);
}
this.setNextStatement(false);
this.setPreviousStatement(true, "default");
this.setOutput(false);
}
}
if (!this.argTypes_ || this.argTypes_.length === 0) {
this.appendDummyInput("EMPTY").appendField("my block");
return;
}
let firstLabel = this.argTypes_[0] === "label";
for (let i = 0; i < this.argTypes_.length; i++) {
const type = this.argTypes_[i];
const name = this.argNames_[i];
if (!type || !name) continue;
if (type === "label") {
this.appendDummyInput().appendField(name);
continue;
}
if (!firstLabel) {
this.appendDummyInput().appendField("my block with");
firstLabel = true;
}
let input;
const key = type + "_" + name;
if (type === "statement") {
input = this.appendStatementInput(key).setCheck("default");
} else {
input = this.appendValueInput(key).setCheck(typeToBlocklyCheck(type));
}
if (oldConnections[key]) {
try {
input.connection.connect(
oldConnections[key].targetBlock()?.outputConnection ||
oldConnections[key]
);
} catch (e) {}
}
}
},
};
Blockly.Blocks["functions_return"] = {
init() {
this.setStyle("procedure_blocks");
this.appendValueInput("VALUE").appendField("return");
this.setPreviousStatement(true, "default");
this.setNextStatement(false);
this.setInputsInline(true);
},
update_() {
const def = this.getSurroundParent();
if (!def || def.type !== "functions_definition") return;
def.updateReturnState_();
def.workspace.updateAllFunctionCalls();
},
onchange(e) {
if (e.isUiEvent || e.isBlank) return;
this.update_();
},
};
BlocklyJS.javascriptGenerator.forBlock["functions_argument_block"] = block => [
block.argType_ + "_" + block.argName_,
BlocklyJS.Order.NONE,
];
BlocklyJS.javascriptGenerator.forBlock["functions_statement_argument_block"] =
block => "statement_" + block.argName_ + "();\n";
BlocklyJS.javascriptGenerator.forBlock["functions_definition"] = function (
block,
generator
) {
const params = block.argTypes_
.map((type, i) => {
if (type === "label") return null;
return type + "_" + block.argNames_[i];
})
.filter(Boolean);
const body = BlocklyJS.javascriptGenerator.statementToCode(block, "BODY");
return `MyFunctions[${generator.quote_(
block.functionId_
)}] = async (${params.join(", ")}) => {\n${body}};\n`;
};
BlocklyJS.javascriptGenerator.forBlock["functions_call"] = function (
block,
generator
) {
const args = [];
for (let i = 0; i < block.argTypes_.length; i++) {
const type = block.argTypes_[i];
const name = block.argNames_[i];
const key = `${type}_${name}`;
if (type === "label") continue;
if (type === "statement")
args.push(`async () => {${generator.statementToCode(block, key)}}`);
else
args.push(
generator.valueToCode(block, key, BlocklyJS.Order.NONE) || "null"
);
}
return `await MyFunctions[${generator.quote_(block.functionId_)}](${args.join(
", "
)});\n`;
};
BlocklyJS.javascriptGenerator.forBlock["functions_return"] = function (
block,
generator
) {
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE);
return `return ${value || "null"};\n`;
};