xml.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview XML reader and writer.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Xml');
  26. goog.require('goog.asserts');
  27. goog.require('goog.dom');
  28. /**
  29. * Encode a block tree as XML.
  30. * @param {!Blockly.Workspace} workspace The workspace containing blocks.
  31. * @param {boolean} opt_noId True if the encoder should skip the block ids.
  32. * @return {!Element} XML document.
  33. */
  34. Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
  35. var xml = goog.dom.createDom('xml');
  36. var blocks = workspace.getTopBlocks(true);
  37. for (var i = 0, block; block = blocks[i]; i++) {
  38. xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId));
  39. }
  40. return xml;
  41. };
  42. /**
  43. * Encode a block subtree as XML with XY coordinates.
  44. * @param {!Blockly.Block} block The root block to encode.
  45. * @param {boolean} opt_noId True if the encoder should skip the block id.
  46. * @return {!Element} Tree of XML elements.
  47. */
  48. Blockly.Xml.blockToDomWithXY = function(block, opt_noId) {
  49. var width; // Not used in LTR.
  50. if (block.workspace.RTL) {
  51. width = block.workspace.getWidth();
  52. }
  53. var element = Blockly.Xml.blockToDom(block, opt_noId);
  54. var xy = block.getRelativeToSurfaceXY();
  55. element.setAttribute('x',
  56. Math.round(block.workspace.RTL ? width - xy.x : xy.x));
  57. element.setAttribute('y', Math.round(xy.y));
  58. return element;
  59. };
  60. /**
  61. * Encode a block subtree as XML.
  62. * @param {!Blockly.Block} block The root block to encode.
  63. * @param {boolean} opt_noId True if the encoder should skip the block id.
  64. * @return {!Element} Tree of XML elements.
  65. */
  66. Blockly.Xml.blockToDom = function(block, opt_noId) {
  67. var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block');
  68. element.setAttribute('type', block.type);
  69. if (!opt_noId) {
  70. element.setAttribute('id', block.id);
  71. }
  72. if (block.mutationToDom) {
  73. // Custom data for an advanced block.
  74. var mutation = block.mutationToDom();
  75. if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) {
  76. element.appendChild(mutation);
  77. }
  78. }
  79. function fieldToDom(field) {
  80. if (field.name && field.EDITABLE) {
  81. var container = goog.dom.createDom('field', null, field.getValue());
  82. container.setAttribute('name', field.name);
  83. element.appendChild(container);
  84. }
  85. }
  86. for (var i = 0, input; input = block.inputList[i]; i++) {
  87. for (var j = 0, field; field = input.fieldRow[j]; j++) {
  88. fieldToDom(field);
  89. }
  90. }
  91. var commentText = block.getCommentText();
  92. if (commentText) {
  93. var commentElement = goog.dom.createDom('comment', null, commentText);
  94. if (typeof block.comment == 'object') {
  95. commentElement.setAttribute('pinned', block.comment.isVisible());
  96. var hw = block.comment.getBubbleSize();
  97. commentElement.setAttribute('h', hw.height);
  98. commentElement.setAttribute('w', hw.width);
  99. }
  100. element.appendChild(commentElement);
  101. }
  102. if (block.data) {
  103. var dataElement = goog.dom.createDom('data', null, block.data);
  104. element.appendChild(dataElement);
  105. }
  106. for (var i = 0, input; input = block.inputList[i]; i++) {
  107. var container;
  108. var empty = true;
  109. if (input.type == Blockly.DUMMY_INPUT) {
  110. continue;
  111. } else {
  112. var childBlock = input.connection.targetBlock();
  113. if (input.type == Blockly.INPUT_VALUE) {
  114. container = goog.dom.createDom('value');
  115. } else if (input.type == Blockly.NEXT_STATEMENT) {
  116. container = goog.dom.createDom('statement');
  117. }
  118. var shadow = input.connection.getShadowDom();
  119. if (shadow && (!childBlock || !childBlock.isShadow())) {
  120. container.appendChild(Blockly.Xml.cloneShadow_(shadow));
  121. }
  122. if (childBlock) {
  123. container.appendChild(Blockly.Xml.blockToDom(childBlock, opt_noId));
  124. empty = false;
  125. }
  126. }
  127. container.setAttribute('name', input.name);
  128. if (!empty) {
  129. element.appendChild(container);
  130. }
  131. }
  132. if (block.inputsInlineDefault != block.inputsInline) {
  133. element.setAttribute('inline', block.inputsInline);
  134. }
  135. if (block.isCollapsed()) {
  136. element.setAttribute('collapsed', true);
  137. }
  138. if (block.disabled) {
  139. element.setAttribute('disabled', true);
  140. }
  141. if (!block.isDeletable() && !block.isShadow()) {
  142. element.setAttribute('deletable', false);
  143. }
  144. if (!block.isMovable() && !block.isShadow()) {
  145. element.setAttribute('movable', false);
  146. }
  147. if (!block.isEditable()) {
  148. element.setAttribute('editable', false);
  149. }
  150. var nextBlock = block.getNextBlock();
  151. if (nextBlock) {
  152. var container = goog.dom.createDom('next', null,
  153. Blockly.Xml.blockToDom(nextBlock, opt_noId));
  154. element.appendChild(container);
  155. }
  156. var shadow = block.nextConnection && block.nextConnection.getShadowDom();
  157. if (shadow && (!nextBlock || !nextBlock.isShadow())) {
  158. container.appendChild(Blockly.Xml.cloneShadow_(shadow));
  159. }
  160. return element;
  161. };
  162. /**
  163. * Deeply clone the shadow's DOM so that changes don't back-wash to the block.
  164. * @param {!Element} shadow A tree of XML elements.
  165. * @return {!Element} A tree of XML elements.
  166. * @private
  167. */
  168. Blockly.Xml.cloneShadow_ = function(shadow) {
  169. shadow = shadow.cloneNode(true);
  170. // Walk the tree looking for whitespace. Don't prune whitespace in a tag.
  171. var node = shadow;
  172. var textNode;
  173. while (node) {
  174. if (node.firstChild) {
  175. node = node.firstChild;
  176. } else {
  177. while (node && !node.nextSibling) {
  178. textNode = node;
  179. node = node.parentNode;
  180. if (textNode.nodeType == 3 && textNode.data.trim() == '' &&
  181. node.firstChild != textNode) {
  182. // Prune whitespace after a tag.
  183. goog.dom.removeNode(textNode);
  184. }
  185. }
  186. if (node) {
  187. textNode = node;
  188. node = node.nextSibling;
  189. if (textNode.nodeType == 3 && textNode.data.trim() == '') {
  190. // Prune whitespace before a tag.
  191. goog.dom.removeNode(textNode);
  192. }
  193. }
  194. }
  195. }
  196. return shadow;
  197. };
  198. /**
  199. * Converts a DOM structure into plain text.
  200. * Currently the text format is fairly ugly: all one line with no whitespace.
  201. * @param {!Element} dom A tree of XML elements.
  202. * @return {string} Text representation.
  203. */
  204. Blockly.Xml.domToText = function(dom) {
  205. var oSerializer = new XMLSerializer();
  206. return oSerializer.serializeToString(dom);
  207. };
  208. /**
  209. * Converts a DOM structure into properly indented text.
  210. * @param {!Element} dom A tree of XML elements.
  211. * @return {string} Text representation.
  212. */
  213. Blockly.Xml.domToPrettyText = function(dom) {
  214. // This function is not guaranteed to be correct for all XML.
  215. // But it handles the XML that Blockly generates.
  216. var blob = Blockly.Xml.domToText(dom);
  217. // Place every open and close tag on its own line.
  218. var lines = blob.split('<');
  219. // Indent every line.
  220. var indent = '';
  221. for (var i = 1; i < lines.length; i++) {
  222. var line = lines[i];
  223. if (line[0] == '/') {
  224. indent = indent.substring(2);
  225. }
  226. lines[i] = indent + '<' + line;
  227. if (line[0] != '/' && line.slice(-2) != '/>') {
  228. indent += ' ';
  229. }
  230. }
  231. // Pull simple tags back together.
  232. // E.g. <foo></foo>
  233. var text = lines.join('\n');
  234. text = text.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g, '$1</$2>');
  235. // Trim leading blank line.
  236. return text.replace(/^\n/, '');
  237. };
  238. /**
  239. * Converts plain text into a DOM structure.
  240. * Throws an error if XML doesn't parse.
  241. * @param {string} text Text representation.
  242. * @return {!Element} A tree of XML elements.
  243. */
  244. Blockly.Xml.textToDom = function(text) {
  245. var oParser = new DOMParser();
  246. var dom = oParser.parseFromString(text, 'text/xml');
  247. // The DOM should have one and only one top-level node, an XML tag.
  248. if (!dom || !dom.firstChild ||
  249. //dom.firstChild.nodeName.toLowerCase() != 'xml' ||
  250. dom.firstChild !== dom.lastChild) {
  251. // Whatever we got back from the parser is not XML.
  252. goog.asserts.fail('Blockly.Xml.textToDom did not obtain a valid XML tree.');
  253. }
  254. return dom.firstChild;
  255. };
  256. /**
  257. * Decode an XML DOM and create blocks on the workspace.
  258. * @param {!Element} xml XML DOM.
  259. * @param {!Blockly.Workspace} workspace The workspace.
  260. */
  261. Blockly.Xml.domToWorkspace = function(xml, workspace) {
  262. if (xml instanceof Blockly.Workspace) {
  263. var swap = xml;
  264. xml = workspace;
  265. workspace = swap;
  266. console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' +
  267. 'swap the arguments.');
  268. }
  269. var width; // Not used in LTR.
  270. if (workspace.RTL) {
  271. width = workspace.getWidth();
  272. }
  273. Blockly.Field.startCache();
  274. // Safari 7.1.3 is known to provide node lists with extra references to
  275. // children beyond the lists' length. Trust the length, do not use the
  276. // looping pattern of checking the index for an object.
  277. var childCount = xml.childNodes.length;
  278. var existingGroup = Blockly.Events.getGroup();
  279. if (!existingGroup) {
  280. Blockly.Events.setGroup(true);
  281. }
  282. // Disable workspace resizes as an optimization.
  283. if (workspace.setResizesEnabled) {
  284. workspace.setResizesEnabled(false);
  285. }
  286. for (var i = 0; i < childCount; i++) {
  287. var xmlChild = xml.childNodes[i];
  288. var name = xmlChild.nodeName.toLowerCase();
  289. if (name == 'block' ||
  290. (name == 'shadow' && !Blockly.Events.recordUndo)) {
  291. // Allow top-level shadow blocks if recordUndo is disabled since
  292. // that means an undo is in progress. Such a block is expected
  293. // to be moved to a nested destination in the next operation.
  294. var block = Blockly.Xml.domToBlock(xmlChild, workspace);
  295. var blockX = parseInt(xmlChild.getAttribute('x'), 10);
  296. var blockY = parseInt(xmlChild.getAttribute('y'), 10);
  297. if (!isNaN(blockX) && !isNaN(blockY)) {
  298. block.moveBy(workspace.RTL ? width - blockX : blockX, blockY);
  299. }
  300. } else if (name == 'shadow') {
  301. goog.asserts.fail('Shadow block cannot be a top-level block.');
  302. }
  303. }
  304. if (!existingGroup) {
  305. Blockly.Events.setGroup(false);
  306. }
  307. Blockly.Field.stopCache();
  308. workspace.updateVariableList(false);
  309. // Re-enable workspace resizing.
  310. if (workspace.setResizesEnabled) {
  311. workspace.setResizesEnabled(true);
  312. }
  313. };
  314. /**
  315. * Decode an XML block tag and create a block (and possibly sub blocks) on the
  316. * workspace.
  317. * @param {!Element} xmlBlock XML block element.
  318. * @param {!Blockly.Workspace} workspace The workspace.
  319. * @return {!Blockly.Block} The root block created.
  320. */
  321. Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
  322. if (xmlBlock instanceof Blockly.Workspace) {
  323. var swap = xmlBlock;
  324. xmlBlock = workspace;
  325. workspace = swap;
  326. console.warn('Deprecated call to Blockly.Xml.domToBlock, ' +
  327. 'swap the arguments.');
  328. }
  329. // Create top-level block.
  330. Blockly.Events.disable();
  331. try {
  332. var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace);
  333. if (workspace.rendered) {
  334. // Hide connections to speed up assembly.
  335. topBlock.setConnectionsHidden(true);
  336. // Generate list of all blocks.
  337. var blocks = topBlock.getDescendants();
  338. // Render each block.
  339. for (var i = blocks.length - 1; i >= 0; i--) {
  340. blocks[i].initSvg();
  341. }
  342. for (var i = blocks.length - 1; i >= 0; i--) {
  343. blocks[i].render(false);
  344. }
  345. // Populating the connection database may be defered until after the
  346. // blocks have rendered.
  347. setTimeout(function() {
  348. if (topBlock.workspace) { // Check that the block hasn't been deleted.
  349. topBlock.setConnectionsHidden(false);
  350. }
  351. }, 1);
  352. topBlock.updateDisabled();
  353. // Allow the scrollbars to resize and move based on the new contents.
  354. // TODO(@picklesrus): #387. Remove when domToBlock avoids resizing.
  355. workspace.resizeContents();
  356. }
  357. } finally {
  358. Blockly.Events.enable();
  359. }
  360. if (Blockly.Events.isEnabled()) {
  361. Blockly.Events.fire(new Blockly.Events.Create(topBlock));
  362. }
  363. return topBlock;
  364. };
  365. /**
  366. * Decode an XML block tag and create a block (and possibly sub blocks) on the
  367. * workspace.
  368. * @param {!Element} xmlBlock XML block element.
  369. * @param {!Blockly.Workspace} workspace The workspace.
  370. * @return {!Blockly.Block} The root block created.
  371. * @private
  372. */
  373. Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
  374. var block = null;
  375. var prototypeName = xmlBlock.getAttribute('type');
  376. goog.asserts.assert(prototypeName, 'Block type unspecified: %s',
  377. xmlBlock.outerHTML);
  378. var id = xmlBlock.getAttribute('id');
  379. block = workspace.newBlock(prototypeName, id);
  380. var blockChild = null;
  381. for (var i = 0, xmlChild; xmlChild = xmlBlock.childNodes[i]; i++) {
  382. if (xmlChild.nodeType == 3) {
  383. // Ignore any text at the <block> level. It's all whitespace anyway.
  384. continue;
  385. }
  386. var input;
  387. // Find any enclosed blocks or shadows in this tag.
  388. var childBlockNode = null;
  389. var childShadowNode = null;
  390. for (var j = 0, grandchildNode; grandchildNode = xmlChild.childNodes[j];
  391. j++) {
  392. if (grandchildNode.nodeType == 1) {
  393. if (grandchildNode.nodeName.toLowerCase() == 'block') {
  394. childBlockNode = grandchildNode;
  395. } else if (grandchildNode.nodeName.toLowerCase() == 'shadow') {
  396. childShadowNode = grandchildNode;
  397. }
  398. }
  399. }
  400. // Use the shadow block if there is no child block.
  401. if (!childBlockNode && childShadowNode) {
  402. childBlockNode = childShadowNode;
  403. }
  404. var name = xmlChild.getAttribute('name');
  405. switch (xmlChild.nodeName.toLowerCase()) {
  406. case 'mutation':
  407. // Custom data for an advanced block.
  408. if (block.domToMutation) {
  409. block.domToMutation(xmlChild);
  410. if (block.initSvg) {
  411. // Mutation may have added some elements that need initalizing.
  412. block.initSvg();
  413. }
  414. }
  415. break;
  416. case 'comment':
  417. block.setCommentText(xmlChild.textContent);
  418. var visible = xmlChild.getAttribute('pinned');
  419. if (visible && !block.isInFlyout) {
  420. // Give the renderer a millisecond to render and position the block
  421. // before positioning the comment bubble.
  422. setTimeout(function() {
  423. if (block.comment && block.comment.setVisible) {
  424. block.comment.setVisible(visible == 'true');
  425. }
  426. }, 1);
  427. }
  428. var bubbleW = parseInt(xmlChild.getAttribute('w'), 10);
  429. var bubbleH = parseInt(xmlChild.getAttribute('h'), 10);
  430. if (!isNaN(bubbleW) && !isNaN(bubbleH) &&
  431. block.comment && block.comment.setVisible) {
  432. block.comment.setBubbleSize(bubbleW, bubbleH);
  433. }
  434. break;
  435. case 'data':
  436. block.data = xmlChild.textContent;
  437. break;
  438. case 'title':
  439. // Titles were renamed to field in December 2013.
  440. // Fall through.
  441. case 'field':
  442. var field = block.getField(name);
  443. if (!field) {
  444. console.warn('Ignoring non-existent field ' + name + ' in block ' +
  445. prototypeName);
  446. break;
  447. }
  448. field.setValue(xmlChild.textContent);
  449. break;
  450. case 'value':
  451. case 'statement':
  452. input = block.getInput(name);
  453. if (!input) {
  454. console.warn('Ignoring non-existent input ' + name + ' in block ' +
  455. prototypeName);
  456. break;
  457. }
  458. if (childShadowNode) {
  459. input.connection.setShadowDom(childShadowNode);
  460. }
  461. if (childBlockNode) {
  462. blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode,
  463. workspace);
  464. if (blockChild.outputConnection) {
  465. input.connection.connect(blockChild.outputConnection);
  466. } else if (blockChild.previousConnection) {
  467. input.connection.connect(blockChild.previousConnection);
  468. } else {
  469. goog.asserts.fail(
  470. 'Child block does not have output or previous statement.');
  471. }
  472. }
  473. break;
  474. case 'next':
  475. if (childShadowNode && block.nextConnection) {
  476. block.nextConnection.setShadowDom(childShadowNode);
  477. }
  478. if (childBlockNode) {
  479. goog.asserts.assert(block.nextConnection,
  480. 'Next statement does not exist.');
  481. // If there is more than one XML 'next' tag.
  482. goog.asserts.assert(!block.nextConnection.isConnected(),
  483. 'Next statement is already connected.');
  484. blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode,
  485. workspace);
  486. goog.asserts.assert(blockChild.previousConnection,
  487. 'Next block does not have previous statement.');
  488. block.nextConnection.connect(blockChild.previousConnection);
  489. }
  490. break;
  491. default:
  492. // Unknown tag; ignore. Same principle as HTML parsers.
  493. console.warn('Ignoring unknown tag: ' + xmlChild.nodeName);
  494. }
  495. }
  496. var inline = xmlBlock.getAttribute('inline');
  497. if (inline) {
  498. block.setInputsInline(inline == 'true');
  499. }
  500. var disabled = xmlBlock.getAttribute('disabled');
  501. if (disabled) {
  502. block.setDisabled(disabled == 'true');
  503. }
  504. var deletable = xmlBlock.getAttribute('deletable');
  505. if (deletable) {
  506. block.setDeletable(deletable == 'true');
  507. }
  508. var movable = xmlBlock.getAttribute('movable');
  509. if (movable) {
  510. block.setMovable(movable == 'true');
  511. }
  512. var editable = xmlBlock.getAttribute('editable');
  513. if (editable) {
  514. block.setEditable(editable == 'true');
  515. }
  516. var lineNumber = xmlBlock.getAttribute('line_number');
  517. if (lineNumber) {
  518. block.setLineNumber(lineNumber);
  519. }
  520. //console.log("LN:", lineNumber);
  521. var collapsed = xmlBlock.getAttribute('collapsed');
  522. if (collapsed) {
  523. block.setCollapsed(collapsed == 'true');
  524. }
  525. if (xmlBlock.nodeName.toLowerCase() == 'shadow') {
  526. // Ensure all children are also shadows.
  527. var children = block.getChildren();
  528. for (var i = 0, child; child = children[i]; i++) {
  529. goog.asserts.assert(child.isShadow(),
  530. 'Shadow block not allowed non-shadow child.');
  531. }
  532. // Ensure this block doesn't have any variable inputs.
  533. goog.asserts.assert(block.getVars().length == 0,
  534. 'Shadow blocks cannot have variable fields.');
  535. block.setShadow(true);
  536. }
  537. return block;
  538. };
  539. /**
  540. * Remove any 'next' block (statements in a stack).
  541. * @param {!Element} xmlBlock XML block element.
  542. */
  543. Blockly.Xml.deleteNext = function(xmlBlock) {
  544. for (var i = 0, child; child = xmlBlock.childNodes[i]; i++) {
  545. if (child.nodeName.toLowerCase() == 'next') {
  546. xmlBlock.removeChild(child);
  547. break;
  548. }
  549. }
  550. };
  551. // Export symbols that would otherwise be renamed by Closure compiler.
  552. if (!goog.global['Blockly']) {
  553. goog.global['Blockly'] = {};
  554. }
  555. if (!goog.global['Blockly']['Xml']) {
  556. goog.global['Blockly']['Xml'] = {};
  557. }
  558. goog.global['Blockly']['Xml']['domToText'] = Blockly.Xml.domToText;
  559. goog.global['Blockly']['Xml']['domToWorkspace'] = Blockly.Xml.domToWorkspace;
  560. goog.global['Blockly']['Xml']['textToDom'] = Blockly.Xml.textToDom;
  561. goog.global['Blockly']['Xml']['workspaceToDom'] = Blockly.Xml.workspaceToDom;