2016-04-26 09:49:52 -04:00
|
|
|
var html = require('htmlparser2');
|
|
|
|
var memoize = require('memoizee');
|
|
|
|
var parseDOM = memoize(html.parseDOM, {
|
|
|
|
length: 1,
|
|
|
|
resolvers: [String],
|
|
|
|
max: 200
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adapter between block creation events and block representation which can be
|
|
|
|
* used by the Scratch runtime.
|
2016-06-06 14:09:27 -04:00
|
|
|
* @param {Object} e `Blockly.events.create`
|
|
|
|
* @return {Array.<Object>} List of blocks from this CREATE event.
|
2016-04-26 09:49:52 -04:00
|
|
|
*/
|
|
|
|
module.exports = function (e) {
|
|
|
|
// Validate input
|
|
|
|
if (typeof e !== 'object') return;
|
|
|
|
if (typeof e.xml !== 'object') return;
|
|
|
|
|
2016-06-06 14:09:27 -04:00
|
|
|
return domToBlocks(parseDOM(e.xml.outerHTML));
|
2016-04-26 09:54:14 -04:00
|
|
|
};
|
2016-04-26 09:49:52 -04:00
|
|
|
|
|
|
|
/**
|
2016-06-06 14:09:27 -04:00
|
|
|
* Convert outer blocks DOM from a Blockly CREATE event
|
|
|
|
* to a usable form for the Scratch runtime.
|
|
|
|
* This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
|
|
|
|
* @param {Element} blocksDOM DOM tree for this event.
|
|
|
|
* @return {Array.<Object>} Usable list of blocks from this CREATE event.
|
2016-04-26 09:49:52 -04:00
|
|
|
*/
|
2016-06-06 14:09:27 -04:00
|
|
|
function domToBlocks (blocksDOM) {
|
|
|
|
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
|
|
|
var blocks = {};
|
|
|
|
for (var i = 0; i < blocksDOM.length; i++) {
|
|
|
|
var block = blocksDOM[i];
|
2016-06-07 12:01:47 -04:00
|
|
|
if (!block.name || !block.attribs) {
|
|
|
|
continue;
|
|
|
|
}
|
2016-06-06 14:09:27 -04:00
|
|
|
var tagName = block.name.toLowerCase();
|
2016-06-06 14:57:58 -04:00
|
|
|
if (tagName == 'block' || tagName == 'shadow') {
|
2016-06-06 14:44:51 -04:00
|
|
|
domToBlock(block, blocks, true);
|
2016-06-06 14:09:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Flatten blocks object into a list.
|
|
|
|
var blocksList = [];
|
|
|
|
for (var b in blocks) {
|
|
|
|
blocksList.push(blocks[b]);
|
|
|
|
}
|
|
|
|
return blocksList;
|
|
|
|
}
|
2016-04-26 09:49:52 -04:00
|
|
|
|
2016-06-06 14:09:27 -04:00
|
|
|
/**
|
|
|
|
* Convert and an individual block DOM to the representation tree.
|
|
|
|
* Based on Blockly's `domToBlockHeadless_`.
|
|
|
|
* @param {Element} blockDOM DOM tree for an individual block.
|
|
|
|
* @param {Object} blocks Collection of blocks to add to.
|
2016-06-08 16:57:08 -04:00
|
|
|
* @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
|
2016-06-06 14:09:27 -04:00
|
|
|
*/
|
2016-06-06 14:44:51 -04:00
|
|
|
function domToBlock (blockDOM, blocks, isTopBlock) {
|
2016-06-06 14:09:27 -04:00
|
|
|
// Block skeleton.
|
|
|
|
var block = {
|
2016-06-06 14:44:51 -04:00
|
|
|
id: blockDOM.attribs.id, // Block ID
|
|
|
|
opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
|
2016-06-06 14:09:27 -04:00
|
|
|
inputs: {}, // Inputs to this block and the blocks they point to.
|
|
|
|
fields: {}, // Fields on this block and their values.
|
|
|
|
next: null, // Next block in the stack, if one exists.
|
2016-06-06 14:44:51 -04:00
|
|
|
topLevel: isTopBlock // If this block starts a stack.
|
2016-04-26 09:49:52 -04:00
|
|
|
};
|
|
|
|
|
2016-06-06 14:09:27 -04:00
|
|
|
// Add the block to the representation tree.
|
|
|
|
blocks[block.id] = block;
|
2016-04-26 09:49:52 -04:00
|
|
|
|
2016-06-06 14:09:27 -04:00
|
|
|
// Process XML children and find enclosed blocks, fields, etc.
|
|
|
|
for (var i = 0; i < blockDOM.children.length; i++) {
|
|
|
|
var xmlChild = blockDOM.children[i];
|
|
|
|
// Enclosed blocks and shadows
|
|
|
|
var childBlockNode = null;
|
|
|
|
var childShadowNode = null;
|
|
|
|
for (var j = 0; j < xmlChild.children.length; j++) {
|
|
|
|
var grandChildNode = xmlChild.children[j];
|
|
|
|
if (!grandChildNode.name) {
|
|
|
|
// Non-XML tag node.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
var grandChildNodeName = grandChildNode.name.toLowerCase();
|
|
|
|
if (grandChildNodeName == 'block') {
|
|
|
|
childBlockNode = grandChildNode;
|
|
|
|
} else if (grandChildNodeName == 'shadow') {
|
|
|
|
childShadowNode = grandChildNode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use shadow block only if there's no real block node.
|
|
|
|
if (!childBlockNode && childShadowNode) {
|
|
|
|
childBlockNode = childShadowNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not all Blockly-type blocks are handled here,
|
|
|
|
// as we won't be using all of them for Scratch.
|
|
|
|
switch (xmlChild.name.toLowerCase()) {
|
|
|
|
case 'field':
|
|
|
|
// Add the field to this block.
|
|
|
|
var fieldName = xmlChild.attribs.name;
|
|
|
|
block.fields[fieldName] = {
|
|
|
|
name: fieldName,
|
|
|
|
value: xmlChild.children[0].data
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'value':
|
|
|
|
case 'statement':
|
|
|
|
// Recursively generate block structure for input block.
|
2016-06-06 14:44:51 -04:00
|
|
|
domToBlock(childBlockNode, blocks, false);
|
2016-06-06 14:09:27 -04:00
|
|
|
// Link this block's input to the child block.
|
|
|
|
var inputName = xmlChild.attribs.name;
|
|
|
|
block.inputs[inputName] = {
|
|
|
|
name: inputName,
|
|
|
|
block: childBlockNode.attribs.id
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'next':
|
2016-06-07 12:01:47 -04:00
|
|
|
if (!childBlockNode || !childBlockNode.attribs) {
|
|
|
|
// Invalid child block.
|
|
|
|
continue;
|
|
|
|
}
|
2016-06-06 14:09:27 -04:00
|
|
|
// Recursively generate block structure for next block.
|
2016-06-06 14:44:51 -04:00
|
|
|
domToBlock(childBlockNode, blocks, false);
|
2016-06-06 14:09:27 -04:00
|
|
|
// Link next block to this block.
|
|
|
|
block.next = childBlockNode.attribs.id;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2016-04-26 09:49:52 -04:00
|
|
|
}
|