utils.js 24 KB

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