2017-04-17 15:10:04 -04:00
|
|
|
const mutationAdapter = require('./mutation-adapter');
|
|
|
|
const html = require('htmlparser2');
|
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.
|
2017-02-01 15:59:50 -05:00
|
|
|
* @param {object} blocks Collection of blocks to add to.
|
|
|
|
* @param {boolean} isTopBlock Whether blocks at this level are "top blocks."
|
2016-09-08 09:40:53 -04:00
|
|
|
* @param {?string} parent Parent block ID.
|
2016-10-23 17:55:31 -04:00
|
|
|
* @return {undefined}
|
2016-06-06 14:09:27 -04:00
|
|
|
*/
|
2017-04-17 17:15:19 -04:00
|
|
|
const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
|
2016-06-06 14:09:27 -04:00
|
|
|
// Block skeleton.
|
2017-04-17 15:10:04 -04:00
|
|
|
const 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-08-31 11:32:59 -04:00
|
|
|
topLevel: isTopBlock, // If this block starts a stack.
|
2016-09-08 09:40:53 -04:00
|
|
|
parent: parent, // Parent block ID, if available.
|
2016-10-23 17:55:31 -04:00
|
|
|
shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot.
|
2016-08-31 11:32:59 -04:00
|
|
|
x: blockDOM.attribs.x, // X position of script, if top-level.
|
|
|
|
y: blockDOM.attribs.y // Y position of script, if top-level.
|
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.
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let i = 0; i < blockDOM.children.length; i++) {
|
|
|
|
const xmlChild = blockDOM.children[i];
|
2016-06-06 14:09:27 -04:00
|
|
|
// Enclosed blocks and shadows
|
2017-04-17 15:10:04 -04:00
|
|
|
let childBlockNode = null;
|
|
|
|
let childShadowNode = null;
|
|
|
|
for (let j = 0; j < xmlChild.children.length; j++) {
|
|
|
|
const grandChildNode = xmlChild.children[j];
|
2016-06-06 14:09:27 -04:00
|
|
|
if (!grandChildNode.name) {
|
|
|
|
// Non-XML tag node.
|
|
|
|
continue;
|
|
|
|
}
|
2017-04-17 15:10:04 -04:00
|
|
|
const grandChildNodeName = grandChildNode.name.toLowerCase();
|
2016-10-23 17:55:31 -04:00
|
|
|
if (grandChildNodeName === 'block') {
|
2016-06-06 14:09:27 -04:00
|
|
|
childBlockNode = grandChildNode;
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (grandChildNodeName === 'shadow') {
|
2016-06-06 14:09:27 -04:00
|
|
|
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':
|
2017-04-17 17:15:19 -04:00
|
|
|
{
|
|
|
|
// Add the field to this block.
|
|
|
|
const fieldName = xmlChild.attribs.name;
|
|
|
|
let fieldData = '';
|
|
|
|
if (xmlChild.children.length > 0 && xmlChild.children[0].data) {
|
|
|
|
fieldData = xmlChild.children[0].data;
|
|
|
|
} else {
|
|
|
|
// If the child of the field with a data property
|
|
|
|
// doesn't exist, set the data to an empty string.
|
|
|
|
fieldData = '';
|
|
|
|
}
|
|
|
|
block.fields[fieldName] = {
|
|
|
|
name: fieldName,
|
|
|
|
value: fieldData
|
|
|
|
};
|
|
|
|
break;
|
2016-06-14 18:14:04 -04:00
|
|
|
}
|
2016-06-06 14:09:27 -04:00
|
|
|
case 'value':
|
|
|
|
case 'statement':
|
2017-04-17 17:15:19 -04:00
|
|
|
{
|
|
|
|
// Recursively generate block structure for input block.
|
|
|
|
domToBlock(childBlockNode, blocks, false, block.id);
|
|
|
|
if (childShadowNode && childBlockNode !== childShadowNode) {
|
|
|
|
// Also generate the shadow block.
|
|
|
|
domToBlock(childShadowNode, blocks, false, block.id);
|
|
|
|
}
|
|
|
|
// Link this block's input to the child block.
|
|
|
|
const inputName = xmlChild.attribs.name;
|
|
|
|
block.inputs[inputName] = {
|
|
|
|
name: inputName,
|
|
|
|
block: childBlockNode.attribs.id,
|
|
|
|
shadow: childShadowNode ? childShadowNode.attribs.id : null
|
|
|
|
};
|
|
|
|
break;
|
2016-09-06 10:55:52 -04:00
|
|
|
}
|
2016-06-06 14:09:27 -04:00
|
|
|
case 'next':
|
2017-04-17 17:15:19 -04:00
|
|
|
{
|
|
|
|
if (!childBlockNode || !childBlockNode.attribs) {
|
|
|
|
// Invalid child block.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Recursively generate block structure for next block.
|
|
|
|
domToBlock(childBlockNode, blocks, false, block.id);
|
|
|
|
// Link next block to this block.
|
|
|
|
block.next = childBlockNode.attribs.id;
|
|
|
|
break;
|
2016-06-07 12:01:47 -04:00
|
|
|
}
|
2016-10-03 17:43:24 -04:00
|
|
|
case 'mutation':
|
2017-04-17 17:15:19 -04:00
|
|
|
{
|
|
|
|
block.mutation = mutationAdapter(xmlChild);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
const domToBlocks = function (blocksDOM) {
|
|
|
|
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
|
|
|
const blocks = {};
|
|
|
|
for (let i = 0; i < blocksDOM.length; i++) {
|
|
|
|
const block = blocksDOM[i];
|
|
|
|
if (!block.name || !block.attribs) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const tagName = block.name.toLowerCase();
|
|
|
|
if (tagName === 'block' || tagName === 'shadow') {
|
|
|
|
domToBlock(block, blocks, true, null);
|
2016-06-06 14:09:27 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 17:15:19 -04:00
|
|
|
// Flatten blocks object into a list.
|
|
|
|
const blocksList = [];
|
|
|
|
for (const b in blocks) {
|
|
|
|
if (!blocks.hasOwnProperty(b)) continue;
|
|
|
|
blocksList.push(blocks[b]);
|
|
|
|
}
|
|
|
|
return blocksList;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adapter between block creation events and block representation which can be
|
|
|
|
* used by the Scratch runtime.
|
|
|
|
* @param {object} e `Blockly.events.create`
|
|
|
|
* @return {Array.<object>} List of blocks from this CREATE event.
|
|
|
|
*/
|
|
|
|
const adapter = function (e) {
|
|
|
|
// Validate input
|
|
|
|
if (typeof e !== 'object') return;
|
|
|
|
if (typeof e.xml !== 'object') return;
|
|
|
|
|
|
|
|
return domToBlocks(html.parseDOM(e.xml.outerHTML));
|
2016-10-23 17:55:31 -04:00
|
|
|
};
|
2016-10-24 11:49:34 -04:00
|
|
|
|
|
|
|
module.exports = adapter;
|