utils.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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 Utility methods.
  22. * These methods are not specific to Blockly, and could be factored out into
  23. * a JavaScript framework such as Closure.
  24. * @author fraser@google.com (Neil Fraser)
  25. */
  26. 'use strict';
  27. goog.provide('Blockly.utils');
  28. goog.require('goog.dom');
  29. goog.require('goog.events.BrowserFeature');
  30. goog.require('goog.math.Coordinate');
  31. goog.require('goog.userAgent');
  32. /**
  33. * Add a CSS class to a element.
  34. * Similar to Closure's goog.dom.classes.add, except it handles SVG elements.
  35. * @param {!Element} element DOM element to add class to.
  36. * @param {string} className Name of class to add.
  37. * @private
  38. */
  39. Blockly.addClass_ = function(element, className) {
  40. var classes = element.getAttribute('class') || '';
  41. if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) {
  42. if (classes) {
  43. classes += ' ';
  44. }
  45. element.setAttribute('class', classes + className);
  46. }
  47. };
  48. /**
  49. * Remove a CSS class from a element.
  50. * Similar to Closure's goog.dom.classes.remove, except it handles SVG elements.
  51. * @param {!Element} element DOM element to remove class from.
  52. * @param {string} className Name of class to remove.
  53. * @private
  54. */
  55. Blockly.removeClass_ = function(element, className) {
  56. var classes = element.getAttribute('class');
  57. if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) {
  58. var classList = classes.split(/\s+/);
  59. for (var i = 0; i < classList.length; i++) {
  60. if (!classList[i] || classList[i] == className) {
  61. classList.splice(i, 1);
  62. i--;
  63. }
  64. }
  65. if (classList.length) {
  66. element.setAttribute('class', classList.join(' '));
  67. } else {
  68. element.removeAttribute('class');
  69. }
  70. }
  71. };
  72. /**
  73. * Checks if an element has the specified CSS class.
  74. * Similar to Closure's goog.dom.classes.has, except it handles SVG elements.
  75. * @param {!Element} element DOM element to check.
  76. * @param {string} className Name of class to check.
  77. * @return {boolean} True if class exists, false otherwise.
  78. * @private
  79. */
  80. Blockly.hasClass_ = function(element, className) {
  81. var classes = element.getAttribute('class');
  82. return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1;
  83. };
  84. /**
  85. * Bind an event to a function call.
  86. * @param {!Node} node Node upon which to listen.
  87. * @param {string} name Event name to listen to (e.g. 'mousedown').
  88. * @param {Object} thisObject The value of 'this' in the function.
  89. * @param {!Function} func Function to call when event is triggered.
  90. * @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
  91. * @private
  92. */
  93. Blockly.bindEvent_ = function(node, name, thisObject, func) {
  94. if (thisObject) {
  95. var wrapFunc = function(e) {
  96. func.call(thisObject, e);
  97. };
  98. } else {
  99. var wrapFunc = func;
  100. }
  101. node.addEventListener(name, wrapFunc, false);
  102. var bindData = [[node, name, wrapFunc]];
  103. // Add equivalent touch event.
  104. if (name in Blockly.bindEvent_.TOUCH_MAP) {
  105. wrapFunc = function(e) {
  106. // Punt on multitouch events.
  107. if (e.changedTouches.length == 1) {
  108. // Map the touch event's properties to the event.
  109. var touchPoint = e.changedTouches[0];
  110. e.clientX = touchPoint.clientX;
  111. e.clientY = touchPoint.clientY;
  112. }
  113. func.call(thisObject, e);
  114. // Stop the browser from scrolling/zooming the page.
  115. e.preventDefault();
  116. };
  117. for (var i = 0, eventName;
  118. eventName = Blockly.bindEvent_.TOUCH_MAP[name][i]; i++) {
  119. node.addEventListener(eventName, wrapFunc, false);
  120. bindData.push([node, eventName, wrapFunc]);
  121. }
  122. }
  123. return bindData;
  124. };
  125. /**
  126. * The TOUCH_MAP lookup dictionary specifies additional touch events to fire,
  127. * in conjunction with mouse events.
  128. * @type {Object}
  129. */
  130. Blockly.bindEvent_.TOUCH_MAP = {};
  131. if (goog.events.BrowserFeature.TOUCH_ENABLED) {
  132. Blockly.bindEvent_.TOUCH_MAP = {
  133. 'mousedown': ['touchstart'],
  134. 'mousemove': ['touchmove'],
  135. 'mouseup': ['touchend', 'touchcancel']
  136. };
  137. }
  138. /**
  139. * Unbind one or more events event from a function call.
  140. * @param {!Array.<!Array>} bindData Opaque data from bindEvent_. This list is
  141. * emptied during the course of calling this function.
  142. * @return {!Function} The function call.
  143. * @private
  144. */
  145. Blockly.unbindEvent_ = function(bindData) {
  146. while (bindData.length) {
  147. var bindDatum = bindData.pop();
  148. var node = bindDatum[0];
  149. var name = bindDatum[1];
  150. var func = bindDatum[2];
  151. node.removeEventListener(name, func, false);
  152. }
  153. return func;
  154. };
  155. /**
  156. * Don't do anything for this event, just halt propagation.
  157. * @param {!Event} e An event.
  158. */
  159. Blockly.noEvent = function(e) {
  160. // This event has been handled. No need to bubble up to the document.
  161. e.preventDefault();
  162. e.stopPropagation();
  163. };
  164. /**
  165. * Is this event targeting a text input widget?
  166. * @param {!Event} e An event.
  167. * @return {boolean} True if text input.
  168. * @private
  169. */
  170. Blockly.isTargetInput_ = function(e) {
  171. return e.target.type == 'textarea' || e.target.type == 'text' ||
  172. e.target.type == 'number' || e.target.type == 'email' ||
  173. e.target.type == 'password' || e.target.type == 'search' ||
  174. e.target.type == 'tel' || e.target.type == 'url' ||
  175. e.target.isContentEditable;
  176. };
  177. /**
  178. * Return the coordinates of the top-left corner of this element relative to
  179. * its parent. Only for SVG elements and children (e.g. rect, g, path).
  180. * @param {!Element} element SVG element to find the coordinates of.
  181. * @return {!goog.math.Coordinate} Object with .x and .y properties.
  182. * @private
  183. */
  184. Blockly.getRelativeXY_ = function(element) {
  185. var xy = new goog.math.Coordinate(0, 0);
  186. // First, check for x and y attributes.
  187. var x = element.getAttribute('x');
  188. if (x) {
  189. xy.x = parseInt(x, 10);
  190. }
  191. var y = element.getAttribute('y');
  192. if (y) {
  193. xy.y = parseInt(y, 10);
  194. }
  195. // Second, check for transform="translate(...)" attribute.
  196. var transform = element.getAttribute('transform');
  197. var r = transform && transform.match(Blockly.getRelativeXY_.XY_REGEXP_);
  198. if (r) {
  199. xy.x += parseFloat(r[1]);
  200. if (r[3]) {
  201. xy.y += parseFloat(r[3]);
  202. }
  203. }
  204. return xy;
  205. };
  206. /**
  207. * Static regex to pull the x,y values out of an SVG translate() directive.
  208. * Note that Firefox and IE (9,10) return 'translate(12)' instead of
  209. * 'translate(12, 0)'.
  210. * Note that IE (9,10) returns 'translate(16 8)' instead of 'translate(16, 8)'.
  211. * Note that IE has been reported to return scientific notation (0.123456e-42).
  212. * @type {!RegExp}
  213. * @private
  214. */
  215. Blockly.getRelativeXY_.XY_REGEXP_ =
  216. /translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*\))?/;
  217. /**
  218. * Return the absolute coordinates of the top-left corner of this element,
  219. * scales that after canvas SVG element, if it's a descendant.
  220. * The origin (0,0) is the top-left corner of the Blockly SVG.
  221. * @param {!Element} element Element to find the coordinates of.
  222. * @param {!Blockly.Workspace} workspace Element must be in this workspace.
  223. * @return {!goog.math.Coordinate} Object with .x and .y properties.
  224. * @private
  225. */
  226. Blockly.getSvgXY_ = function(element, workspace) {
  227. var x = 0;
  228. var y = 0;
  229. var scale = 1;
  230. if (goog.dom.contains(workspace.getCanvas(), element) ||
  231. goog.dom.contains(workspace.getBubbleCanvas(), element)) {
  232. // Before the SVG canvas, scale the coordinates.
  233. scale = workspace.scale;
  234. }
  235. do {
  236. // Loop through this block and every parent.
  237. var xy = Blockly.getRelativeXY_(element);
  238. if (element == workspace.getCanvas() ||
  239. element == workspace.getBubbleCanvas()) {
  240. // After the SVG canvas, don't scale the coordinates.
  241. scale = 1;
  242. }
  243. x += xy.x * scale;
  244. y += xy.y * scale;
  245. element = element.parentNode;
  246. } while (element && element != workspace.getParentSvg());
  247. return new goog.math.Coordinate(x, y);
  248. };
  249. /**
  250. * Helper method for creating SVG elements.
  251. * @param {string} name Element's tag name.
  252. * @param {!Object} attrs Dictionary of attribute names and values.
  253. * @param {Element} parent Optional parent on which to append the element.
  254. * @param {Blockly.Workspace=} opt_workspace Optional workspace for access to
  255. * context (scale...).
  256. * @return {!SVGElement} Newly created SVG element.
  257. */
  258. Blockly.createSvgElement = function(name, attrs, parent, opt_workspace) {
  259. var e = /** @type {!SVGElement} */ (
  260. document.createElementNS(Blockly.SVG_NS, name));
  261. for (var key in attrs) {
  262. e.setAttribute(key, attrs[key]);
  263. }
  264. // IE defines a unique attribute "runtimeStyle", it is NOT applied to
  265. // elements created with createElementNS. However, Closure checks for IE
  266. // and assumes the presence of the attribute and crashes.
  267. if (document.body.runtimeStyle) { // Indicates presence of IE-only attr.
  268. e.runtimeStyle = e.currentStyle = e.style;
  269. }
  270. if (parent) {
  271. parent.appendChild(e);
  272. }
  273. return e;
  274. };
  275. /**
  276. * Is this event a right-click?
  277. * @param {!Event} e Mouse event.
  278. * @return {boolean} True if right-click.
  279. */
  280. Blockly.isRightButton = function(e) {
  281. if (e.ctrlKey && goog.userAgent.MAC) {
  282. // Control-clicking on Mac OS X is treated as a right-click.
  283. // WebKit on Mac OS X fails to change button to 2 (but Gecko does).
  284. return true;
  285. }
  286. return e.button == 2;
  287. };
  288. /**
  289. * Return the converted coordinates of the given mouse event.
  290. * The origin (0,0) is the top-left corner of the Blockly svg.
  291. * @param {!Event} e Mouse event.
  292. * @param {!Element} svg SVG element.
  293. * @param {SVGMatrix} matrix Inverted screen CTM to use.
  294. * @return {!Object} Object with .x and .y properties.
  295. */
  296. Blockly.mouseToSvg = function(e, svg, matrix) {
  297. var svgPoint = svg.createSVGPoint();
  298. svgPoint.x = e.clientX;
  299. svgPoint.y = e.clientY;
  300. if (!matrix) {
  301. matrix = svg.getScreenCTM().inverse();
  302. }
  303. return svgPoint.matrixTransform(matrix);
  304. };
  305. /**
  306. * Given an array of strings, return the length of the shortest one.
  307. * @param {!Array.<string>} array Array of strings.
  308. * @return {number} Length of shortest string.
  309. */
  310. Blockly.shortestStringLength = function(array) {
  311. if (!array.length) {
  312. return 0;
  313. }
  314. var len = array[0].length;
  315. for (var i = 1; i < array.length; i++) {
  316. len = Math.min(len, array[i].length);
  317. }
  318. return len;
  319. };
  320. /**
  321. * Given an array of strings, return the length of the common prefix.
  322. * Words may not be split. Any space after a word is included in the length.
  323. * @param {!Array.<string>} array Array of strings.
  324. * @param {number=} opt_shortest Length of shortest string.
  325. * @return {number} Length of common prefix.
  326. */
  327. Blockly.commonWordPrefix = function(array, opt_shortest) {
  328. if (!array.length) {
  329. return 0;
  330. } else if (array.length == 1) {
  331. return array[0].length;
  332. }
  333. var wordPrefix = 0;
  334. var max = opt_shortest || Blockly.shortestStringLength(array);
  335. for (var len = 0; len < max; len++) {
  336. var letter = array[0][len];
  337. for (var i = 1; i < array.length; i++) {
  338. if (letter != array[i][len]) {
  339. return wordPrefix;
  340. }
  341. }
  342. if (letter == ' ') {
  343. wordPrefix = len + 1;
  344. }
  345. }
  346. for (var i = 1; i < array.length; i++) {
  347. var letter = array[i][len];
  348. if (letter && letter != ' ') {
  349. return wordPrefix;
  350. }
  351. }
  352. return max;
  353. };
  354. /**
  355. * Given an array of strings, return the length of the common suffix.
  356. * Words may not be split. Any space after a word is included in the length.
  357. * @param {!Array.<string>} array Array of strings.
  358. * @param {number=} opt_shortest Length of shortest string.
  359. * @return {number} Length of common suffix.
  360. */
  361. Blockly.commonWordSuffix = function(array, opt_shortest) {
  362. if (!array.length) {
  363. return 0;
  364. } else if (array.length == 1) {
  365. return array[0].length;
  366. }
  367. var wordPrefix = 0;
  368. var max = opt_shortest || Blockly.shortestStringLength(array);
  369. for (var len = 0; len < max; len++) {
  370. var letter = array[0].substr(-len - 1, 1);
  371. for (var i = 1; i < array.length; i++) {
  372. if (letter != array[i].substr(-len - 1, 1)) {
  373. return wordPrefix;
  374. }
  375. }
  376. if (letter == ' ') {
  377. wordPrefix = len + 1;
  378. }
  379. }
  380. for (var i = 1; i < array.length; i++) {
  381. var letter = array[i].charAt(array[i].length - len - 1);
  382. if (letter && letter != ' ') {
  383. return wordPrefix;
  384. }
  385. }
  386. return max;
  387. };
  388. /**
  389. * Is the given string a number (includes negative and decimals).
  390. * @param {string} str Input string.
  391. * @return {boolean} True if number, false otherwise.
  392. */
  393. Blockly.isNumber = function(str) {
  394. return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/);
  395. };
  396. /**
  397. * Parse a string with any number of interpolation tokens (%1, %2, ...).
  398. * '%' characters may be self-escaped (%%).
  399. * @param {string} message Text containing interpolation tokens.
  400. * @return {!Array.<string|number>} Array of strings and numbers.
  401. */
  402. Blockly.utils.tokenizeInterpolation = function(message) {
  403. var tokens = [];
  404. var chars = message.split('');
  405. chars.push(''); // End marker.
  406. // Parse the message with a finite state machine.
  407. // 0 - Base case.
  408. // 1 - % found.
  409. // 2 - Digit found.
  410. var state = 0;
  411. var buffer = [];
  412. var number = null;
  413. for (var i = 0; i < chars.length; i++) {
  414. var c = chars[i];
  415. if (state == 0) {
  416. if (c == '%') {
  417. state = 1; // Start escape.
  418. } else {
  419. buffer.push(c); // Regular char.
  420. }
  421. } else if (state == 1) {
  422. if (c == '%') {
  423. buffer.push(c); // Escaped %: %%
  424. state = 0;
  425. } else if ('0' <= c && c <= '9') {
  426. state = 2;
  427. number = c;
  428. var text = buffer.join('');
  429. if (text) {
  430. tokens.push(text);
  431. }
  432. buffer.length = 0;
  433. } else {
  434. buffer.push('%', c); // Not an escape: %a
  435. state = 0;
  436. }
  437. } else if (state == 2) {
  438. if ('0' <= c && c <= '9') {
  439. number += c; // Multi-digit number.
  440. } else {
  441. tokens.push(parseInt(number, 10));
  442. i--; // Parse this char again.
  443. state = 0;
  444. }
  445. }
  446. }
  447. var text = buffer.join('');
  448. if (text) {
  449. tokens.push(text);
  450. }
  451. return tokens;
  452. };
  453. /**
  454. * Generate a unique ID. This should be globally unique.
  455. * 87 characters ^ 20 length > 128 bits (better than a UUID).
  456. * @return {string} A globally unique ID string.
  457. */
  458. Blockly.genUid = function() {
  459. var length = 20;
  460. var soupLength = Blockly.genUid.soup_.length;
  461. var id = [];
  462. for (var i = 0; i < length; i++) {
  463. id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength);
  464. }
  465. return id.join('');
  466. };
  467. /**
  468. * Legal characters for the unique ID.
  469. * Should be all on a US keyboard. No XML special characters or control codes.
  470. * Removed $ due to issue 251.
  471. * @private
  472. */
  473. Blockly.genUid.soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' +
  474. 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  475. /**
  476. * Wrap text to the specified width.
  477. * @param {string} text Text to wrap.
  478. * @param {number} limit Width to wrap each line.
  479. * @return {string} Wrapped text.
  480. */
  481. Blockly.utils.wrap = function(text, limit) {
  482. var lines = text.split('\n');
  483. for (var i = 0; i < lines.length; i++) {
  484. lines[i] = Blockly.utils.wrap_line_(lines[i], limit);
  485. }
  486. return lines.join('\n');
  487. };
  488. /**
  489. * Wrap single line of text to the specified width.
  490. * @param {string} text Text to wrap.
  491. * @param {number} limit Width to wrap each line.
  492. * @return {string} Wrapped text.
  493. * @private
  494. */
  495. Blockly.utils.wrap_line_ = function(text, limit) {
  496. if (text.length <= limit) {
  497. // Short text, no need to wrap.
  498. return text;
  499. }
  500. // Split the text into words.
  501. var words = text.trim().split(/\s+/);
  502. // Set limit to be the length of the largest word.
  503. for (var i = 0; i < words.length; i++) {
  504. if (words[i].length > limit) {
  505. limit = words[i].length;
  506. }
  507. }
  508. var lastScore;
  509. var score = -Infinity;
  510. var lastText;
  511. var lineCount = 1;
  512. do {
  513. lastScore = score;
  514. lastText = text;
  515. // Create a list of booleans representing if a space (false) or
  516. // a break (true) appears after each word.
  517. var wordBreaks = [];
  518. // Seed the list with evenly spaced linebreaks.
  519. var steps = words.length / lineCount;
  520. var insertedBreaks = 1;
  521. for (var i = 0; i < words.length - 1; i++) {
  522. if (insertedBreaks < (i + 1.5) / steps) {
  523. insertedBreaks++;
  524. wordBreaks[i] = true;
  525. } else {
  526. wordBreaks[i] = false;
  527. }
  528. }
  529. wordBreaks = Blockly.utils.wrapMutate_(words, wordBreaks, limit);
  530. score = Blockly.utils.wrapScore_(words, wordBreaks, limit);
  531. text = Blockly.utils.wrapToText_(words, wordBreaks);
  532. lineCount++;
  533. } while (score > lastScore);
  534. return lastText;
  535. };
  536. /**
  537. * Compute a score for how good the wrapping is.
  538. * @param {!Array.<string>} words Array of each word.
  539. * @param {!Array.<boolean>} wordBreaks Array of line breaks.
  540. * @param {number} limit Width to wrap each line.
  541. * @return {number} Larger the better.
  542. * @private
  543. */
  544. Blockly.utils.wrapScore_ = function(words, wordBreaks, limit) {
  545. // If this function becomes a performance liability, add caching.
  546. // Compute the length of each line.
  547. var lineLengths = [0];
  548. var linePunctuation = [];
  549. for (var i = 0; i < words.length; i++) {
  550. lineLengths[lineLengths.length - 1] += words[i].length;
  551. if (wordBreaks[i] === true) {
  552. lineLengths.push(0);
  553. linePunctuation.push(words[i].charAt(words[i].length - 1));
  554. } else if (wordBreaks[i] === false) {
  555. lineLengths[lineLengths.length - 1]++;
  556. }
  557. }
  558. var maxLength = Math.max.apply(Math, lineLengths);
  559. var score = 0;
  560. for (var i = 0; i < lineLengths.length; i++) {
  561. // Optimize for width.
  562. // -2 points per char over limit (scaled to the power of 1.5).
  563. score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2;
  564. // Optimize for even lines.
  565. // -1 point per char smaller than max (scaled to the power of 1.5).
  566. score -= Math.pow(maxLength - lineLengths[i], 1.5);
  567. // Optimize for structure.
  568. // Add score to line endings after punctuation.
  569. if ('.?!'.indexOf(linePunctuation[i]) != -1) {
  570. score += limit / 3;
  571. } else if (',;)]}'.indexOf(linePunctuation[i]) != -1) {
  572. score += limit / 4;
  573. }
  574. }
  575. // All else being equal, the last line should not be longer than the
  576. // previous line. For example, this looks wrong:
  577. // aaa bbb
  578. // ccc ddd eee
  579. if (lineLengths.length > 1 && lineLengths[lineLengths.length - 1] <=
  580. lineLengths[lineLengths.length - 2]) {
  581. score += 0.5;
  582. }
  583. return score;
  584. };
  585. /**
  586. * Mutate the array of line break locations until an optimal solution is found.
  587. * No line breaks are added or deleted, they are simply moved around.
  588. * @param {!Array.<string>} words Array of each word.
  589. * @param {!Array.<boolean>} wordBreaks Array of line breaks.
  590. * @param {number} limit Width to wrap each line.
  591. * @return {!Array.<boolean>} New array of optimal line breaks.
  592. * @private
  593. */
  594. Blockly.utils.wrapMutate_ = function(words, wordBreaks, limit) {
  595. var bestScore = Blockly.utils.wrapScore_(words, wordBreaks, limit);
  596. var bestBreaks;
  597. // Try shifting every line break forward or backward.
  598. for (var i = 0; i < wordBreaks.length - 1; i++) {
  599. if (wordBreaks[i] == wordBreaks[i + 1]) {
  600. continue;
  601. }
  602. var mutatedWordBreaks = [].concat(wordBreaks);
  603. mutatedWordBreaks[i] = !mutatedWordBreaks[i];
  604. mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1];
  605. var mutatedScore =
  606. Blockly.utils.wrapScore_(words, mutatedWordBreaks, limit);
  607. if (mutatedScore > bestScore) {
  608. bestScore = mutatedScore;
  609. bestBreaks = mutatedWordBreaks;
  610. }
  611. }
  612. if (bestBreaks) {
  613. // Found an improvement. See if it may be improved further.
  614. return Blockly.utils.wrapMutate_(words, bestBreaks, limit);
  615. }
  616. // No improvements found. Done.
  617. return wordBreaks;
  618. };
  619. /**
  620. * Reassemble the array of words into text, with the specified line breaks.
  621. * @param {!Array.<string>} words Array of each word.
  622. * @param {!Array.<boolean>} wordBreaks Array of line breaks.
  623. * @return {string} Plain text.
  624. * @private
  625. */
  626. Blockly.utils.wrapToText_ = function(words, wordBreaks) {
  627. var text = [];
  628. for (var i = 0; i < words.length; i++) {
  629. text.push(words[i]);
  630. if (wordBreaks[i] !== undefined) {
  631. text.push(wordBreaks[i] ? '\n' : ' ');
  632. }
  633. }
  634. return text.join('');
  635. };