From b28c50e6a5e8b644ccb8ff6321b6f6cf178f8c76 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:09:27 -0400
Subject: [PATCH 01/27] Rewrite of adapter to support trees of blocks, inputs

---
 src/engine/adapter.js | 170 ++++++++++++++++++++++++++----------------
 src/index.js          |  12 ++-
 2 files changed, 116 insertions(+), 66 deletions(-)

diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 222273c5c..93bc901b3 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -9,10 +9,8 @@ var parseDOM = memoize(html.parseDOM, {
 /**
  * Adapter between block creation events and block representation which can be
  * used by the Scratch runtime.
- *
- * @param {Object} `Blockly.events.create`
- *
- * @return {Object}
+ * @param {Object} e `Blockly.events.create`
+ * @return {Array.<Object>} List of blocks from this CREATE event.
  */
 module.exports = function (e) {
     // Validate input
@@ -20,68 +18,112 @@ module.exports = function (e) {
     if (typeof e.blockId !== 'string') return;
     if (typeof e.xml !== 'object') return;
 
-    // Storage object
-    var obj = {
-        id: e.blockId,
-        opcode: null,
-        next: null,
-        fields: {}
-    };
-
-    // Set opcode
-    if (typeof e.xml.attributes === 'object') {
-        obj.opcode = e.xml.attributes.type.value;
-    }
-
-    // Extract fields from event's `innerHTML`
-    if (typeof e.xml.innerHTML !== 'string') return obj;
-    if (e.xml.innerHTML === '') return obj;
-    obj.fields = extract(parseDOM(e.xml.innerHTML));
-
-    return obj;
+    return domToBlocks(parseDOM(e.xml.outerHTML));
 };
 
 /**
- * Extracts fields from a block's innerHTML.
- * @todo Extend this to support vertical grammar / nested blocks.
- *
- * @param {Object} DOM representation of block's innerHTML
- *
- * @return {Object}
+ * 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.
  */
-function extract (dom) {
-    // Storage object
-    var fields = {};
-
-    // Field
-    var field = dom[0];
-    var fieldName = field.attribs.name;
-    fields[fieldName] = {
-        name: fieldName,
-        value: null,
-        blocks: {}
-    };
-
-    // Shadow block
-    var shadow = field.children[0];
-    var shadowId = shadow.attribs.id;
-    var shadowOpcode = shadow.attribs.type;
-    fields[fieldName].blocks[shadowId] = {
-        id: shadowId,
-        opcode: shadowOpcode,
-        next: null,
-        fields: {}
-    };
-
-    // Primitive
-    var primitive = shadow.children[0];
-    var primitiveName = primitive.attribs.name;
-    var primitiveValue = primitive.children[0].data;
-    fields[fieldName].blocks[shadowId].fields[primitiveName] = {
-        name: primitiveName,
-        value: primitiveValue,
-        blocks: null
-    };
-
-    return fields;
+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];
+        var tagName = block.name.toLowerCase();
+        if (tagName === 'block') {
+            domToBlock(block, blocks, 0);
+        }
+    }
+    // Flatten blocks object into a list.
+    var blocksList = [];
+    for (var b in blocks) {
+        blocksList.push(blocks[b]);
+    }
+    return blocksList;
+}
+
+/**
+ * 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 {Number} treeDepth How far down the tree we have recursed.
+ * @param {Object} blocks Collection of blocks to add to.
+ */
+function domToBlock (blockDOM, blocks, treeDepth) {
+    // Block skeleton.
+    var block = {
+        id: null, // Block ID
+        opcode: null, // Execution opcode, e.g., "event_whengreenflag".
+        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.
+        topLevel: treeDepth == 0 // If this block starts a stack.
+    };
+
+    // Basic properties of the block from XML.
+    block.id = blockDOM.attribs.id;
+    block.opcode = blockDOM.attribs.type;
+
+    // Add the block to the representation tree.
+    blocks[block.id] = block;
+
+    // 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.
+            domToBlock(childBlockNode, blocks, treeDepth + 1);
+            // 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':
+            // Recursively generate block structure for next block.
+            domToBlock(childBlockNode, blocks, treeDepth + 1);
+            // Link next block to this block.
+            block.next = childBlockNode.attribs.id;
+            break;
+        }
+    }
 }
