PageRenderTime 51ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/resources/public/js/soy/soyutils.js

http://github.com/fmw/vix
JavaScript | 880 lines | 359 code | 115 blank | 406 comment | 68 complexity | 937ff83928f510ae37d726c8f86182e5 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright 2008 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. // Utility functions and classes for Soy.
  17. //
  18. // The top portion of this file contains utilities for Soy users:
  19. // + soy.StringBuilder: Compatible with the 'stringbuilder' code style.
  20. // + soy.renderElement: Render template and set as innerHTML of an element.
  21. // + soy.renderAsFragment: Render template and return as HTML fragment.
  22. //
  23. // The bottom portion of this file contains utilities that should only be called
  24. // by Soy-generated JS code. Please do not use these functions directly from
  25. // your hand-writen code. Their names all start with '$$'.
  26. /**
  27. * Base name for the soy utilities, when used outside of Closure Library.
  28. * Check to see soy is already defined in the current scope before asigning to
  29. * prevent clobbering if soyutils.js is loaded more than once.
  30. * @type {Object}
  31. */
  32. var soy = soy || {};
  33. // Just enough browser detection for this file.
  34. (function() {
  35. var ua = navigator.userAgent;
  36. var isOpera = ua.indexOf('Opera') == 0;
  37. /**
  38. * @type {boolean}
  39. * @private
  40. */
  41. soy.IS_OPERA_ = isOpera;
  42. /**
  43. * @type {boolean}
  44. * @private
  45. */
  46. soy.IS_IE_ = !isOpera && ua.indexOf('MSIE') != -1;
  47. /**
  48. * @type {boolean}
  49. * @private
  50. */
  51. soy.IS_WEBKIT_ = !isOpera && ua.indexOf('WebKit') != -1;
  52. })();
  53. // -----------------------------------------------------------------------------
  54. // StringBuilder (compatible with the 'stringbuilder' code style).
  55. /**
  56. * Utility class to facilitate much faster string concatenation in IE,
  57. * using Array.join() rather than the '+' operator. For other browsers
  58. * we simply use the '+' operator.
  59. *
  60. * @param {Object|number|string|boolean=} opt_a1 Optional first initial item
  61. * to append.
  62. * @param {Object|number|string|boolean} var_args Other initial items to
  63. * append, e.g., new soy.StringBuilder('foo', 'bar').
  64. * @constructor
  65. */
  66. soy.StringBuilder = function(opt_a1, var_args) {
  67. /**
  68. * Internal buffer for the string to be concatenated.
  69. * @type {string|Array}
  70. * @private
  71. */
  72. this.buffer_ = soy.IS_IE_ ? [] : '';
  73. if (opt_a1 != null) {
  74. this.append.apply(this, arguments);
  75. }
  76. };
  77. /**
  78. * Length of internal buffer (faster than calling buffer_.length).
  79. * Only used for IE.
  80. * @type {number}
  81. * @private
  82. */
  83. soy.StringBuilder.prototype.bufferLength_ = 0;
  84. /**
  85. * Appends one or more items to the string.
  86. *
  87. * Calling this with null, undefined, or empty arguments is an error.
  88. *
  89. * @param {Object|number|string|boolean} a1 Required first string.
  90. * @param {Object|number|string|boolean=} opt_a2 Optional second string.
  91. * @param {Object|number|string|boolean} var_args Other items to append,
  92. * e.g., sb.append('foo', 'bar', 'baz').
  93. * @return {soy.StringBuilder} This same StringBuilder object.
  94. */
  95. soy.StringBuilder.prototype.append = function(a1, opt_a2, var_args) {
  96. if (soy.IS_IE_) {
  97. if (opt_a2 == null) { // no second argument (note: undefined == null)
  98. // Array assignment is 2x faster than Array push. Also, use a1
  99. // directly to avoid arguments instantiation, another 2x improvement.
  100. this.buffer_[this.bufferLength_++] = a1;
  101. } else {
  102. this.buffer_.push.apply(this.buffer_, arguments);
  103. this.bufferLength_ = this.buffer_.length;
  104. }
  105. } else {
  106. // Use a1 directly to avoid arguments instantiation for single-arg case.
  107. this.buffer_ += a1;
  108. if (opt_a2 != null) { // no second argument (note: undefined == null)
  109. for (var i = 1; i < arguments.length; i++) {
  110. this.buffer_ += arguments[i];
  111. }
  112. }
  113. }
  114. return this;
  115. };
  116. /**
  117. * Clears the string.
  118. */
  119. soy.StringBuilder.prototype.clear = function() {
  120. if (soy.IS_IE_) {
  121. this.buffer_.length = 0; // reuse array to avoid creating new object
  122. this.bufferLength_ = 0;
  123. } else {
  124. this.buffer_ = '';
  125. }
  126. };
  127. /**
  128. * Returns the concatenated string.
  129. *
  130. * @return {string} The concatenated string.
  131. */
  132. soy.StringBuilder.prototype.toString = function() {
  133. if (soy.IS_IE_) {
  134. var str = this.buffer_.join('');
  135. // Given a string with the entire contents, simplify the StringBuilder by
  136. // setting its contents to only be this string, rather than many fragments.
  137. this.clear();
  138. if (str) {
  139. this.append(str);
  140. }
  141. return str;
  142. } else {
  143. return /** @type {string} */ (this.buffer_);
  144. }
  145. };
  146. // -----------------------------------------------------------------------------
  147. // Public utilities.
  148. /**
  149. * Helper function to render a Soy template and then set the output string as
  150. * the innerHTML of an element. It is recommended to use this helper function
  151. * instead of directly setting innerHTML in your hand-written code, so that it
  152. * will be easier to audit the code for cross-site scripting vulnerabilities.
  153. *
  154. * @param {Element} element The element whose content we are rendering.
  155. * @param {Function} template The Soy template defining the element's content.
  156. * @param {Object=} opt_templateData The data for the template.
  157. */
  158. soy.renderElement = function(element, template, opt_templateData) {
  159. element.innerHTML = template(opt_templateData);
  160. };
  161. /**
  162. * Helper function to render a Soy template into a single node or a document
  163. * fragment. If the rendered HTML string represents a single node, then that
  164. * node is returned. Otherwise a document fragment is returned containing the
  165. * rendered nodes.
  166. *
  167. * @param {Function} template The Soy template defining the element's content.
  168. * @param {Object=} opt_templateData The data for the template.
  169. * @return {Node} The resulting node or document fragment.
  170. */
  171. soy.renderAsFragment = function(template, opt_templateData) {
  172. var tempDiv = document.createElement('div');
  173. tempDiv.innerHTML = template(opt_templateData);
  174. if (tempDiv.childNodes.length == 1) {
  175. return tempDiv.firstChild;
  176. } else {
  177. var fragment = document.createDocumentFragment();
  178. while (tempDiv.firstChild) {
  179. fragment.appendChild(tempDiv.firstChild);
  180. }
  181. return fragment;
  182. }
  183. };
  184. // -----------------------------------------------------------------------------
  185. // Below are private utilities to be used by Soy-generated code only.
  186. /**
  187. * Builds an augmented data object to be passed when a template calls another,
  188. * and needs to pass both original data and additional params. The returned
  189. * object will contain both the original data and the additional params. If the
  190. * same key appears in both, then the value from the additional params will be
  191. * visible, while the value from the original data will be hidden. The original
  192. * data object will be used, but not modified.
  193. *
  194. * @param {!Object} origData The original data to pass.
  195. * @param {Object} additionalParams The additional params to pass.
  196. * @return {Object} An augmented data object containing both the original data
  197. * and the additional params.
  198. */
  199. soy.$$augmentData = function(origData, additionalParams) {
  200. // Create a new object whose '__proto__' field is set to origData.
  201. /** @constructor */
  202. function tempCtor() {};
  203. tempCtor.prototype = origData;
  204. var newData = new tempCtor();
  205. // Add the additional params to the new object.
  206. for (var key in additionalParams) {
  207. newData[key] = additionalParams[key];
  208. }
  209. return newData;
  210. };
  211. /**
  212. * Escapes HTML special characters in a string. Escapes double quote '"' in
  213. * addition to '&', '<', and '>' so that a string can be included in an HTML
  214. * tag attribute value within double quotes.
  215. *
  216. * @param {*} str The string to be escaped. Can be other types, but the value
  217. * will be coerced to a string.
  218. * @return {string} An escaped copy of the string.
  219. */
  220. soy.$$escapeHtml = function(str) {
  221. str = String(str);
  222. // This quick test helps in the case when there are no chars to replace, in
  223. // the worst case this makes barely a difference to the time taken.
  224. if (!soy.$$EscapeHtmlRe_.ALL_SPECIAL_CHARS.test(str)) {
  225. return str;
  226. }
  227. // Since we're only checking one char at a time, we use String.indexOf(),
  228. // which is faster than RegExp.test(). Important: Must replace '&' first!
  229. if (str.indexOf('&') != -1) {
  230. str = str.replace(soy.$$EscapeHtmlRe_.AMP, '&amp;');
  231. }
  232. if (str.indexOf('<') != -1) {
  233. str = str.replace(soy.$$EscapeHtmlRe_.LT, '&lt;');
  234. }
  235. if (str.indexOf('>') != -1) {
  236. str = str.replace(soy.$$EscapeHtmlRe_.GT, '&gt;');
  237. }
  238. if (str.indexOf('"') != -1) {
  239. str = str.replace(soy.$$EscapeHtmlRe_.QUOT, '&quot;');
  240. }
  241. return str;
  242. };
  243. /**
  244. * Regular expressions used within escapeHtml().
  245. * @enum {RegExp}
  246. * @private
  247. */
  248. soy.$$EscapeHtmlRe_ = {
  249. ALL_SPECIAL_CHARS: /[&<>\"]/,
  250. AMP: /&/g,
  251. LT: /</g,
  252. GT: />/g,
  253. QUOT: /\"/g
  254. };
  255. /**
  256. * Escapes characters in the string to make it a valid content for a JS string literal.
  257. *
  258. * @param {*} s The string to be escaped. Can be other types, but the value
  259. * will be coerced to a string.
  260. * @return {string} An escaped copy of the string.
  261. */
  262. soy.$$escapeJs = function(s) {
  263. s = String(s);
  264. var sb = [];
  265. for (var i = 0; i < s.length; i++) {
  266. sb[i] = soy.$$escapeChar(s.charAt(i));
  267. }
  268. return sb.join('');
  269. };
  270. /**
  271. * Takes a character and returns the escaped string for that character. For
  272. * example escapeChar(String.fromCharCode(15)) -> "\\x0E".
  273. * @param {string} c The character to escape.
  274. * @return {string} An escaped string representing {@code c}.
  275. */
  276. soy.$$escapeChar = function(c) {
  277. if (c in soy.$$escapeCharJs_) {
  278. return soy.$$escapeCharJs_[c];
  279. }
  280. var rv = c;
  281. var cc = c.charCodeAt(0);
  282. if (cc > 31 && cc < 127) {
  283. rv = c;
  284. } else {
  285. // tab is 9 but handled above
  286. if (cc < 256) {
  287. rv = '\\x';
  288. if (cc < 16 || cc > 256) {
  289. rv += '0';
  290. }
  291. } else {
  292. rv = '\\u';
  293. if (cc < 4096) { // \u1000
  294. rv += '0';
  295. }
  296. }
  297. rv += cc.toString(16).toUpperCase();
  298. }
  299. return soy.$$escapeCharJs_[c] = rv;
  300. };
  301. /**
  302. * Character mappings used internally for soy.$$escapeJs
  303. * @private
  304. * @type {Object}
  305. */
  306. soy.$$escapeCharJs_ = {
  307. '\b': '\\b',
  308. '\f': '\\f',
  309. '\n': '\\n',
  310. '\r': '\\r',
  311. '\t': '\\t',
  312. '\x0B': '\\x0B', // '\v' is not supported in JScript
  313. '"': '\\"',
  314. '\'': '\\\'',
  315. '\\': '\\\\'
  316. };
  317. /**
  318. * Escapes a string so that it can be safely included in a URI.
  319. *
  320. * @param {*} str The string to be escaped. Can be other types, but the value
  321. * will be coerced to a string.
  322. * @return {string} An escaped copy of the string.
  323. */
  324. soy.$$escapeUri = function(str) {
  325. str = String(str);
  326. // Checking if the search matches before calling encodeURIComponent avoids an
  327. // extra allocation in IE6. This adds about 10us time in FF and a similiar
  328. // over head in IE6 for lower working set apps, but for large working set
  329. // apps, it saves about 70us per call.
  330. if (!soy.$$ENCODE_URI_REGEXP_.test(str)) {
  331. return encodeURIComponent(str);
  332. } else {
  333. return str;
  334. }
  335. };
  336. /**
  337. * Regular expression used for determining if a string needs to be encoded.
  338. * @type {RegExp}
  339. * @private
  340. */
  341. soy.$$ENCODE_URI_REGEXP_ = /^[a-zA-Z0-9\-_.!~*'()]*$/;
  342. /**
  343. * Inserts word breaks ('wbr' tags) into a HTML string at a given interval. The
  344. * counter is reset if a space is encountered. Word breaks aren't inserted into
  345. * HTML tags or entities. Entites count towards the character count; HTML tags
  346. * do not.
  347. *
  348. * @param {*} str The HTML string to insert word breaks into. Can be other
  349. * types, but the value will be coerced to a string.
  350. * @param {number} maxCharsBetweenWordBreaks Maximum number of non-space
  351. * characters to allow before adding a word break.
  352. * @return {string} The string including word breaks.
  353. */
  354. soy.$$insertWordBreaks = function(str, maxCharsBetweenWordBreaks) {
  355. str = String(str);
  356. var resultArr = [];
  357. var resultArrLen = 0;
  358. // These variables keep track of important state while looping through str.
  359. var isInTag = false; // whether we're inside an HTML tag
  360. var isMaybeInEntity = false; // whether we might be inside an HTML entity
  361. var numCharsWithoutBreak = 0; // number of characters since last word break
  362. var flushIndex = 0; // index of first char not yet flushed to resultArr
  363. for (var i = 0, n = str.length; i < n; ++i) {
  364. var charCode = str.charCodeAt(i);
  365. // If hit maxCharsBetweenWordBreaks, and not space next, then add <wbr>.
  366. if (numCharsWithoutBreak >= maxCharsBetweenWordBreaks &&
  367. charCode != soy.$$CharCode_.SPACE) {
  368. resultArr[resultArrLen++] = str.substring(flushIndex, i);
  369. flushIndex = i;
  370. resultArr[resultArrLen++] = soy.WORD_BREAK_;
  371. numCharsWithoutBreak = 0;
  372. }
  373. if (isInTag) {
  374. // If inside an HTML tag and we see '>', it's the end of the tag.
  375. if (charCode == soy.$$CharCode_.GREATER_THAN) {
  376. isInTag = false;
  377. }
  378. } else if (isMaybeInEntity) {
  379. switch (charCode) {
  380. // If maybe inside an entity and we see ';', it's the end of the entity.
  381. // The entity that just ended counts as one char, so increment
  382. // numCharsWithoutBreak.
  383. case soy.$$CharCode_.SEMI_COLON:
  384. isMaybeInEntity = false;
  385. ++numCharsWithoutBreak;
  386. break;
  387. // If maybe inside an entity and we see '<', we weren't actually in an
  388. // entity. But now we're inside and HTML tag.
  389. case soy.$$CharCode_.LESS_THAN:
  390. isMaybeInEntity = false;
  391. isInTag = true;
  392. break;
  393. // If maybe inside an entity and we see ' ', we weren't actually in an
  394. // entity. Just correct the state and reset the numCharsWithoutBreak
  395. // since we just saw a space.
  396. case soy.$$CharCode_.SPACE:
  397. isMaybeInEntity = false;
  398. numCharsWithoutBreak = 0;
  399. break;
  400. }
  401. } else { // !isInTag && !isInEntity
  402. switch (charCode) {
  403. // When not within a tag or an entity and we see '<', we're now inside
  404. // an HTML tag.
  405. case soy.$$CharCode_.LESS_THAN:
  406. isInTag = true;
  407. break;
  408. // When not within a tag or an entity and we see '&', we might be inside
  409. // an entity.
  410. case soy.$$CharCode_.AMPERSAND:
  411. isMaybeInEntity = true;
  412. break;
  413. // When we see a space, reset the numCharsWithoutBreak count.
  414. case soy.$$CharCode_.SPACE:
  415. numCharsWithoutBreak = 0;
  416. break;
  417. // When we see a non-space, increment the numCharsWithoutBreak.
  418. default:
  419. ++numCharsWithoutBreak;
  420. break;
  421. }
  422. }
  423. }
  424. // Flush the remaining chars at the end of the string.
  425. resultArr[resultArrLen++] = str.substring(flushIndex);
  426. return resultArr.join('');
  427. };
  428. /**
  429. * Special characters used within insertWordBreaks().
  430. * @enum {number}
  431. * @private
  432. */
  433. soy.$$CharCode_ = {
  434. SPACE: 32, // ' '.charCodeAt(0)
  435. AMPERSAND: 38, // '&'.charCodeAt(0)
  436. SEMI_COLON: 59, // ';'.charCodeAt(0)
  437. LESS_THAN: 60, // '<'.charCodeAt(0)
  438. GREATER_THAN: 62 // '>'.charCodeAt(0)
  439. };
  440. /**
  441. * String inserted as a word break by insertWordBreaks(). Safari requires
  442. * <wbr></wbr>, Opera needs the 'shy' entity, though this will give a visible
  443. * hyphen at breaks. Other browsers just use <wbr>.
  444. * @type {string}
  445. * @private
  446. */
  447. soy.WORD_BREAK_ =
  448. soy.IS_WEBKIT_ ? '<wbr></wbr>' : soy.IS_OPERA_ ? '&shy;' : '<wbr>';
  449. /**
  450. * Converts \r\n, \r, and \n to <br>s
  451. * @param {*} str The string in which to convert newlines.
  452. * @return {string} A copy of {@code str} with converted newlines.
  453. */
  454. soy.$$changeNewlineToBr = function(str) {
  455. str = String(str);
  456. // This quick test helps in the case when there are no chars to replace, in
  457. // the worst case this makes barely a difference to the time taken.
  458. if (!soy.$$CHANGE_NEWLINE_TO_BR_RE_.test(str)) {
  459. return str;
  460. }
  461. return str.replace(/(\r\n|\r|\n)/g, '<br>');
  462. };
  463. /**
  464. * Regular expression used within $$changeNewlineToBr().
  465. * @type {RegExp}
  466. * @private
  467. */
  468. soy.$$CHANGE_NEWLINE_TO_BR_RE_ = /[\r\n]/;
  469. /**
  470. * Estimate the overall directionality of text. If opt_isHtml, makes sure to
  471. * ignore the LTR nature of the mark-up and escapes in text, making the logic
  472. * suitable for HTML and HTML-escaped text.
  473. * @param {string} text The text whose directionality is to be estimated.
  474. * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
  475. * Default: false.
  476. * @return {number} 1 if text is LTR, -1 if it is RTL, and 0 if it is neutral.
  477. */
  478. soy.$$bidiTextDir = function(text, opt_isHtml) {
  479. text = soy.$$bidiStripHtmlIfNecessary_(text, opt_isHtml);
  480. if (!text) {
  481. return 0;
  482. }
  483. return soy.$$bidiDetectRtlDirectionality_(text) ? -1 : 1;
  484. };
  485. /**
  486. * Returns "dir=ltr" or "dir=rtl", depending on text's estimated
  487. * directionality, if it is not the same as bidiGlobalDir.
  488. * Otherwise, returns the empty string.
  489. * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
  490. * in text, making the logic suitable for HTML and HTML-escaped text.
  491. * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
  492. * if rtl, 0 if unknown.
  493. * @param {string} text The text whose directionality is to be estimated.
  494. * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
  495. * Default: false.
  496. * @return {string} "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR
  497. * text in non-LTR context; else, the empty string.
  498. */
  499. soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
  500. var dir = soy.$$bidiTextDir(text, opt_isHtml);
  501. if (dir != bidiGlobalDir) {
  502. return dir < 0 ? 'dir=rtl' : dir > 0 ? 'dir=ltr' : '';
  503. }
  504. return '';
  505. };
  506. /**
  507. * Returns a Unicode BiDi mark matching bidiGlobalDir (LRM or RLM) if the
  508. * directionality or the exit directionality of text are opposite to
  509. * bidiGlobalDir. Otherwise returns the empty string.
  510. * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
  511. * in text, making the logic suitable for HTML and HTML-escaped text.
  512. * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
  513. * if rtl, 0 if unknown.
  514. * @param {string} text The text whose directionality is to be estimated.
  515. * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
  516. * Default: false.
  517. * @return {string} A Unicode bidi mark matching bidiGlobalDir, or
  518. * the empty string when text's overall and exit directionalities both match
  519. * bidiGlobalDir.
  520. */
  521. soy.$$bidiMarkAfter = function(bidiGlobalDir, text, opt_isHtml) {
  522. var dir = soy.$$bidiTextDir(text, opt_isHtml);
  523. return soy.$$bidiMarkAfterKnownDir(bidiGlobalDir, dir, text, opt_isHtml);
  524. };
  525. /**
  526. * Returns a Unicode BiDi mark matching bidiGlobalDir (LRM or RLM) if the
  527. * directionality or the exit directionality of text are opposite to
  528. * bidiGlobalDir. Otherwise returns the empty string.
  529. * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
  530. * in text, making the logic suitable for HTML and HTML-escaped text.
  531. * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
  532. * if rtl, 0 if unknown.
  533. * @param {number} dir text's directionality: 1 if ltr, -1 if rtl, 0 if unknown.
  534. * @param {string} text The text whose directionality is to be estimated.
  535. * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
  536. * Default: false.
  537. * @return {string} A Unicode bidi mark matching bidiGlobalDir, or
  538. * the empty string when text's overall and exit directionalities both match
  539. * bidiGlobalDir.
  540. */
  541. soy.$$bidiMarkAfterKnownDir = function(bidiGlobalDir, dir, text, opt_isHtml) {
  542. return (
  543. bidiGlobalDir > 0 && (dir < 0 ||
  544. soy.$$bidiIsRtlExitText_(text, opt_isHtml)) ? '\u200E' : // LRM
  545. bidiGlobalDir < 0 && (dir > 0 ||
  546. soy.$$bidiIsLtrExitText_(text, opt_isHtml)) ? '\u200F' : // RLM
  547. '');
  548. };
  549. /**
  550. * Strips str of any HTML mark-up and escapes. Imprecise in several ways, but
  551. * precision is not very important, since the result is only meant to be used
  552. * for directionality detection.
  553. * @param {string} str The string to be stripped.
  554. * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
  555. * Default: false.
  556. * @return {string} The stripped string.
  557. * @private
  558. */
  559. soy.$$bidiStripHtmlIfNecessary_ = function(str, opt_isHtml) {
  560. return opt_isHtml ? str.replace(soy.$$BIDI_HTML_SKIP_RE_, ' ') : str;
  561. };
  562. /**
  563. * Simplified regular expression for am HTML tag (opening or closing) or an HTML
  564. * escape - the things we want to skip over in order to ignore their ltr
  565. * characters.
  566. * @type {RegExp}
  567. * @private
  568. */
  569. soy.$$BIDI_HTML_SKIP_RE_ = /<[^>]*>|&[^;]+;/g;
  570. /**
  571. * Returns str wrapped in a <span dir=ltr|rtl> according to its directionality -
  572. * but only if that is neither neutral nor the same as the global context.
  573. * Otherwise, returns str unchanged.
  574. * Always treats str as HTML/HTML-escaped, i.e. ignores mark-up and escapes when
  575. * estimating str's directionality.
  576. * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
  577. * if rtl, 0 if unknown.
  578. * @param {*} str The string to be wrapped. Can be other types, but the value
  579. * will be coerced to a string.
  580. * @return {string} The wrapped string.
  581. */
  582. soy.$$bidiSpanWrap = function(bidiGlobalDir, str) {
  583. str = String(str);
  584. var textDir = soy.$$bidiTextDir(str, true);
  585. var reset = soy.$$bidiMarkAfterKnownDir(bidiGlobalDir, textDir, str, true);
  586. if (textDir > 0 && bidiGlobalDir <= 0) {
  587. str = '<span dir=ltr>' + str + '</span>';
  588. } else if (textDir < 0 && bidiGlobalDir >= 0) {
  589. str = '<span dir=rtl>' + str + '</span>';
  590. }
  591. return str + reset;
  592. };
  593. /**
  594. * Returns str wrapped in Unicode BiDi formatting characters according to its
  595. * directionality, i.e. either LRE or RLE at the beginning and PDF at the end -
  596. * but only if str's directionality is neither neutral nor the same as the
  597. * global context. Otherwise, returns str unchanged.
  598. * Always treats str as HTML/HTML-escaped, i.e. ignores mark-up and escapes when
  599. * estimating str's directionality.
  600. * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
  601. * if rtl, 0 if unknown.
  602. * @param {*} str The string to be wrapped. Can be other types, but the value
  603. * will be coerced to a string.
  604. * @return {string} The wrapped string.
  605. */
  606. soy.$$bidiUnicodeWrap = function(bidiGlobalDir, str) {
  607. str = String(str);
  608. var textDir = soy.$$bidiTextDir(str, true);
  609. var reset = soy.$$bidiMarkAfterKnownDir(bidiGlobalDir, textDir, str, true);
  610. if (textDir > 0 && bidiGlobalDir <= 0) {
  611. str = '\u202A' + str + '\u202C';
  612. } else if (textDir < 0 && bidiGlobalDir >= 0) {
  613. str = '\u202B' + str + '\u202C';
  614. }
  615. return str + reset;
  616. };
  617. /**
  618. * A practical pattern to identify strong LTR character. This pattern is not
  619. * theoretically correct according to unicode standard. It is simplified for
  620. * performance and small code size.
  621. * @type {string}
  622. * @private
  623. */
  624. soy.$$bidiLtrChars_ =
  625. 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' +
  626. '\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF';
  627. /**
  628. * A practical pattern to identify strong neutral and weak character. This
  629. * pattern is not theoretically correct according to unicode standard. It is
  630. * simplified for performance and small code size.
  631. * @type {string}
  632. * @private
  633. */
  634. soy.$$bidiNeutralChars_ =
  635. '\u0000-\u0020!-@[-`{-\u00BF\u00D7\u00F7\u02B9-\u02FF\u2000-\u2BFF';
  636. /**
  637. * A practical pattern to identify strong RTL character. This pattern is not
  638. * theoretically correct according to unicode standard. It is simplified for
  639. * performance and small code size.
  640. * @type {string}
  641. * @private
  642. */
  643. soy.$$bidiRtlChars_ = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';
  644. /**
  645. * Regular expressions to check if a piece of text is of RTL directionality
  646. * on first character with strong directionality.
  647. * @type {RegExp}
  648. * @private
  649. */
  650. soy.$$bidiRtlDirCheckRe_ = new RegExp(
  651. '^[^' + soy.$$bidiLtrChars_ + ']*[' + soy.$$bidiRtlChars_ + ']');
  652. /**
  653. * Regular expressions to check if a piece of text is of neutral directionality.
  654. * Url are considered as neutral.
  655. * @type {RegExp}
  656. * @private
  657. */
  658. soy.$$bidiNeutralDirCheckRe_ = new RegExp(
  659. '^[' + soy.$$bidiNeutralChars_ + ']*$|^http://');
  660. /**
  661. * Check the directionality of the a piece of text based on the first character
  662. * with strong directionality.
  663. * @param {string} str string being checked.
  664. * @return {boolean} return true if rtl directionality is being detected.
  665. * @private
  666. */
  667. soy.$$bidiIsRtlText_ = function(str) {
  668. return soy.$$bidiRtlDirCheckRe_.test(str);
  669. };
  670. /**
  671. * Check the directionality of the a piece of text based on the first character
  672. * with strong directionality.
  673. * @param {string} str string being checked.
  674. * @return {boolean} true if all characters have neutral directionality.
  675. * @private
  676. */
  677. soy.$$bidiIsNeutralText_ = function(str) {
  678. return soy.$$bidiNeutralDirCheckRe_.test(str);
  679. };
  680. /**
  681. * This constant controls threshold of rtl directionality.
  682. * @type {number}
  683. * @private
  684. */
  685. soy.$$bidiRtlDetectionThreshold_ = 0.40;
  686. /**
  687. * Returns the RTL ratio based on word count.
  688. * @param {string} str the string that need to be checked.
  689. * @return {number} the ratio of RTL words among all words with directionality.
  690. * @private
  691. */
  692. soy.$$bidiRtlWordRatio_ = function(str) {
  693. var rtlCount = 0;
  694. var totalCount = 0;
  695. var tokens = str.split(' ');
  696. for (var i = 0; i < tokens.length; i++) {
  697. if (soy.$$bidiIsRtlText_(tokens[i])) {
  698. rtlCount++;
  699. totalCount++;
  700. } else if (!soy.$$bidiIsNeutralText_(tokens[i])) {
  701. totalCount++;
  702. }
  703. }
  704. return totalCount == 0 ? 0 : rtlCount / totalCount;
  705. };
  706. /**
  707. * Check the directionality of a piece of text, return true if the piece of
  708. * text should be laid out in RTL direction.
  709. * @param {string} str The piece of text that need to be detected.
  710. * @return {boolean} true if this piece of text should be laid out in RTL.
  711. * @private
  712. */
  713. soy.$$bidiDetectRtlDirectionality_ = function(str) {
  714. return soy.$$bidiRtlWordRatio_(str) >
  715. soy.$$bidiRtlDetectionThreshold_;
  716. };
  717. /**
  718. * Regular expressions to check if the last strongly-directional character in a
  719. * piece of text is LTR.
  720. * @type {RegExp}
  721. * @private
  722. */
  723. soy.$$bidiLtrExitDirCheckRe_ = new RegExp(
  724. '[' + soy.$$bidiLtrChars_ + '][^' + soy.$$bidiRtlChars_ + ']*$');
  725. /**
  726. * Regular expressions to check if the last strongly-directional character in a
  727. * piece of text is RTL.
  728. * @type {RegExp}
  729. * @private
  730. */
  731. soy.$$bidiRtlExitDirCheckRe_ = new RegExp(
  732. '[' + soy.$$bidiRtlChars_ + '][^' + soy.$$bidiLtrChars_ + ']*$');
  733. /**
  734. * Check if the exit directionality a piece of text is LTR, i.e. if the last
  735. * strongly-directional character in the string is LTR.
  736. * @param {string} str string being checked.
  737. * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
  738. * Default: false.
  739. * @return {boolean} Whether LTR exit directionality was detected.
  740. * @private
  741. */
  742. soy.$$bidiIsLtrExitText_ = function(str, opt_isHtml) {
  743. str = soy.$$bidiStripHtmlIfNecessary_(str, opt_isHtml);
  744. return soy.$$bidiLtrExitDirCheckRe_.test(str);
  745. };
  746. /**
  747. * Check if the exit directionality a piece of text is RTL, i.e. if the last
  748. * strongly-directional character in the string is RTL.
  749. * @param {string} str string being checked.
  750. * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
  751. * Default: false.
  752. * @return {boolean} Whether RTL exit directionality was detected.
  753. * @private
  754. */
  755. soy.$$bidiIsRtlExitText_ = function(str, opt_isHtml) {
  756. str = soy.$$bidiStripHtmlIfNecessary_(str, opt_isHtml);
  757. return soy.$$bidiRtlExitDirCheckRe_.test(str);
  758. };