From faf953b18b5aa306b34ff174ac348815b897420a Mon Sep 17 00:00:00 2001 From: Monica Kozbial Date: Mon, 11 Jan 2021 15:58:45 -0800 Subject: [PATCH] Process xml tags in an explicit order in domToBlockHeadless_ (#4571) * Process xml tags in an explicit order in domToBlockHeadless_ --- core/xml.js | 364 ++++++++++++++++++++++++++++++++++----------------- package.json | 1 + 2 files changed, 247 insertions(+), 118 deletions(-) diff --git a/core/xml.js b/core/xml.js index 6fa0aa797..cb497f236 100644 --- a/core/xml.js +++ b/core/xml.js @@ -614,6 +614,241 @@ Blockly.Xml.domToVariables = function(xmlVariables, workspace) { } }; +/** + * A mapping of nodeName to node for child nodes of xmlBlock. + * @typedef {{ + * mutation: !Array, + * comment: !Array, + * data: !Array, + * field: !Array, + * input: !Array, + * next: !Array + * }} + */ +Blockly.Xml.childNodeTagMap; + +/** + * Creates a mapping of childNodes for each supported xml tag for the provided + * xmlBlock. Logs a warning for any encountered unsupported tags. + * @param {!Element} xmlBlock XML block element. + * @return {Blockly.Xml.childNodeTagMap} The childNode map from nodeName to + * node. + */ +Blockly.Xml.mapSupportedXmlTags_ = function(xmlBlock) { + var childNodeMap = { + mutation: [], comment: [], data: [], field: [], input: [], + next: [] + }; + for (var i = 0, xmlChild; (xmlChild = xmlBlock.childNodes[i]); i++) { + if (xmlChild.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE) { + // Ignore any text at the level. It's all whitespace anyway. + continue; + } + switch (xmlChild.nodeName.toLowerCase()) { + case 'mutation': + childNodeMap.mutation.push(xmlChild); + break; + case 'comment': + if (!Blockly.Comment) { + console.warn('Missing require for Blockly.Comment, ' + + 'ignoring block comment.'); + break; + } + childNodeMap.comment.push(xmlChild); + break; + case 'data': + childNodeMap.data.push(xmlChild); + break; + case 'title': + // Titles were renamed to field in December 2013. + // Fall through. + case 'field': + childNodeMap.field.push(xmlChild); + break; + case 'value': + case 'statement': + childNodeMap.input.push(xmlChild); + break; + case 'next': + childNodeMap.next.push(xmlChild); + break; + default: + // Unknown tag; ignore. Same principle as HTML parsers. + console.warn('Ignoring unknown tag: ' + xmlChild.nodeName); + } + } + return childNodeMap; +}; + +/** + * Applies mutation tag child nodes to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @return {boolean} True if mutation may have added some elements that need + * initialization (requiring initSvg call). + * @private + */ +Blockly.Xml.applyMutationTagNodes_ = function(xmlChildren, block) { + var shouldCallInitSvg = false; + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + // Custom data for an advanced block. + if (block.domToMutation) { + block.domToMutation(xmlChild); + if (block.initSvg) { + // Mutation may have added some elements that need initializing. + shouldCallInitSvg = true; + } + } + } + return shouldCallInitSvg; +}; + +/** + * Applies comment tag child nodes to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @private + */ +Blockly.Xml.applyCommentTagNodes_ = function(xmlChildren, block) { + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + var text = xmlChild.textContent; + var pinned = xmlChild.getAttribute('pinned') == 'true'; + var width = parseInt(xmlChild.getAttribute('w'), 10); + var height = parseInt(xmlChild.getAttribute('h'), 10); + + block.setCommentText(text); + block.commentModel.pinned = pinned; + if (!isNaN(width) && !isNaN(height)) { + block.commentModel.size = new Blockly.utils.Size(width, height); + } + + if (pinned && block.getCommentIcon && !block.isInFlyout) { + setTimeout(function() { + block.getCommentIcon().setVisible(true); + }, 1); + } + } +}; + +/** + * Applies data tag child nodes to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @private + */ +Blockly.Xml.applyDataTagNodes_ = function(xmlChildren, block) { + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + block.data = xmlChild.textContent; + } +}; +/** + * Applies field tag child nodes to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @private + */ +Blockly.Xml.applyFieldTagNodes_ = function(xmlChildren, block) { + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + var nodeName = xmlChild.getAttribute('name'); + Blockly.Xml.domToField_(block, nodeName, xmlChild); + } +}; + +/** + * Finds any enclosed blocks or shadows within this xml node. + * @param {!Element} xmlNode The xml node to extract child block info from. + * @return {{childBlockElement: ?Element, childShadowElement: ?Element}} Any + * found child block. + * @private + */ +Blockly.Xml.findChildBlocks_ = function(xmlNode) { + var childBlockInfo = {childBlockElement: null, childShadowElement: null}; + for (var i = 0, xmlChild; (xmlChild = xmlNode.childNodes[i]); i++) { + if (xmlChild.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) { + if (xmlChild.nodeName.toLowerCase() == 'block') { + childBlockInfo.childBlockElement = /** @type {!Element} */ (xmlChild); + } else if (xmlChild.nodeName.toLowerCase() == 'shadow') { + childBlockInfo.childShadowElement = /** @type {!Element} */ (xmlChild); + } + } + } + return childBlockInfo; +}; + +/** + * Applies input child nodes (value or statement) to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Workspace} workspace The workspace containing the given + * block. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @param {string} prototypeName The prototype name of the block. + * @private + */ +Blockly.Xml.applyInputTagNodes_ = function(xmlChildren, workspace, block, + prototypeName) { + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + var nodeName = xmlChild.getAttribute('name'); + var input = block.getInput(nodeName); + if (!input) { + console.warn('Ignoring non-existent input ' + nodeName + ' in block ' + + prototypeName); + break; + } + var childBlockInfo = Blockly.Xml.findChildBlocks_(xmlChild); + if (childBlockInfo.childBlockElement) { + // Create child block. + var blockChild = Blockly.Xml.domToBlockHeadless_( + childBlockInfo.childBlockElement, workspace); + if (blockChild.outputConnection) { + input.connection.connect(blockChild.outputConnection); + } else if (blockChild.previousConnection) { + input.connection.connect(blockChild.previousConnection); + } else { + throw TypeError( + 'Child block does not have output or previous statement.'); + } + } + // Set shadow after so we don't create a shadow we delete immediately. + if (childBlockInfo.childShadowElement) { + input.connection.setShadowDom(childBlockInfo.childShadowElement); + } + } +}; + +/** + * Applies next child nodes to the given block. + * @param {Array} xmlChildren Child nodes. + * @param {!Blockly.Workspace} workspace The workspace containing the given + * block. + * @param {!Blockly.Block} block The block to apply the child nodes on. + * @private + */ +Blockly.Xml.applyNextTagNodes_ = function(xmlChildren, workspace, block) { + for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) { + var childBlockInfo = Blockly.Xml.findChildBlocks_(xmlChild); + if (childBlockInfo.childBlockElement) { + if (!block.nextConnection) { + throw TypeError('Next statement does not exist.'); + } + // If there is more than one XML 'next' tag. + if (block.nextConnection.isConnected()) { + throw TypeError('Next statement is already connected.'); + } + var blockChild = Blockly.Xml.domToBlockHeadless_( + childBlockInfo.childBlockElement, workspace); + if (!blockChild.previousConnection) { + throw TypeError('Next block does not have previous statement.'); + } + block.nextConnection.connect(blockChild.previousConnection); + } + // Set shadow after so we don't create a shadow we delete immediately. + if (childBlockInfo.childShadowElement && block.nextConnection) { + block.nextConnection.setShadowDom(childBlockInfo.childShadowElement); + } + } +}; + + /** * Decode an XML block tag and create a block (and possibly sub blocks) on the * workspace. @@ -631,126 +866,19 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { var id = xmlBlock.getAttribute('id'); block = workspace.newBlock(prototypeName, id); - var blockChild = null; - for (var i = 0, xmlChild; (xmlChild = xmlBlock.childNodes[i]); i++) { - if (xmlChild.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE) { - // Ignore any text at the level. It's all whitespace anyway. - continue; - } - var input; + // Preprocess childNodes so tags can be processed in a consistent order. + var xmlChildNameMap = Blockly.Xml.mapSupportedXmlTags_(xmlBlock); - // Find any enclosed blocks or shadows in this tag. - var childBlockElement = null; - var childShadowElement = null; - for (var j = 0, grandchild; (grandchild = xmlChild.childNodes[j]); j++) { - if (grandchild.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) { - if (grandchild.nodeName.toLowerCase() == 'block') { - childBlockElement = /** @type {!Element} */ (grandchild); - } else if (grandchild.nodeName.toLowerCase() == 'shadow') { - childShadowElement = /** @type {!Element} */ (grandchild); - } - } - } + var shouldCallInitSvg = + Blockly.Xml.applyMutationTagNodes_(xmlChildNameMap.mutation, block); + Blockly.Xml.applyCommentTagNodes_(xmlChildNameMap.comment, block); + Blockly.Xml.applyDataTagNodes_(xmlChildNameMap.data, block); + Blockly.Xml.applyFieldTagNodes_(xmlChildNameMap.field, block); + Blockly.Xml.applyInputTagNodes_( + xmlChildNameMap.input, workspace, block, prototypeName); + Blockly.Xml.applyNextTagNodes_(xmlChildNameMap.next, workspace, block); - var callInitSvg = false; - var name = xmlChild.getAttribute('name'); - var xmlChildElement = /** @type {!Element} */ (xmlChild); - switch (xmlChild.nodeName.toLowerCase()) { - case 'mutation': - // Custom data for an advanced block. - if (block.domToMutation) { - block.domToMutation(xmlChildElement); - if (block.initSvg) { - // Mutation may have added some elements that need initializing. - callInitSvg = true; - } - } - break; - case 'comment': - if (!Blockly.Comment) { - console.warn('Missing require for Blockly.Comment, ' + - 'ignoring block comment.'); - break; - } - var text = xmlChildElement.textContent; - var pinned = xmlChildElement.getAttribute('pinned') == 'true'; - var width = parseInt(xmlChildElement.getAttribute('w'), 10); - var height = parseInt(xmlChildElement.getAttribute('h'), 10); - - block.setCommentText(text); - block.commentModel.pinned = pinned; - if (!isNaN(width) && !isNaN(height)) { - block.commentModel.size = new Blockly.utils.Size(width, height); - } - - if (pinned && block.getCommentIcon && !block.isInFlyout) { - setTimeout(function() { - block.getCommentIcon().setVisible(true); - }, 1); - } - break; - case 'data': - block.data = xmlChild.textContent; - break; - case 'title': - // Titles were renamed to field in December 2013. - // Fall through. - case 'field': - Blockly.Xml.domToField_(block, name, xmlChildElement); - break; - case 'value': - case 'statement': - input = block.getInput(name); - if (!input) { - console.warn('Ignoring non-existent input ' + name + ' in block ' + - prototypeName); - break; - } - if (childBlockElement) { - blockChild = Blockly.Xml.domToBlockHeadless_(childBlockElement, - workspace); - if (blockChild.outputConnection) { - input.connection.connect(blockChild.outputConnection); - } else if (blockChild.previousConnection) { - input.connection.connect(blockChild.previousConnection); - } else { - throw TypeError( - 'Child block does not have output or previous statement.'); - } - } - // Set shadow after so we don't create a shadow we delete immediately. - if (childShadowElement) { - input.connection.setShadowDom(childShadowElement); - } - break; - case 'next': - if (childBlockElement) { - if (!block.nextConnection) { - throw TypeError('Next statement does not exist.'); - } - // If there is more than one XML 'next' tag. - if (block.nextConnection.isConnected()) { - throw TypeError('Next statement is already connected.'); - } - blockChild = Blockly.Xml.domToBlockHeadless_(childBlockElement, - workspace); - if (!blockChild.previousConnection) { - throw TypeError('Next block does not have previous statement.'); - } - block.nextConnection.connect(blockChild.previousConnection); - } - // Set shadow after so we don't create a shadow we delete immediately. - if (childShadowElement && block.nextConnection) { - block.nextConnection.setShadowDom(childShadowElement); - } - break; - default: - // Unknown tag; ignore. Same principle as HTML parsers. - console.warn('Ignoring unknown tag: ' + xmlChild.nodeName); - } - } - - if (callInitSvg) { + if (shouldCallInitSvg) { // InitSvg needs to be called after variable fields are loaded. block.initSvg(); } diff --git a/package.json b/package.json index 257f36a11..3c4687ffa 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "recompile": "gulp recompile", "release": "gulp gitCreateRC", "test": "concurrently 'npm run test:prepare' 'sleep 5 && npm run test:run'", + "test:generators": "concurrently 'npm run test:prepare' 'sleep 5 && tests/scripts/run_generators.sh'", "test:prepare": "npm run test:setupselenium && npm run test:startselenium", "test:run": "tests/run_all_tests.sh", "test:setupselenium": "selenium-standalone install --config=./tests/scripts/selenium-config.js",