diff --git a/src/index.js b/src/index.js
index d128aab90..5c433f899 100644
--- a/src/index.js
+++ b/src/index.js
@@ -31,7 +31,11 @@ function VirtualMachine () {
         // Blocks
         switch (e.type) {
         case 'create':
-            instance.runtime.createBlock(adapter(e), false);
+            var newBlocks = adapter(e);
+            // A create event can create many blocks. Add them all.
+            for (var i = 0; i < newBlocks.length; i++) {
+                instance.runtime.createBlock(newBlocks[i], false);
+            }
             break;
         case 'change':
             instance.runtime.changeBlock({
@@ -64,7 +68,11 @@ function VirtualMachine () {
     instance.flyoutBlockListener = function (e) {
         switch (e.type) {
         case 'create':
-            instance.runtime.createBlock(adapter(e), true);
+            var newBlocks = adapter(e);
+            // A create event can create many blocks. Add them all.
+            for (var i = 0; i < newBlocks.length; i++) {
+                instance.runtime.createBlock(newBlocks[i], true);
+            }
             break;
         case 'change':
             instance.runtime.changeBlock({

From 0dfc72893cee71fd0a404138102fc184bab238ba Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:12:43 -0400
Subject: [PATCH 02/27] Update Runtime createBlock for new structure; _addStack
 helper.

---
 src/engine/runtime.js | 33 +++++++++++++++++++--------------
 1 file changed, 19 insertions(+), 14 deletions(-)

diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 0507b96b5..28e76fba8 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -91,21 +91,11 @@ Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
     // Create new block
     this.blocks[block.id] = block;
 
-    // Walk each field and add any shadow blocks
-    // @todo Expand this to cover vertical / nested blocks
-    for (var i in block.fields) {
-        var shadows = block.fields[i].blocks;
-        for (var y in shadows) {
-            var shadow = shadows[y];
-            this.blocks[shadow.id] = shadow;
-        }
-    }
-
     // Push block id to stacks array. New blocks are always a stack even if only
     // momentary. If the new block is added to an existing stack this stack will
     // be removed by the `moveBlock` method below.
     if (!opt_isFlyoutBlock) {
-        this.stacks.push(block.id);
+        this._addStack(block.id);
     }
 };
 
@@ -132,8 +122,7 @@ Runtime.prototype.moveBlock = function (e) {
 
     // Block was removed from parent
     if (e.newParent === undefined && e.oldParent !== undefined) {
-        // Add stack
-        _this.stacks.push(e.id);
+        _this.addStack(e.id);
 
         // Update old parent
         if (e.oldField === undefined) {
@@ -360,13 +349,29 @@ Runtime.prototype.start = function () {
 // -----------------------------------------------------------------------------
 // -----------------------------------------------------------------------------
 
+/**
+ * Helper to add a stack to `this.stacks`
+ * @param {?string} id ID of block that starts the stack
+ */
+Runtime.prototype._addStack = function (id) {
+    var i = this.stacks.indexOf(id);
+    if (i > -1) return; // Already in stacks.
+    this.stacks.push(id);
+    // Update `topLevel` property on the top block.
+    this.blocks[id].topLevel = true;
+};
+
 /**
  * Helper to remove a stack from `this.stacks`
  * @param {?string} id ID of block that starts the stack
  */
 Runtime.prototype._deleteStack = function (id) {
     var i = this.stacks.indexOf(id);
-    if (i > -1) this.stacks.splice(i, 1);
+    if (i > -1) {
+        this.stacks.splice(i, 1);
+        // Update `topLevel` property on the top block.
+        this.blocks[id].topLevel = false;
+    }
 };
 
 /**

From 9fef9d7edd11383fc14ac3b8ada863e75812b219 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:44:51 -0400
Subject: [PATCH 03/27] Fix up adapter's domToBlock

---
 src/engine/adapter.js | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 93bc901b3..586d8d5d7 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -35,7 +35,7 @@ function domToBlocks (blocksDOM) {
         var block = blocksDOM[i];
         var tagName = block.name.toLowerCase();
         if (tagName === 'block') {
-            domToBlock(block, blocks, 0);
+            domToBlock(block, blocks, true);
         }
     }
     // Flatten blocks object into a list.
@@ -50,24 +50,20 @@ function domToBlocks (blocksDOM) {
  * 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 {Number} treeDepth How far down the tree we have recursed.
+ * @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
  * @param {Object} blocks Collection of blocks to add to.
  */
-function domToBlock (blockDOM, blocks, treeDepth) {
+function domToBlock (blockDOM, blocks, isTopBlock) {
     // Block skeleton.
     var block = {
-        id: null, // Block ID
-        opcode: null, // Execution opcode, e.g., "event_whengreenflag".
+        id: blockDOM.attribs.id, // Block ID
+        opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
         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.
-        topLevel: treeDepth == 0 // If this block starts a stack.
+        topLevel: isTopBlock // If this block starts a stack.
     };
 
-    // Basic properties of the block from XML.
-    block.id = blockDOM.attribs.id;
-    block.opcode = blockDOM.attribs.type;
-
     // Add the block to the representation tree.
     blocks[block.id] = block;
 
@@ -110,7 +106,7 @@ function domToBlock (blockDOM, blocks, treeDepth) {
         case 'value':
         case 'statement':
             // Recursively generate block structure for input block.
-            domToBlock(childBlockNode, blocks, treeDepth + 1);
+            domToBlock(childBlockNode, blocks, false);
             // Link this block's input to the child block.
             var inputName = xmlChild.attribs.name;
             block.inputs[inputName] = {
@@ -120,7 +116,7 @@ function domToBlock (blockDOM, blocks, treeDepth) {
             break;
         case 'next':
             // Recursively generate block structure for next block.
-            domToBlock(childBlockNode, blocks, treeDepth + 1);
+            domToBlock(childBlockNode, blocks, false);
             // Link next block to this block.
             block.next = childBlockNode.attribs.id;
             break;

From e5b677c7f6197d2ea4c640d7ce2ff5eab16722bd Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:53:14 -0400
Subject: [PATCH 04/27] Fix naming of MOVE event in index.js

---
 src/index.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/index.js b/src/index.js
index 5c433f899..f8e7e1ca4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -49,9 +49,9 @@ function VirtualMachine () {
             instance.runtime.moveBlock({
                 id: e.blockId,
                 oldParent: e.oldParentId,
-                oldField: e.oldInputName,
+                oldInput: e.oldInputName,
                 newParent: e.newParentId,
-                newField: e.newInputName
+                newInput: e.newInputName
             });
             break;
         case 'delete':

From 285c2792ef0e9a2fe19c45cd4c5cad50a88d92ae Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:53:42 -0400
Subject: [PATCH 05/27] Only add to stacks if topLevel is true in CREATE

---
 src/engine/runtime.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 28e76fba8..1c7c370f0 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -91,10 +91,11 @@ Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
     // Create new block
     this.blocks[block.id] = block;
 
-    // Push block id to stacks array. New blocks are always a stack even if only
-    // momentary. If the new block is added to an existing stack this stack will
-    // be removed by the `moveBlock` method below.
-    if (!opt_isFlyoutBlock) {
+    // Push block id to stacks array.
+    // Blocks are added as a top-level stack if they are marked as a topBlock
+    // (if they were top-level XML in the event) and if they are not
+    // flyout blocks.
+    if (!opt_isFlyoutBlock && block.topBlock) {
         this._addStack(block.id);
     }
 };

From 18e4228399f44f4f1c6de1c68a9ac266a9d38f9d Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:53:59 -0400
Subject: [PATCH 06/27] Update runtime's handling of delete for new blocks
 structure

---
 src/engine/runtime.js | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 1c7c370f0..55a13d33a 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -168,14 +168,11 @@ Runtime.prototype.deleteBlock = function (e) {
         this.deleteBlock({id: block.next});
     }
 
-    // Delete substacks and fields
-    for (var field in block.fields) {
-        if (field === 'SUBSTACK') {
-            this.deleteBlock({id: block.fields[field].value});
-        } else {
-            for (var shadow in block.fields[field].blocks) {
-                this.deleteBlock({id: shadow});
-            }
+    // Delete inputs (including substacks)
+    for (var input in block.inputs) {
+        // If it's null, the block in this input moved away.
+        if (block.inputs[input].block !== null) {
+            this.deleteBlock({id: block.inputs[input].block});
         }
     }
 

From 214daa8087d50e1d604fb8357c4eb8fe9e275423 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:54:32 -0400
Subject: [PATCH 07/27] Always unset `topLevel` in deleteStack_

Blocks may have topLevel set even if they are not present in this.stacks
---
 src/engine/runtime.js | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 55a13d33a..356adcba8 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -365,11 +365,9 @@ Runtime.prototype._addStack = function (id) {
  */
 Runtime.prototype._deleteStack = function (id) {
     var i = this.stacks.indexOf(id);
-    if (i > -1) {
-        this.stacks.splice(i, 1);
-        // Update `topLevel` property on the top block.
-        this.blocks[id].topLevel = false;
-    }
+    if (i > -1) this.stacks.splice(i, 1);
+    // Update `topLevel` property on the top block.
+    if (this.blocks[id]) this.blocks[id].topLevel = false;
 };
 
 /**

From aa152fd604bfbafb3308daafeebc98e0d0d6402a Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:54:45 -0400
Subject: [PATCH 08/27] Rewrite of moveBlock to better reflect Blockly

---
 src/engine/runtime.js | 49 ++++++++++++++++++++++---------------------
 1 file changed, 25 insertions(+), 24 deletions(-)

diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 356adcba8..17013576c 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -121,34 +121,35 @@ Runtime.prototype.changeBlock = function (args) {
 Runtime.prototype.moveBlock = function (e) {
     var _this = this;
 
-    // Block was removed from parent
-    if (e.newParent === undefined && e.oldParent !== undefined) {
-        _this.addStack(e.id);
-
-        // Update old parent
-        if (e.oldField === undefined) {
-            _this.blocks[e.oldParent].next = null;
-        } else {
-            delete _this.blocks[e.oldParent].fields[e.oldField];
+    // Remove from any old parent.
+    if (e.oldParent !== undefined) {
+        var oldParent = _this.blocks[e.oldParent];
+        if (e.oldInput !== undefined &&
+            oldParent.inputs[e.oldInput].block === e.id) {
+            // This block was connected to the old parent's input.
+            oldParent.inputs[e.oldInput].block = null;
+        } else if (oldParent.next === e.id) {
+            // This block was connected to the old parent's next connection.
+            oldParent.next = null;
         }
-    } else if (e.newParent !== undefined) {
-        // Block was moved to a new parent
-        // Either happens because it was previously parentless
-        // (e.oldParent === undefined)
-        // or because a block was moved in front of it.
+    }
 
-        // Remove stack
+    // Has the block become a top-level block?
+    if (e.newParent === undefined) {
+        _this._addStack(e.id);
+    } else {
+        // Remove stack, if one exists.
         _this._deleteStack(e.id);
-
-        // Update new parent
-        if (e.newField === undefined) {
-            _this.blocks[e.newParent].next = e.id;
-        } else {
-            _this.blocks[e.newParent].fields[e.newField] = {
-                name: e.newField,
-                value: e.id,
-                blocks: {}
+        // Otherwise, try to connect it in its new place.
+        if (e.newInput !== undefined) {
+            // Moved to the new parent's input.
+            _this.blocks[e.newParent].inputs[e.newInput] = {
+                name: e.newInput,
+                block: e.id
             };
+        } else {
+            // Moved to the new parent's next connection.
+            _this.blocks[e.newParent].next = e.id;
         }
     }
 };

From 25c3a38c16cf5f228ca450e1dae11e87baa64b62 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 14:57:58 -0400
Subject: [PATCH 09/27] Allow top-level creation of shadow blocks in adapter

---
 src/engine/adapter.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 586d8d5d7..2a7e9d2fc 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -34,7 +34,7 @@ function domToBlocks (blocksDOM) {
     for (var i = 0; i < blocksDOM.length; i++) {
         var block = blocksDOM[i];
         var tagName = block.name.toLowerCase();
-        if (tagName === 'block') {
+        if (tagName == 'block' || tagName == 'shadow') {
             domToBlock(block, blocks, true);
         }
     }

From 9be33089a8b2f8396403c04c8c786b82c07bd119 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:29:34 -0400
Subject: [PATCH 10/27] Refactor block management into a separate module.

---
 src/engine/blocks.js    | 199 ++++++++++++++++++++++++++++++++++++++++
 src/engine/runtime.js   | 190 +++-----------------------------------
 src/engine/sequencer.js |  19 ++--
 src/index.js            |  12 ++-
 4 files changed, 231 insertions(+), 189 deletions(-)
 create mode 100644 src/engine/blocks.js

diff --git a/src/engine/blocks.js b/src/engine/blocks.js
new file mode 100644
index 000000000..26351c1b1
--- /dev/null
+++ b/src/engine/blocks.js
@@ -0,0 +1,199 @@
+/**
+* @fileoverview
+ * Store and mutate the VM block representation,
+ * and handle updates from Scratch Blocks events.
+ */
+
+ function Blocks () {
+     /**
+      * All blocks in the workspace.
+      * Keys are block IDs, values are metadata about the block.
+      * @type {Object.<string, Object>}
+      */
+     this._blocks = {};
+
+     /**
+      * All stacks in the workspace.
+      * A list of block IDs that represent stacks (first block in stack).
+      * @type {Array.<String>}
+      */
+     this._stacks = [];
+ }
+
+/**
+ * Provide an object with metadata for the requested block ID.
+ * @param {!string} blockId ID of block we have stored.
+ * @return {?Object} Metadata about the block, if it exists.
+ */
+ Blocks.prototype.getBlock = function (blockId) {
+     return this._blocks[blockId];
+ };
+
+/**
+ * Get all known top-level blocks that start stacks.
+ * @return {Array.<string>} List of block IDs.
+ */
+ Blocks.prototype.getStacks = function () {
+     return this._stacks;
+ };
+
+ /**
+  * Get the next block for a particular block
+  * @param {?string} id ID of block to get the next block for
+  * @return {?string} ID of next block in the sequence
+  */
+ Blocks.prototype.getNextBlock = function (id) {
+     if (typeof this._blocks[id] === 'undefined') return null;
+     return this._blocks[id].next;
+ };
+
+ /**
+  * Get the substack for a particular C-shaped block
+  * @param {?string} id ID for block to get the substack for
+  * @return {?string} ID of block in the substack
+  */
+ Blocks.prototype.getSubstack = function (id) {
+     var block = this._blocks[id];
+     if (typeof block === 'undefined') return null;
+     // Empty C-block?
+     if (!('SUBSTACK' in block.inputs)) return null;
+     return block.inputs['SUBSTACK'].block;
+ };
+
+ /**
+  * Get the opcode for a particular block
+  * @param {?string} id ID of block to query
+  * @return {?string} the opcode corresponding to that block
+  */
+ Blocks.prototype.getOpcode = function (id) {
+     if (typeof this._blocks[id] === 'undefined') return null;
+     return this._blocks[id].opcode;
+ };
+
+ // ---------------------------------------------------------------------
+
+ /**
+  * Block management: create blocks and stacks from a `create` event
+  * @param {!Object} block Blockly create event to be processed
+  */
+ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
+     // Create new block
+     this._blocks[block.id] = block;
+
+     // Push block id to stacks array.
+     // Blocks are added as a top-level stack if they are marked as a topBlock
+     // (if they were top-level XML in the event) and if they are not
+     // flyout blocks.
+     if (!opt_isFlyoutBlock && block.topBlock) {
+         this._addStack(block.id);
+     }
+ };
+
+ /**
+  * Block management: change block field values
+  * @param {!Object} args Blockly change event to be processed
+  */
+ Blocks.prototype.changeBlock = function (args) {
+     // Validate
+     if (args.element !== 'field') return;
+     if (typeof this._blocks[args.id] === 'undefined') return;
+     if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
+
+     // Update block value
+     this._blocks[args.id].fields[args.name].value = args.value;
+ };
+
+ /**
+  * Block management: move blocks from parent to parent
+  * @param {!Object} e Blockly move event to be processed
+  */
+ Blocks.prototype.moveBlock = function (e) {
+     // Remove from any old parent.
+     if (e.oldParent !== undefined) {
+         var oldParent = this._blocks[e.oldParent];
+         if (e.oldInput !== undefined &&
+             oldParent.inputs[e.oldInput].block === e.id) {
+             // This block was connected to the old parent's input.
+             oldParent.inputs[e.oldInput].block = null;
+         } else if (oldParent.next === e.id) {
+             // This block was connected to the old parent's next connection.
+             oldParent.next = null;
+         }
+     }
+
+     // Has the block become a top-level block?
+     if (e.newParent === undefined) {
+         this._addStack(e.id);
+     } else {
+         // Remove stack, if one exists.
+         this._deleteStack(e.id);
+         // Otherwise, try to connect it in its new place.
+         if (e.newInput !== undefined) {
+             // Moved to the new parent's input.
+             this._blocks[e.newParent].inputs[e.newInput] = {
+                 name: e.newInput,
+                 block: e.id
+             };
+         } else {
+             // Moved to the new parent's next connection.
+             this._blocks[e.newParent].next = e.id;
+         }
+     }
+ };
+
+ /**
+  * Block management: delete blocks and their associated stacks
+  * @param {!Object} e Blockly delete event to be processed
+  */
+ Blocks.prototype.deleteBlock = function (e) {
+     // @todo In runtime, stop threads running on this stack
+
+     // Get block
+     var block = this._blocks[e.id];
+
+     // Delete children
+     if (block.next !== null) {
+         this.deleteBlock({id: block.next});
+     }
+
+     // Delete inputs (including substacks)
+     for (var input in block.inputs) {
+         // If it's null, the block in this input moved away.
+         if (block.inputs[input].block !== null) {
+             this.deleteBlock({id: block.inputs[input].block});
+         }
+     }
+
+     // Delete stack
+     this._deleteStack(e.id);
+
+     // Delete block
+     delete this._blocks[e.id];
+ };
+
+ // ---------------------------------------------------------------------
+
+ /**
+  * Helper to add a stack to `this._stacks`
+  * @param {?string} id ID of block that starts the stack
+  */
+ Blocks.prototype._addStack = function (id) {
+     var i = this._stacks.indexOf(id);
+     if (i > -1) return; // Already in stacks.
+     this._stacks.push(id);
+     // Update `topLevel` property on the top block.
+     this._blocks[id].topLevel = true;
+ };
+
+ /**
+  * Helper to remove a stack from `this._stacks`
+  * @param {?string} id ID of block that starts the stack
+  */
+ Blocks.prototype._deleteStack = function (id) {
+     var i = this._stacks.indexOf(id);
+     if (i > -1) this._stacks.splice(i, 1);
+     // Update `topLevel` property on the top block.
+     if (this._blocks[id]) this._blocks[id].topLevel = false;
+ };
+
+ module.exports = Blocks;
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 17013576c..9531eadc7 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -10,25 +10,18 @@ var defaultBlockPackages = {
 
 /**
  * Manages blocks, stacks, and the sequencer.
+ * @param blocks Blocks instance for this runtime.
  */
-function Runtime () {
+function Runtime (blocks) {
     // Bind event emitter
     EventEmitter.call(this);
 
     // State for the runtime
-    /**
-     * All blocks in the workspace.
-     * Keys are block IDs, values are metadata about the block.
-     * @type {Object.<string, Object>}
-     */
-    this.blocks = {};
 
     /**
-     * All stacks in the workspace.
-     * A list of block IDs that represent stacks (first block in stack).
-     * @type {Array.<String>}
+     * Block management and storage
      */
-    this.stacks = [];
+    this.blocks = blocks;
 
     /**
      * A list of threads that are currently running in the VM.
@@ -83,106 +76,6 @@ util.inherits(Runtime, EventEmitter);
  */
 Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
 
-/**
- * Block management: create blocks and stacks from a `create` event
- * @param {!Object} block Blockly create event to be processed
- */
-Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
-    // Create new block
-    this.blocks[block.id] = block;
-
-    // Push block id to stacks array.
-    // Blocks are added as a top-level stack if they are marked as a topBlock
-    // (if they were top-level XML in the event) and if they are not
-    // flyout blocks.
-    if (!opt_isFlyoutBlock && block.topBlock) {
-        this._addStack(block.id);
-    }
-};
-
-/**
- * Block management: change block field values
- * @param {!Object} args Blockly change event to be processed
- */
-Runtime.prototype.changeBlock = function (args) {
-    // Validate
-    if (args.element !== 'field') return;
-    if (typeof this.blocks[args.id] === 'undefined') return;
-    if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
-
-    // Update block value
-    this.blocks[args.id].fields[args.name].value = args.value;
-};
-
-/**
- * Block management: move blocks from parent to parent
- * @param {!Object} e Blockly move event to be processed
- */
-Runtime.prototype.moveBlock = function (e) {
-    var _this = this;
-
-    // Remove from any old parent.
-    if (e.oldParent !== undefined) {
-        var oldParent = _this.blocks[e.oldParent];
-        if (e.oldInput !== undefined &&
-            oldParent.inputs[e.oldInput].block === e.id) {
-            // This block was connected to the old parent's input.
-            oldParent.inputs[e.oldInput].block = null;
-        } else if (oldParent.next === e.id) {
-            // This block was connected to the old parent's next connection.
-            oldParent.next = null;
-        }
-    }
-
-    // Has the block become a top-level block?
-    if (e.newParent === undefined) {
-        _this._addStack(e.id);
-    } else {
-        // Remove stack, if one exists.
-        _this._deleteStack(e.id);
-        // Otherwise, try to connect it in its new place.
-        if (e.newInput !== undefined) {
-            // Moved to the new parent's input.
-            _this.blocks[e.newParent].inputs[e.newInput] = {
-                name: e.newInput,
-                block: e.id
-            };
-        } else {
-            // Moved to the new parent's next connection.
-            _this.blocks[e.newParent].next = e.id;
-        }
-    }
-};
-
-/**
- * Block management: delete blocks and their associated stacks
- * @param {!Object} e Blockly delete event to be processed
- */
-Runtime.prototype.deleteBlock = function (e) {
-    // @todo Stop threads running on this stack
-
-    // Get block
-    var block = this.blocks[e.id];
-
-    // Delete children
-    if (block.next !== null) {
-        this.deleteBlock({id: block.next});
-    }
-
-    // Delete inputs (including substacks)
-    for (var input in block.inputs) {
-        // If it's null, the block in this input moved away.
-        if (block.inputs[input].block !== null) {
-            this.deleteBlock({id: block.inputs[input].block});
-        }
-    }
-
-    // Delete stack
-    this._deleteStack(e.id);
-
-    // Delete block
-    delete this.blocks[e.id];
-};
 
 // -----------------------------------------------------------------------------
 // -----------------------------------------------------------------------------
@@ -268,10 +161,11 @@ Runtime.prototype.greenFlag = function () {
         this._removeThread(this.threads[i]);
     }
     // Add all top stacks with green flag
-    for (var j = 0; j < this.stacks.length; j++) {
-        var topBlock = this.stacks[j];
-        if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
-            this._pushThread(this.stacks[j]);
+    var stacks = this.blocks.getStacks();
+    for (var j = 0; j < stacks.length; j++) {
+        var topBlock = stacks[j];
+        if (this.blocks.getBlock(topBlock).opcode === 'event_whenflagclicked') {
+            this._pushThread(stacks[j]);
         }
     }
 };
@@ -281,9 +175,11 @@ Runtime.prototype.greenFlag = function () {
  */
 Runtime.prototype.startDistanceSensors = function () {
     // Add all top stacks with distance sensor
-    for (var j = 0; j < this.stacks.length; j++) {
-        var topBlock = this.stacks[j];
-        if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') {
+    var stacks = this.blocks.getStacks();
+    for (var j = 0; j < stacks.length; j++) {
+        var topBlock = stacks[j];
+        if (this.blocks.getBlock(topBlock).opcode ===
+            'wedo_whendistanceclose') {
             var alreadyRunning = false;
             for (var k = 0; k < this.threads.length; k++) {
                 if (this.threads[k].topBlock === topBlock) {
@@ -291,7 +187,7 @@ Runtime.prototype.startDistanceSensors = function () {
                 }
             }
             if (!alreadyRunning) {
-                this._pushThread(this.stacks[j]);
+                this._pushThread(stacks[j]);
             }
         }
     }
@@ -345,60 +241,4 @@ Runtime.prototype.start = function () {
     }.bind(this), Runtime.THREAD_STEP_INTERVAL);
 };
 
-// -----------------------------------------------------------------------------
-// -----------------------------------------------------------------------------
-
-/**
- * Helper to add a stack to `this.stacks`
- * @param {?string} id ID of block that starts the stack
- */
-Runtime.prototype._addStack = function (id) {
-    var i = this.stacks.indexOf(id);
-    if (i > -1) return; // Already in stacks.
-    this.stacks.push(id);
-    // Update `topLevel` property on the top block.
-    this.blocks[id].topLevel = true;
-};
-
-/**
- * Helper to remove a stack from `this.stacks`
- * @param {?string} id ID of block that starts the stack
- */
-Runtime.prototype._deleteStack = function (id) {
-    var i = this.stacks.indexOf(id);
-    if (i > -1) this.stacks.splice(i, 1);
-    // Update `topLevel` property on the top block.
-    if (this.blocks[id]) this.blocks[id].topLevel = false;
-};
-
-/**
- * Helper to get the next block for a particular block
- * @param {?string} id ID of block to get the next block for
- * @return {?string} ID of next block in the sequence
- */
-Runtime.prototype._getNextBlock = function (id) {
-    if (typeof this.blocks[id] === 'undefined') return null;
-    return this.blocks[id].next;
-};
-
-/**
- * Helper to get the substack for a particular C-shaped block
- * @param {?string} id ID for block to get the substack for
- * @return {?string} ID of block in the substack
- */
-Runtime.prototype._getSubstack = function (id) {
-    if (typeof this.blocks[id] === 'undefined') return null;
-    return this.blocks[id].fields['SUBSTACK'];
-};
-
-/**
- * Helper to get the opcode for a particular block
- * @param {?string} id ID of block to query
- * @return {?string} the opcode corresponding to that block
- */
-Runtime.prototype._getOpcode = function (id) {
-    if (typeof this.blocks[id] === 'undefined') return null;
-    return this.blocks[id].opcode;
-};
-
 module.exports = Runtime;
diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js
index f495264c8..95a28f7bf 100644
--- a/src/engine/sequencer.js
+++ b/src/engine/sequencer.js
@@ -97,13 +97,13 @@ Sequencer.prototype.stepThread = function (thread) {
     // If the primitive would like to do control flow,
     // it can overwrite nextBlock.
     var currentBlock = thread.nextBlock;
-    if (!currentBlock || !this.runtime.blocks[currentBlock]) {
+    if (!currentBlock || !this.runtime.blocks.getBlock(currentBlock)) {
         thread.status = Thread.STATUS_DONE;
         return;
     }
-    thread.nextBlock = this.runtime._getNextBlock(currentBlock);
+    thread.nextBlock = this.runtime.blocks.getNextBlock(currentBlock);
 
-    var opcode = this.runtime._getOpcode(currentBlock);
+    var opcode = this.runtime.blocks.getOpcode(currentBlock);
 
     // Push the current block to the stack
     thread.stack.push(currentBlock);
@@ -130,7 +130,7 @@ Sequencer.prototype.stepThread = function (thread) {
     var threadDoneCallback = function () {
         thread.status = Thread.STATUS_DONE;
         // Refresh nextBlock in case it has changed during a yield.
-        thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
+        thread.nextBlock = instance.runtime.blocks.getNextBlock(currentBlock);
         // Pop the stack and stack frame
         thread.stack.pop();
         thread.stackFrames.pop();
@@ -141,9 +141,10 @@ Sequencer.prototype.stepThread = function (thread) {
      * @todo very hacked...
      */
     var startHats = function(callback) {
-        for (var i = 0; i < instance.runtime.stacks.length; i++) {
-            var stack = instance.runtime.stacks[i];
-            var stackBlock = instance.runtime.blocks[stack];
+        var stacks = instance.runtime.blocks.getStacks();
+        for (var i = 0; i < stacks.length; i++) {
+            var stack = stacks[i];
+            var stackBlock = instance.runtime.blocks.getBlock(stack);
             var result = callback(stackBlock);
             if (result) {
                 // Check if the stack is already running
@@ -174,7 +175,7 @@ Sequencer.prototype.stepThread = function (thread) {
      */
     var threadStartSubstack = function () {
         // Set nextBlock to the start of the substack
-        var substack = instance.runtime._getSubstack(currentBlock);
+        var substack = instance.runtime.blocks.getSubstack(currentBlock);
         if (substack && substack.value) {
             thread.nextBlock = substack.value;
         } else {
@@ -185,7 +186,7 @@ Sequencer.prototype.stepThread = function (thread) {
 
     // @todo extreme hack to get the single argument value for prototype
     var argValues = [];
-    var blockInputs = this.runtime.blocks[currentBlock].fields;
+    var blockInputs = this.runtime.blocks.getBlock(currentBlock).fields;
     for (var bi in blockInputs) {
         var outer = blockInputs[bi];
         for (var b in outer.blocks) {
diff --git a/src/index.js b/src/index.js
index f8e7e1ca4..ce147657c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,7 @@
 var EventEmitter = require('events');
 var util = require('util');
 
+var Blocks = require('./engine/Blocks');
 var Runtime = require('./engine/runtime');
 var adapter = require('./engine/adapter');
 
@@ -15,7 +16,8 @@ function VirtualMachine () {
     // Bind event emitter and runtime to VM instance
     // @todo Post message (Web Worker) polyfill
     EventEmitter.call(instance);
-    instance.runtime = new Runtime();
+    instance.blocks = new Blocks();
+    instance.runtime = new Runtime(instance.blocks);
 
     /**
      * Event listener for blocks. Handles validation and serves as a generic
@@ -34,11 +36,11 @@ function VirtualMachine () {
             var newBlocks = adapter(e);
             // A create event can create many blocks. Add them all.
             for (var i = 0; i < newBlocks.length; i++) {
-                instance.runtime.createBlock(newBlocks[i], false);
+                instance.blocks.createBlock(newBlocks[i], false);
             }
             break;
         case 'change':
-            instance.runtime.changeBlock({
+            instance.blocks.changeBlock({
                 id: e.blockId,
                 element: e.element,
                 name: e.name,
@@ -46,7 +48,7 @@ function VirtualMachine () {
             });
             break;
         case 'move':
-            instance.runtime.moveBlock({
+            instance.blocks.moveBlock({
                 id: e.blockId,
                 oldParent: e.oldParentId,
                 oldInput: e.oldInputName,
@@ -55,7 +57,7 @@ function VirtualMachine () {
             });
             break;
         case 'delete':
-            instance.runtime.deleteBlock({
+            instance.blocks.deleteBlock({
                 id: e.blockId
             });
             break;

From dbb31462addae59796fe68d8a68dff1bfab10d37 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:31:14 -0400
Subject: [PATCH 11/27] Extend getSubstack to support multiple substacks

---
 src/engine/blocks.js | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index 26351c1b1..f7f6ddf9c 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -50,14 +50,21 @@
  /**
   * Get the substack for a particular C-shaped block
   * @param {?string} id ID for block to get the substack for
+  * @param {?number} substackNum Which substack to select (e.g. for if-else)
   * @return {?string} ID of block in the substack
   */
- Blocks.prototype.getSubstack = function (id) {
+ Blocks.prototype.getSubstack = function (id, substackNum) {
      var block = this._blocks[id];
      if (typeof block === 'undefined') return null;
+
+     var inputName = 'SUBSTACK';
+     if (substackNum > 0) {
+         inputName += substackNum;
+     }
+
      // Empty C-block?
-     if (!('SUBSTACK' in block.inputs)) return null;
-     return block.inputs['SUBSTACK'].block;
+     if (!(inputName in block.inputs)) return null;
+     return block.inputs[inputName].block;
  };
 
  /**

From 0efe7e2b277e7b4309fd53ad878fbec0c7ffdf17 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:44:13 -0400
Subject: [PATCH 12/27] Fix block events for flyout blocks

---
 src/index.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/index.js b/src/index.js
index ce147657c..a18d17da2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -73,11 +73,11 @@ function VirtualMachine () {
             var newBlocks = adapter(e);
             // A create event can create many blocks. Add them all.
             for (var i = 0; i < newBlocks.length; i++) {
-                instance.runtime.createBlock(newBlocks[i], true);
+                instance.blocks.createBlock(newBlocks[i], true);
             }
             break;
         case 'change':
-            instance.runtime.changeBlock({
+            instance.blocks.changeBlock({
                 id: e.blockId,
                 element: e.element,
                 name: e.name,
@@ -85,7 +85,7 @@ function VirtualMachine () {
             });
             break;
         case 'delete':
-            instance.runtime.deleteBlock({
+            instance.blocks.deleteBlock({
                 id: e.blockId
             });
             break;

From f63e26189278782b8ffe48fa79f90fb1d318b83d Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:58:40 -0400
Subject: [PATCH 13/27] Remove breaking check for blockId

---
 src/engine/adapter.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 2a7e9d2fc..7f891da34 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -15,7 +15,6 @@ var parseDOM = memoize(html.parseDOM, {
 module.exports = function (e) {
     // Validate input
     if (typeof e !== 'object') return;
-    if (typeof e.blockId !== 'string') return;
     if (typeof e.xml !== 'object') return;
 
     return domToBlocks(parseDOM(e.xml.outerHTML));

From fd95d3045d19778c1613b47d21bdffec2cd97c1e Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:58:54 -0400
Subject: [PATCH 14/27] Fix property check of `topLevel` in `createBlock`

---
 src/engine/blocks.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index f7f6ddf9c..ad4c8d1c7 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -88,10 +88,10 @@
      this._blocks[block.id] = block;
 
      // Push block id to stacks array.
-     // Blocks are added as a top-level stack if they are marked as a topBlock
+     // Blocks are added as a top-level stack if they are marked as a top-block
      // (if they were top-level XML in the event) and if they are not
      // flyout blocks.
-     if (!opt_isFlyoutBlock && block.topBlock) {
+     if (!opt_isFlyoutBlock && block.topLevel) {
          this._addStack(block.id);
      }
  };

From feea7a056634a80473903b1ce5a46358615a406e Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:59:07 -0400
Subject: [PATCH 15/27] Update CREATE event fixture to have outerHTML

---
 test/fixtures/events.json | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index fca8693ff..064c76c69 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -1,16 +1,11 @@
 {
     "create": {
-        "blockId": "z!+#Nqr,_(V=xz0y7a@d",
         "workspaceId": "7Luws3lyb*Z98~Kk+IG|",
         "group": ";OswyM#@%`%,xOrhOXC=",
         "recordUndo": true,
+        "name": "block",
         "xml": {
-            "attributes": {
-                "type": {
-                    "value": "wedo_motorclockwise"
-                }
-            },
-            "innerHTML": "<value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value>"
+            "outerHTML": "<block type=\"wedo_motorclockwise\" id=\"z!+#Nqr,_(V=xz0y7a@d\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value></block>"
         },
         "ids": [
             "z!+#Nqr,_(V=xz0y7a@d",

From 773240910219034b0c73351d8f939316b4d61dcd Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 15:59:21 -0400
Subject: [PATCH 16/27] Update unit tests for new adapter, blocks module

---
 test/unit/adapter.js | 26 +++++++++---
 test/unit/blocks.js  | 97 ++++++++++++++++++++++++++++++++++++++++++++
 test/unit/runtime.js | 77 -----------------------------------
 3 files changed, 118 insertions(+), 82 deletions(-)
 create mode 100644 test/unit/blocks.js

diff --git a/test/unit/adapter.js b/test/unit/adapter.js
index 547e0b9b0..31e904845 100644
--- a/test/unit/adapter.js
+++ b/test/unit/adapter.js
@@ -10,11 +10,27 @@ test('spec', function (t) {
 test('create event', function (t) {
     var result = adapter(events.create);
 
-    t.type(result, 'object');
-    t.type(result.id, 'string');
-    t.type(result.opcode, 'string');
-    t.type(result.fields, 'object');
-    t.type(result.fields['DURATION'], 'object');
+    t.ok(Array.isArray(result));
+    t.equal(result.length, 2);
+
+    // Outer block
+    t.type(result[0].id, 'string');
+    t.type(result[0].opcode, 'string');
+    t.type(result[0].fields, 'object');
+    t.type(result[0].inputs, 'object');
+    t.type(result[0].inputs['DURATION'], 'object');
+    t.type(result[0].topLevel, 'boolean');
+    t.equal(result[0].topLevel, true);
+
+    // Enclosed shadow block
+    t.type(result[1].id, 'string');
+    t.type(result[1].opcode, 'string');
+    t.type(result[1].fields, 'object');
+    t.type(result[1].inputs, 'object');
+    t.type(result[1].fields['NUM'], 'object');
+    t.type(result[1].fields['NUM'].value, '10');
+    t.type(result[1].topLevel, 'boolean');
+    t.equal(result[1].topLevel, false);
 
     t.end();
 });
diff --git a/test/unit/blocks.js b/test/unit/blocks.js
new file mode 100644
index 000000000..0e1bcabcc
--- /dev/null
+++ b/test/unit/blocks.js
@@ -0,0 +1,97 @@
+var test = require('tap').test;
+var Blocks = require('../../src/engine/Blocks');
+
+test('spec', function (t) {
+    var b = new Blocks();
+
+    t.type(Blocks, 'function');
+    t.type(b, 'object');
+    t.ok(b instanceof Blocks);
+
+    t.type(b._blocks, 'object');
+    t.type(b._stacks, 'object');
+    t.ok(Array.isArray(b._stacks));
+
+    t.type(b.createBlock, 'function');
+    t.type(b.moveBlock, 'function');
+    t.type(b.changeBlock, 'function');
+    t.type(b.deleteBlock, 'function');
+
+    t.end();
+});
+
+test('create', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+
+    t.type(b._blocks['foo'], 'object');
+    t.equal(b._blocks['foo'].opcode, 'TEST_BLOCK');
+    t.notEqual(b._stacks.indexOf('foo'), -1);
+    t.end();
+});
+
+test('move', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'bar',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+
+    // Attach 'bar' to the end of 'foo'
+    b.moveBlock({
+        id: 'bar',
+        newParent: 'foo'
+    });
+    t.equal(b._stacks.length, 1);
+    t.equal(Object.keys(b._blocks).length, 2);
+    t.equal(b._blocks['foo'].next, 'bar');
+
+    // Detach 'bar' from 'foo'
+    b.moveBlock({
+        id: 'bar',
+        oldParent: 'foo'
+    });
+    t.equal(b._stacks.length, 2);
+    t.equal(Object.keys(b._blocks).length, 2);
+    t.equal(b._blocks['foo'].next, null);
+
+    t.end();
+});
+
+test('delete', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    b.deleteBlock({
+        id: 'foo'
+    });
+
+    t.type(b._blocks['foo'], 'undefined');
+    t.equal(b._stacks.indexOf('foo'), -1);
+    t.end();
+});
diff --git a/test/unit/runtime.js b/test/unit/runtime.js
index 0e41ed5be..40757c054 100644
--- a/test/unit/runtime.js
+++ b/test/unit/runtime.js
@@ -8,82 +8,5 @@ test('spec', function (t) {
     t.type(r, 'object');
     t.ok(r instanceof Runtime);
 
-    t.type(r.blocks, 'object');
-    t.type(r.stacks, 'object');
-    t.ok(Array.isArray(r.stacks));
-
-    t.type(r.createBlock, 'function');
-    t.type(r.moveBlock, 'function');
-    t.type(r.changeBlock, 'function');
-    t.type(r.deleteBlock, 'function');
-
-    t.end();
-});
-
-test('create', function (t) {
-    var r = new Runtime();
-    r.createBlock({
-        id: 'foo',
-        opcode: 'TEST_BLOCK',
-        next: null,
-        fields: {}
-    });
-
-    t.type(r.blocks['foo'], 'object');
-    t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
-    t.notEqual(r.stacks.indexOf('foo'), -1);
-    t.end();
-});
-
-test('move', function (t) {
-    var r = new Runtime();
-    r.createBlock({
-        id: 'foo',
-        opcode: 'TEST_BLOCK',
-        next: null,
-        fields: {}
-    });
-    r.createBlock({
-        id: 'bar',
-        opcode: 'TEST_BLOCK',
-        next: null,
-        fields: {}
-    });
-
-    // Attach 'bar' to the end of 'foo'
-    r.moveBlock({
-        id: 'bar',
-        newParent: 'foo'
-    });
-    t.equal(r.stacks.length, 1);
-    t.equal(Object.keys(r.blocks).length, 2);
-    t.equal(r.blocks['foo'].next, 'bar');
-
-    // Detach 'bar' from 'foo'
-    r.moveBlock({
-        id: 'bar',
-        oldParent: 'foo'
-    });
-    t.equal(r.stacks.length, 2);
-    t.equal(Object.keys(r.blocks).length, 2);
-    t.equal(r.blocks['foo'].next, null);
-
-    t.end();
-});
-
-test('delete', function (t) {
-    var r = new Runtime();
-    r.createBlock({
-        id: 'foo',
-        opcode: 'TEST_BLOCK',
-        next: null,
-        fields: {}
-    });
-    r.deleteBlock({
-        id: 'foo'
-    });
-
-    t.type(r.blocks['foo'], 'undefined');
-    t.equal(r.stacks.indexOf('foo'), -1);
     t.end();
 });

From c8e050494c2d220790ae7c9e7bedb6fdb41e9dfe Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 16:03:40 -0400
Subject: [PATCH 17/27] Update "anatomy of a block" README for new structure

---
 README.md | 50 ++++++++++++++++++++++++--------------------------
 1 file changed, 24 insertions(+), 26 deletions(-)

diff --git a/README.md b/README.md
index 0d43b2616..a14c91d8d 100644
--- a/README.md
+++ b/README.md
@@ -52,35 +52,33 @@ The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tr
 #### Anatomy of a Block
 ```json
 {
-    "id": "^1r~63Gdl7;Dh?I*OP3_",
-    "opcode": "wedo_motorclockwise",
-    "next": null,
-    "fields": {
+    "7AJZR#NA;m*b}R]pdq63": {
+      "id": "7AJZR#NA;m*b}R]pdq63",
+      "opcode": "control_wait",
+      "inputs": {
         "DURATION": {
-            "name": "DURATION",
-            "value": null,
-            "blocks": {
-                "1?P=eV(OiDY3vMk!24Ip": {
-                    "id": "1?P=eV(OiDY3vMk!24Ip",
-                    "opcode": "math_number",
-                    "next": null,
-                    "fields": {
-                        "NUM": {
-                            "name": "NUM",
-                            "value": "10",
-                            "blocks": null
-                        }
-                    }
-                }
-            }
-        },
-        "SUBSTACK": {
-            "name": "SUBSTACK",
-            "value": "@1ln(HsUO4!]*2*%BrE|",
-            "blocks": null
+          "name": "DURATION",
+          "block": ",xA8/S!Z6+kR,9dph.rO"
         }
+      },
+      "fields": {},
+      "next": null,
+      "topLevel": true
+    },
+    ",xA8/S!Z6+kR,9dph.rO": {
+      "id": ",xA8/S!Z6+kR,9dph.rO",
+      "opcode": "math_number",
+      "inputs": {},
+      "fields": {
+        "NUM": {
+          "name": "NUM",
+          "value": "1"
+        }
+      },
+      "next": null,
+      "topLevel": false
     }
-}
+  }
 ```
 
 ## Testing

From 53372c5704870de11400761f840e96779bed4c48 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 16:17:12 -0400
Subject: [PATCH 18/27] Fixing case problem with blocks.js tests

---
 test/unit/blocks.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index 0e1bcabcc..3c2af2bc6 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -1,5 +1,5 @@
 var test = require('tap').test;
-var Blocks = require('../../src/engine/Blocks');
+var Blocks = require('../../src/engine/blocks');
 
 test('spec', function (t) {
     var b = new Blocks();

From f158af18b13da3b3604c73ae54555278f237268e Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Mon, 6 Jun 2016 16:19:57 -0400
Subject: [PATCH 19/27] Case problem with blocks.js in index.js

---
 src/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/index.js b/src/index.js
index a18d17da2..3b4bece67 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,7 @@
 var EventEmitter = require('events');
 var util = require('util');
 
-var Blocks = require('./engine/Blocks');
+var Blocks = require('./engine/blocks');
 var Runtime = require('./engine/runtime');
 var adapter = require('./engine/adapter');
 

From 4eb63cf33e16fdaff4d85ecb9cebda036d65e3f2 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 10:49:12 -0400
Subject: [PATCH 20/27] Test for CREATE event with substack

---
 test/fixtures/events.json | 11 +++++++++++
 test/unit/adapter.js      | 24 ++++++++++++++++++++++++
 2 files changed, 35 insertions(+)

diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index 064c76c69..4b7573504 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -11,5 +11,16 @@
             "z!+#Nqr,_(V=xz0y7a@d",
             "!6Ahqg4f}Ljl}X5Hws?Z"
         ]
+    },
+    "createsubstack": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><statement name=\"SUBSTACK\"><block type=\"control_wait\" id=\"{Rwt[LFtD1-JPAi-qf:.\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"VMDxt_9SYe5{*eNRe5dZ\"><field name=\"NUM\">1</field></shadow></value></block></statement></block>"
+        },
+        "ids": [
+            "r9`RpL74T6*SXPKv7}Dq",
+            "{Rwt[LFtD1-JPAi-qf:.",
+            "VMDxt_9SYe5{*eNRe5dZ"
+        ]
     }
 }
diff --git a/test/unit/adapter.js b/test/unit/adapter.js
index 31e904845..44ea13928 100644
--- a/test/unit/adapter.js
+++ b/test/unit/adapter.js
@@ -34,3 +34,27 @@ test('create event', function (t) {
 
     t.end();
 });
+
+test('create with substack', function (t) {
+    var result = adapter(events.createsubstack);
+    // Outer block
+    t.type(result[0].id, 'string');
+    t.type(result[0].opcode, 'string');
+    t.type(result[0].fields, 'object');
+    t.type(result[0].inputs, 'object');
+    t.type(result[0].inputs['SUBSTACK'], 'object');
+    t.type(result[0].topLevel, 'boolean');
+    t.equal(result[0].topLevel, true);
+    // In substack
+    var substackBlockId = result[0].inputs['SUBSTACK']['block'];
+    t.type(substackBlockId, 'string');
+    // Find actual substack block
+    var substackBlock = null;
+    for (var i = 0; i < result.length; i++) {
+        if (result[i].id == substackBlockId) {
+            substackBlock = result[i];
+        }
+    }
+    t.type(substackBlock, 'object');
+    t.end();
+});

From 06eaee9108bd966a61f4b750025b0555da6c93bf Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:04:44 -0400
Subject: [PATCH 21/27] Additional adapter tests for new cases

---
 test/fixtures/events.json | 25 +++++++++---
 test/unit/adapter.js      | 83 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+), 6 deletions(-)

diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index 4b7573504..f6f4cb796 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -16,11 +16,24 @@
         "name": "block",
         "xml": {
             "outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><statement name=\"SUBSTACK\"><block type=\"control_wait\" id=\"{Rwt[LFtD1-JPAi-qf:.\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"VMDxt_9SYe5{*eNRe5dZ\"><field name=\"NUM\">1</field></shadow></value></block></statement></block>"
-        },
-        "ids": [
-            "r9`RpL74T6*SXPKv7}Dq",
-            "{Rwt[LFtD1-JPAi-qf:.",
-            "VMDxt_9SYe5{*eNRe5dZ"
-        ]
+        }
+    },
+    "createtwosubstacks": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type=\"control_if_else\" id=\"8W?lmIY!Tgnh)~0!G#9-\" x=\"87\" y=\"159\"><statement name=\"SUBSTACK\"><block type=\"event_broadcast\" id=\"lgU2GGtwlREuasCB02Vr\"></block></statement><statement name=\"SUBSTACK2\"><block type=\"event_broadcast\" id=\"Gb]N,2P;|J%F?pxSwz(2\"></block></statement></block>"
+        }
+    },
+    "createtoplevelshadow": {
+        "name": "shadow",
+        "xml": {
+            "outerHTML": "<shadow type=\"math_number\" id=\"z9d57=IUI5se;DBbyug)\"><field name=\"NUM\">4</field></shadow>"
+        }
+    },
+    "createwithnext": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type=\"wedo_setcolor\" id=\"*CT)7+UKjQIEtUw.OGT6\" x=\"89\" y=\"48\"><next><block type=\"wedo_motorspeed\" id=\"Er*:^o7yYL#dX+5)R^xq\"></block></next></block>"
+        }
     }
 }
diff --git a/test/unit/adapter.js b/test/unit/adapter.js
index 44ea13928..72be10d88 100644
--- a/test/unit/adapter.js
+++ b/test/unit/adapter.js
@@ -7,6 +7,14 @@ test('spec', function (t) {
     t.end();
 });
 
+test('invalid inputs', function(t) {
+    var nothing = adapter('not an object');
+    t.type(nothing, 'undefined');
+    nothing = adapter({noxmlproperty:true});
+    t.type(nothing, 'undefined');
+    t.end();
+});
+
 test('create event', function (t) {
     var result = adapter(events.create);
 
@@ -58,3 +66,78 @@ test('create with substack', function (t) {
     t.type(substackBlock, 'object');
     t.end();
 });
+
+test('create with two substacks', function (t) {
+    var result = adapter(events.createtwosubstacks);
+    // Outer block
+    t.type(result[0].id, 'string');
+    t.type(result[0].opcode, 'string');
+    t.type(result[0].fields, 'object');
+    t.type(result[0].inputs, 'object');
+    t.type(result[0].inputs['SUBSTACK'], 'object');
+    t.type(result[0].inputs['SUBSTACK2'], 'object');
+    t.type(result[0].topLevel, 'boolean');
+    t.equal(result[0].topLevel, true);
+    // In substacks
+    var firstSubstackBlockId = result[0].inputs['SUBSTACK']['block'];
+    var secondSubstackBlockId = result[0].inputs['SUBSTACK2']['block'];
+    t.type(firstSubstackBlockId, 'string');
+    t.type(secondSubstackBlockId, 'string');
+    // Find actual substack blocks
+    var firstSubstackBlock = null;
+    var secondSubstackBlock = null;
+    for (var i = 0; i < result.length; i++) {
+        if (result[i].id == firstSubstackBlockId) {
+            firstSubstackBlock = result[i];
+        }
+        if (result[i].id == secondSubstackBlockId) {
+            secondSubstackBlock = result[i];
+        }
+    }
+    t.type(firstSubstackBlock, 'object');
+    t.type(secondSubstackBlock, 'object');
+    t.end();
+});
+
+test('create with top-level shadow', function (t) {
+    var result = adapter(events.createtoplevelshadow);
+    t.ok(Array.isArray(result));
+    t.equal(result.length, 1);
+
+    // Outer block
+    t.type(result[0].id, 'string');
+    t.type(result[0].opcode, 'string');
+    t.type(result[0].fields, 'object');
+    t.type(result[0].inputs, 'object');
+    t.type(result[0].topLevel, 'boolean');
+    t.equal(result[0].topLevel, true);
+    t.end();
+});
+
+test('create with next connection', function (t) {
+    var result = adapter(events.createwithnext);
+
+    t.ok(Array.isArray(result));
+    t.equal(result.length, 2);
+
+    // First block
+    t.type(result[0].id, 'string');
+    t.type(result[0].opcode, 'string');
+    t.type(result[0].fields, 'object');
+    t.type(result[0].inputs, 'object');
+    t.type(result[0].topLevel, 'boolean');
+    t.equal(result[0].topLevel, true);
+    t.type(result[0].next, 'string');
+    t.equal(result[0].next, result[1].id);
+
+    // Second block
+    t.type(result[1].id, 'string');
+    t.type(result[1].opcode, 'string');
+    t.type(result[1].fields, 'object');
+    t.type(result[1].inputs, 'object');
+    t.type(result[1].topLevel, 'boolean');
+    t.equal(result[1].topLevel, false);
+    t.equal(result[1].next, null);
+
+    t.end();
+});

From 855caab8d285607745158b55c8046b17dc3a7ed4 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:11:20 -0400
Subject: [PATCH 22/27] Add blocks delete chain test

---
 test/unit/blocks.js | 40 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index 3c2af2bc6..b9803d23f 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -95,3 +95,43 @@ test('delete', function (t) {
     t.equal(b._stacks.indexOf('foo'), -1);
     t.end();
 });
+
+test('delete chain', function (t) {
+    // Create a chain of connected blocks and delete the top one.
+    // All of them should be deleted.
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: 'foo2',
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: 'foo3',
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+    b.createBlock({
+        id: 'foo3',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+    b.deleteBlock({
+        id: 'foo'
+    });
+    t.type(b._blocks['foo'], 'undefined');
+    t.type(b._blocks['foo2'], 'undefined');
+    t.type(b._blocks['foo3'], 'undefined');
+    t.equal(b._stacks.indexOf('foo'), -1);
+    t.equal(Object.keys(b._blocks).length, 0);
+    t.equal(b._stacks.length, 0);
+    t.end();
+});

From 84213087829662e726b59eb79afb4e4e9147d2a9 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:18:41 -0400
Subject: [PATCH 23/27] Add delete test for blocks with inputs/statements

---
 test/unit/blocks.js | 63 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)

diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index b9803d23f..09690bf9a 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -135,3 +135,66 @@ test('delete chain', function (t) {
     t.equal(b._stacks.length, 0);
     t.end();
 });
+
+test('delete inputs', function (t) {
+    // Create a block with two inputs, one of which has its own input.
+    // Delete the block - all of them should be deleted.
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {
+            input1: {
+                name: 'input1',
+                block: 'foo2'
+            },
+            SUBSTACK: {
+                name: 'SUBSTACK',
+                block: 'foo3'
+            }
+        },
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+    b.createBlock({
+        id: 'foo3',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {
+            subinput: {
+                name: 'subinput',
+                block: 'foo4'
+            }
+        },
+        topLevel: false
+    });
+    b.createBlock({
+        id: 'foo4',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+    b.deleteBlock({
+        id: 'foo'
+    });
+    t.type(b._blocks['foo'], 'undefined');
+    t.type(b._blocks['foo2'], 'undefined');
+    t.type(b._blocks['foo3'], 'undefined');
+    t.type(b._blocks['foo4'], 'undefined');
+    t.equal(b._stacks.indexOf('foo'), -1);
+    t.equal(Object.keys(b._blocks).length, 0);
+    t.equal(b._stacks.length, 0);
+    t.end();
+});

From 3b4a00ce464bd431dd9222c976fd1010735d96c6 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:29:51 -0400
Subject: [PATCH 24/27] Add check for null `substackNum`, fix numbering

---
 src/engine/blocks.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index ad4c8d1c7..896e9c8b4 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -56,9 +56,10 @@
  Blocks.prototype.getSubstack = function (id, substackNum) {
      var block = this._blocks[id];
      if (typeof block === 'undefined') return null;
+     if (!substackNum) substackNum = 1;
 
      var inputName = 'SUBSTACK';
-     if (substackNum > 0) {
+     if (substackNum > 1) {
          inputName += substackNum;
      }
 

From 6e087475f6e5674bad9eb4914501d2e0b1486c8c Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:34:01 -0400
Subject: [PATCH 25/27] Tests for blocks.js getters

---
 test/unit/blocks.js | 211 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 211 insertions(+)

diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index 09690bf9a..ead221b24 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -16,10 +16,221 @@ test('spec', function (t) {
     t.type(b.moveBlock, 'function');
     t.type(b.changeBlock, 'function');
     t.type(b.deleteBlock, 'function');
+    t.type(b.getBlock, 'function');
+    t.type(b.getStacks, 'function');
+    t.type(b.getNextBlock, 'function');
+    t.type(b.getSubstack, 'function');
+    t.type(b.getOpcode, 'function');
+
 
     t.end();
 });
 
+// Getter tests
+test('getBlock', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    var block = b.getBlock('foo');
+    t.type(block, 'object');
+    var notBlock = b.getBlock('?');
+    t.type(notBlock, 'undefined');
+    t.end();
+});
+
+test('getStacks', function (t) {
+    var b = new Blocks();
+    var stacks = b.getStacks();
+    t.type(stacks, 'object');
+    t.equals(stacks.length, 0);
+    // Create two top-level blocks and one not.
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo3',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+
+    stacks = b.getStacks();
+    t.type(stacks, 'object');
+    t.equals(stacks.length, 2);
+    t.ok(stacks.indexOf('foo') > -1);
+    t.ok(stacks.indexOf('foo2') > -1);
+    t.equals(stacks.indexOf('foo3'), -1);
+    t.end();
+
+});
+
+test('getNextBlock', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+
+    var next = b.getNextBlock('foo');
+    t.equals(next, null);
+
+    // Add a block with "foo" as its next.
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: 'foo',
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+
+    next = b.getNextBlock('foo2');
+    t.equals(next, 'foo');
+
+    // Block that doesn't exist.
+    var noBlock = b.getNextBlock('?');
+    t.equals(noBlock, null);
+
+    t.end();
+});
+
+test('getSubstack', function (t) {
+    var b = new Blocks();
+    // Single substack
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {
+            SUBSTACK: {
+                name: 'SUBSTACK',
+                block: 'foo2'
+            }
+        },
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+
+    var substack = b.getSubstack('foo');
+    t.equals(substack, 'foo2');
+
+    var notSubstack = b.getSubstack('?');
+    t.equals(notSubstack, null);
+
+    t.end();
+});
+
+test('getSubstack2', function (t) {
+    var b = new Blocks();
+    // Second substack
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {
+            SUBSTACK: {
+                name: 'SUBSTACK',
+                block: 'foo2'
+            },
+            SUBSTACK2: {
+                name: 'SUBSTACK2',
+                block: 'foo3'
+            }
+        },
+        topLevel: true
+    });
+    b.createBlock({
+        id: 'foo2',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+    b.createBlock({
+        id: 'foo3',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: false
+    });
+
+    var substack1 = b.getSubstack('foo', 1);
+    var substack2 = b.getSubstack('foo', 2);
+    t.equals(substack1, 'foo2');
+    t.equals(substack2, 'foo3');
+
+    t.end();
+});
+
+test('getSubstack with none', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    var noSubstack = b.getSubstack('foo');
+    t.equals(noSubstack, null);
+    t.end();
+});
+
+test('getOpcode', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {},
+        inputs: {},
+        topLevel: true
+    });
+    var opcode = b.getOpcode('foo');
+    t.equals(opcode, 'TEST_BLOCK');
+    var notOpcode = b.getOpcode('?');
+    t.equals(notOpcode, null);
+    t.end();
+});
+
+// Block events tests
 test('create', function (t) {
     var b = new Blocks();
     b.createBlock({

From e9accca107cecc3273dd47013f2cc3603fc40743 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 11:40:47 -0400
Subject: [PATCH 26/27] Add test for blocks.js `changeBlock`

---
 test/unit/blocks.js | 57 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index ead221b24..518317f59 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -288,6 +288,63 @@ test('move', function (t) {
     t.end();
 });
 
+test('change', function (t) {
+    var b = new Blocks();
+    b.createBlock({
+        id: 'foo',
+        opcode: 'TEST_BLOCK',
+        next: null,
+        fields: {
+            someField: {
+                name: 'someField',
+                value: 'initial-value'
+            }
+        },
+        inputs: {},
+        topLevel: true
+    });
+
+    // Test that the field is updated
+    t.equal(b._blocks['foo'].fields.someField.value, 'initial-value');
+
+    b.changeBlock({
+        element: 'field',
+        id: 'foo',
+        name: 'someField',
+        value: 'final-value'
+    });
+
+    t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+    // Invalid cases
+    // No `element`
+    b.changeBlock({
+        id: 'foo',
+        name: 'someField',
+        value: 'invalid-value'
+    });
+    t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+    // No block ID
+    b.changeBlock({
+        element: 'field',
+        name: 'someField',
+        value: 'invalid-value'
+    });
+    t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+    // No such field
+    b.changeBlock({
+        element: 'field',
+        id: 'foo',
+        name: 'someWrongField',
+        value: 'final-value'
+    });
+    t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+    t.end();
+});
+
 test('delete', function (t) {
     var b = new Blocks();
     b.createBlock({

From 2339c5e71024626ec6faa148736fa6cf5ae69a70 Mon Sep 17 00:00:00 2001
From: Tim Mickel <tim.mickel@gmail.com>
Date: Tue, 7 Jun 2016 12:01:47 -0400
Subject: [PATCH 27/27] Additional tests for invalid `adapter` inputs

---
 src/engine/adapter.js     |  7 +++++++
 test/fixtures/events.json | 18 ++++++++++++++++++
 test/unit/adapter.js      | 24 ++++++++++++++++++++++++
 3 files changed, 49 insertions(+)

diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 7f891da34..48613ec5d 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -32,6 +32,9 @@ function domToBlocks (blocksDOM) {
     var blocks = {};
     for (var i = 0; i < blocksDOM.length; i++) {
         var block = blocksDOM[i];
+        if (!block.name || !block.attribs) {
+            continue;
+        }
         var tagName = block.name.toLowerCase();
         if (tagName == 'block' || tagName == 'shadow') {
             domToBlock(block, blocks, true);
@@ -114,6 +117,10 @@ function domToBlock (blockDOM, blocks, isTopBlock) {
             };
             break;
         case 'next':
+            if (!childBlockNode || !childBlockNode.attribs) {
+                // Invalid child block.
+                continue;
+            }
             // Recursively generate block structure for next block.
             domToBlock(childBlockNode, blocks, false);
             // Link next block to this block.
diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index f6f4cb796..6d94fc62a 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -35,5 +35,23 @@
         "xml": {
             "outerHTML": "<block type=\"wedo_setcolor\" id=\"*CT)7+UKjQIEtUw.OGT6\" x=\"89\" y=\"48\"><next><block type=\"wedo_motorspeed\" id=\"Er*:^o7yYL#dX+5)R^xq\"></block></next></block>"
         }
+    },
+    "createinvalid": {
+        "name": "whatever",
+        "xml": {
+            "outerHTML": "<xml></xml>"
+        }
+    },
+    "createinvalidgrandchild": {
+        "name": "block",
+        "xml": {
+            "outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><next><invalidgrandchild>xxx</invalidgrandchild></next></block>"
+        }
+    },
+    "createbadxml": {
+        "name": "whatever",
+        "xml": {
+            "outerHTML": "></xml>"
+        }
     }
 }
diff --git a/test/unit/adapter.js b/test/unit/adapter.js
index 72be10d88..838815626 100644
--- a/test/unit/adapter.js
+++ b/test/unit/adapter.js
@@ -141,3 +141,27 @@ test('create with next connection', function (t) {
 
     t.end();
 });
+
+test('create with invalid block xml', function (t) {
+    // Entirely invalid block XML
+    var result = adapter(events.createinvalid);
+    t.ok(Array.isArray(result));
+    t.equal(result.length, 0);
+
+    // Invalid grandchild tag
+    var result2 = adapter(events.createinvalidgrandchild);
+    t.ok(Array.isArray(result2));
+    t.equal(result2.length, 1);
+    t.type(result2[0].id, 'string');
+    t.equal(Object.keys(result2[0].inputs).length, 0);
+    t.equal(Object.keys(result2[0].fields).length, 0);
+
+    t.end();
+});
+
+test('create with invalid xml', function (t) {
+    var result = adapter(events.createbadxml);
+    t.ok(Array.isArray(result));
+    t.equal(result.length, 0);
+    t.end();
+});