export function patchBlocks(pkgTargetVersion: string, dom: Element) {
// is this a old script?
if (pxt.semver.majorCmp(pkgTargetVersion || "0.0.0", "4.0.20") >= 0) return;
// button and pin pressed/released blocks
/*
Button.A
TouchPin.P0
TouchPin.P1
converts to
Button.B
ButtonEvent.Click
TouchPin.P2
ButtonEvent.Up
*/
const inputNodes = pxt.U.toArray(dom.querySelectorAll("block[type=device_button_event]"))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=device_pin_event]")))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=device_pin_released]")))
inputNodes.forEach(node => {
const nodeType = node.getAttribute("type");
if(nodeType === "device_button_event") {
node.setAttribute("type", "device_button_selected_event");
} else {
node.setAttribute("type", "device_pin_custom_event");
}
// fix lowercase 'name'in device_pin_event
if(nodeType === "device_pin_event") {
node.querySelectorAll("field[name=name]")[0].setAttribute("name", "NAME")
}
const valueNode = node.ownerDocument.createElement("value");
valueNode.setAttribute("name", "eventType")
const shadowNode = node.ownerDocument.createElement("shadow");
shadowNode.setAttribute("type", "control_button_event_value")
const fieldNode = node.ownerDocument.createElement("field");
fieldNode.setAttribute("name", "id")
if(nodeType === "device_button_event") {
fieldNode.textContent = "ButtonEvent.Click";
} else if(nodeType === "device_pin_released") {
fieldNode.textContent = "ButtonEvent.Up";
} else {
fieldNode.textContent = "ButtonEvent.Down";
}
shadowNode.prepend(fieldNode)
valueNode.prepend(shadowNode)
node.prepend(valueNode)
});
// loudness
/*
converts to
*/
const loudnessNodes = pxt.U.toArray(dom.querySelectorAll("block[type=loudness]"))
loudnessNodes.forEach(node => {
node.setAttribute("type", "soundLevel");
});
// rgbw to rgb block
const rgbwNodes = pxt.U.toArray(dom.querySelectorAll("block[type=core_rgbw]"))
rgbwNodes.forEach(node => {
node.setAttribute("type", "core_rgb");
node.querySelectorAll("value[name=white]")[0].remove();
});
// arrow blocks
/*
ArrowNames.North
converts to
IconNames.ArrowNorth
*/
const arrowNodes = pxt.U.toArray(dom.querySelectorAll("block[type=basic_show_arrow]"))
arrowNodes.forEach(node => {
node.setAttribute("type", "basic_show_icon");
const arrowNode = node.querySelectorAll("value[name=i]")[0]
const iconName = "IconNames.Arrow" + arrowNode.querySelectorAll("field[name=arrow]")[0].textContent.split('.')[1];
const iconNode = node.ownerDocument.createElement("field");
iconNode.setAttribute("name", "i")
iconNode.textContent = iconName;
const mutationNode = node.ownerDocument.createElement("mutation");
// mutationNode.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
mutationNode.setAttribute("_expanded", "0")
mutationNode.setAttribute("_input_init", "false")
node.prepend(iconNode)
node.prepend(mutationNode)
node.removeChild(arrowNode);
});
// arrow icons
/*
ArrowNames.East
converts to
IconNames.ArrowEast
*/
const arrowImageNodes = pxt.U.toArray(dom.querySelectorAll("block[type=builtin_arrow_image]"))
arrowImageNodes.forEach(node => {
node.setAttribute("type", "builtin_image");
const arrowNode = node.querySelectorAll("field[name=i]")[0];
arrowNode.textContent = "IconNames.Arrow" + arrowNode.textContent.split('.')[1];
});
// is this a very old script?
if (pxt.semver.majorCmp(pkgTargetVersion || "0.0.0", "1.0.0") >= 0) return;
// LEDs
/**
*
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
TRUE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
TRUE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
FALSE
to
`
# # # # #
. . . . #
. . . . .
. . . . #
. . . . #
`
*/
const nodes = pxt.U.toArray(dom.querySelectorAll("block[type=device_show_leds]"))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=device_build_image]")))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=device_build_image]")))
.concat(pxt.U.toArray(dom.querySelectorAll("block[type=device_build_big_image]")))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=device_build_big_image]")));
nodes.forEach(node => {
// don't rewrite if already upgraded, eg. field LEDS already present
if (pxt.U.toArray(node.children).filter(child => child.tagName == "field" && "LEDS" == child.getAttribute("name"))[0])
return;
// read LEDxx value and assmebly into a new field
const leds: string[][] = [[], [], [], [], []];
pxt.U.toArray(node.children)
.filter(child => child.tagName == "field" && /^LED\d+$/.test(child.getAttribute("name")))
.forEach(lednode => {
let n = lednode.getAttribute("name");
let col = parseInt(n[3]);
let row = parseInt(n[4]);
leds[row][col] = lednode.textContent == "TRUE" ? "#" : ".";
// remove node
node.removeChild(lednode);
});
// add new field
const f = node.ownerDocument.createElement("field");
f.setAttribute("name", "LEDS");
const s = '`\n' + leds.map(row => row.join('')).join('\n') + '\n`';
f.appendChild(node.ownerDocument.createTextNode(s));
node.insertBefore(f, null);
});
// radio
/*
receivedNumber
name
value
receivedString
converts to
receivedNumber
name
value
receivedString
*/
const varids: pxt.Map = {};
function addField(node: Element, renameMap: pxt.Map, name: string) {
const f = node.ownerDocument.createElement("field");
f.setAttribute("name", "HANDLER_" + name)
f.setAttribute("id", varids[renameMap[name] || name]);
f.appendChild(node.ownerDocument.createTextNode(name));
node.appendChild(f);
}
pxt.U.toArray(dom.querySelectorAll("variable")).forEach(node => varids[node.textContent] = node.getAttribute("id"));
pxt.U.toArray(dom.querySelectorAll("block[type=radio_on_packet]"))
.forEach(node => {
const mutation = node.querySelector("mutation");
if (!mutation) return;
const renameMap = JSON.parse(node.getAttribute("renamemap") || "{}");
const props = mutation.getAttribute("callbackproperties");
if (props) {
const parts = props.split(",");
// It's tempting to generate radio_on_number if parts.length === 0 but
// that would create a variable named "receivedNumber" and possibly shadow
// an existing variable in the user's program. It's safer to stick to the
// old block.
if (parts.length === 1) {
if (parts[0] === "receivedNumber") {
node.setAttribute("type", "radio_on_number");
node.removeChild(node.querySelector("field[name=receivedNumber]"));
addField(node, renameMap, "receivedNumber");
}
else if (parts[0] === "receivedString") {
node.setAttribute("type", "radio_on_string");
node.removeChild(node.querySelector("field[name=receivedString]"));
addField(node, renameMap, "receivedString");
}
else {
return;
}
node.removeChild(mutation);
}
else if (parts.length === 2 && parts.indexOf("receivedNumber") !== -1 && parts.indexOf("receivedString") !== -1) {
node.setAttribute("type", "radio_on_value");
node.removeChild(node.querySelector("field[name=receivedNumber]"));
node.removeChild(node.querySelector("field[name=receivedString]"));
addField(node, renameMap, "name");
addField(node, renameMap, "value");
node.removeChild(mutation);
}
}
})
// device_random now refers to randomRange() so we need to add the missing lower bound argument
pxt.U.toArray(dom.querySelectorAll("block[type=device_random]"))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=device_random]")))
.forEach(node => {
if (getValue(node, "min")) return;
const v = node.ownerDocument.createElement("value");
v.setAttribute("name", "min");
addNumberShadow(v);
node.appendChild(v);
});
/*
DIVIDE
0
2
1
3
*/
pxt.U.toArray(dom.querySelectorAll("block[type=math_arithmetic]"))
.concat(pxt.U.toArray(dom.querySelectorAll("shadow[type=math_arithmetic]")))
.forEach(node => {
const op = getField(node, "OP");
if (!op || op.textContent.trim() !== "DIVIDE") return;
// Convert to integer division
/*
idiv
0
0
*/
node.setAttribute("type", "math_js_op");
op.textContent = "idiv";
const mutation = node.ownerDocument.createElement("mutation");
mutation.setAttribute("op-type", "infix");
// mutation has to be first or Blockly will drop the second argument
node.insertBefore(mutation, node.firstChild);
const a = getValue(node, "A");
if (a) a.setAttribute("name", "ARG0");
const b = getValue(node, "B");
if (b) b.setAttribute("name", "ARG1");
});
renameField(dom, "math_number_minmax", "NUM", "SLIDER");
renameField(dom, "device_note", "note", "name");
}
function renameField(dom: Element, blockType: string, oldName: string, newName: string) {
pxt.U.toArray(dom.querySelectorAll(`block[type=${blockType}]`))
.concat(pxt.U.toArray(dom.querySelectorAll(`shadow[type=${blockType}]`)))
.forEach(node => {
const thefield = getField(node, oldName);
if (thefield) {
thefield.setAttribute("name", newName);
}
});
}
function getField(parent: Element, name: string) {
return getFieldOrValue(parent, name, true);
}
function getValue(parent: Element, name: string) {
return getFieldOrValue(parent, name, false);
}
function getFieldOrValue(parent: Element, name: string, isField: boolean) {
const nodeType = isField ? "field" : "value";
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children.item(i);
if (child.tagName === nodeType && child.getAttribute("name") === name) {
return child;
}
}
return undefined;
}
function addNumberShadow(valueNode: Element) {
const s = valueNode.ownerDocument.createElement("shadow");
s.setAttribute("type", "math_number");
const f = valueNode.ownerDocument.createElement("field");
f.setAttribute("name", "NUM");
f.textContent = "0";
s.appendChild(f);
valueNode.appendChild(s);
}