closure 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +12 -12
  3. data/closure-compiler/README.md +503 -0
  4. data/closure-compiler/compiler.jar +0 -0
  5. data/closure-templates/SoyToJsSrcCompiler.jar +0 -0
  6. data/closure-templates/soyutils.js +1593 -469
  7. data/closure-templates/soyutils_usegoog.js +1105 -223
  8. data/docs/SCRIPT.md +5 -5
  9. data/docs/closure/Closure/BeanShell.html +63 -54
  10. data/docs/closure/Closure/Compiler/Compilation.html +124 -107
  11. data/docs/closure/Closure/Compiler/Error.html +28 -21
  12. data/docs/closure/Closure/Compiler.html +81 -76
  13. data/docs/closure/Closure/FileResponse.html +113 -98
  14. data/docs/closure/Closure/Goog.html +264 -253
  15. data/docs/closure/Closure/Middleware.html +66 -55
  16. data/docs/closure/Closure/Script/NotFound.html +28 -21
  17. data/docs/closure/Closure/Script/RenderStackOverflow.html +28 -21
  18. data/docs/closure/Closure/Script.html +212 -203
  19. data/docs/closure/Closure/Server.html +100 -90
  20. data/docs/closure/Closure/ShowExceptions.html +63 -52
  21. data/docs/closure/Closure/Sources.html +254 -246
  22. data/docs/closure/Closure/Templates/Error.html +28 -21
  23. data/docs/closure/Closure/Templates.html +88 -80
  24. data/docs/closure/Closure.html +181 -163
  25. data/docs/closure/_index.html +42 -38
  26. data/docs/closure/class_list.html +19 -8
  27. data/docs/closure/css/full_list.css +4 -2
  28. data/docs/closure/css/style.css +68 -51
  29. data/docs/closure/file.LICENSE.html +24 -217
  30. data/docs/closure/file.README.html +54 -47
  31. data/docs/closure/file_list.html +20 -9
  32. data/docs/closure/frames.html +18 -5
  33. data/docs/closure/index.html +54 -47
  34. data/docs/closure/js/app.js +60 -46
  35. data/docs/closure/js/full_list.js +24 -10
  36. data/docs/closure/js/jquery.js +4 -16
  37. data/docs/closure/method_list.html +74 -175
  38. data/docs/closure/top-level-namespace.html +29 -20
  39. data/lib/closure/compiler.rb +32 -42
  40. data/lib/closure/goog.rb +12 -12
  41. data/lib/closure/server.rb +6 -6
  42. data/lib/closure/show_exceptions.rb +15 -12
  43. data/lib/closure/version.rb +1 -1
  44. data/scripts/git.erb +183 -0
  45. data/scripts/hello/compiler.js.erb +2 -2
  46. data/scripts/hello/hello.js +1 -1
  47. data/scripts/hello/index.erb +6 -0
  48. data/scripts/hello/legume.js +12 -7
  49. data/scripts/index.erb +15 -13
  50. data/scripts/modules/compiler.js.erb +3 -3
  51. data/scripts/modules/compiler_build.js +3 -3
  52. data/scripts/modules/compiler_build.map +13 -12159
  53. data/scripts/modules/compiler_build_api.js +1 -1
  54. data/scripts/modules/compiler_build_app.js +74 -71
  55. data/scripts/modules/compiler_build_settings.js +2 -2
  56. data/scripts/modules/index.erb +5 -3
  57. data/scripts/modules/settings.js +1 -1
  58. data/scripts/svn.erb +11 -11
  59. data/scripts/welcome.erb +7 -6
  60. metadata +65 -81
  61. data/closure-compiler/README +0 -292
  62. data/scripts/hello/compiler_build.js +0 -5
  63. data/scripts/hello/compiler_build.map +0 -748
  64. data/scripts/hello/compiler_debug.js +0 -119
  65. data/scripts/modules/compiler_debug.js +0 -6
  66. data/scripts/modules/compiler_debug_api.js +0 -11
  67. data/scripts/modules/compiler_debug_app.js +0 -2414
  68. data/scripts/modules/compiler_debug_settings.js +0 -39
  69. data/scripts/rails/index.erb +0 -46
  70. data/scripts/rails/rails_ujs.js +0 -96
@@ -30,27 +30,41 @@
30
30
  * by Soy-generated JS code. Please do not use these functions directly from
31
31
  * your hand-writen code. Their names all start with '$$'.
32
32
  *
33
+ * @author Garrett Boyer
33
34
  * @author Mike Samuel
34
35
  * @author Kai Huang
35
- * @author Aharon Lenin
36
+ * @author Aharon Lanin
36
37
  */
37
38
 
38
39
 
39
40
  // COPIED FROM nogoog_shim.js
40
41
 
41
42
  // Create closure namespaces.
42
- var goog;
43
- if (typeof goog == "undefined") {
44
- goog = {};
45
- }
43
+ var goog = goog || {};
44
+
45
+
46
+ goog.DEBUG = false;
47
+
46
48
 
47
49
  goog.inherits = function(childCtor, parentCtor) {
48
50
  /** @constructor */
49
- function tempCtor() {}
51
+ function tempCtor() {};
50
52
  tempCtor.prototype = parentCtor.prototype;
51
53
  childCtor.superClass_ = parentCtor.prototype;
52
54
  childCtor.prototype = new tempCtor();
53
55
  childCtor.prototype.constructor = childCtor;
56
+
57
+ /**
58
+ * Calls superclass constructor/method.
59
+ * @param {!Object} me Should always be "this".
60
+ * @param {string} methodName
61
+ * @param {...*} var_args
62
+ * @return {?} The return value of the superclass method/constructor.
63
+ */
64
+ childCtor.base = function(me, methodName, var_args) {
65
+ var args = Array.prototype.slice.call(arguments, 2);
66
+ return parentCtor.prototype[methodName].apply(me, args);
67
+ };
54
68
  };
55
69
 
56
70
 
@@ -64,45 +78,76 @@ if (!goog.userAgent) {
64
78
  }
65
79
  var isOpera = userAgent.indexOf('Opera') == 0;
66
80
  return {
81
+ jscript: {
82
+ /**
83
+ * @type {boolean}
84
+ */
85
+ HAS_JSCRIPT: 'ScriptEngine' in this
86
+ },
67
87
  /**
68
88
  * @type {boolean}
69
89
  */
70
- HAS_JSCRIPT: typeof 'ScriptEngine' in this,
71
- /**
72
- * @type {boolean}
73
- */
74
- IS_OPERA: isOpera,
90
+ OPERA: isOpera,
75
91
  /**
76
92
  * @type {boolean}
77
93
  */
78
- IS_IE: !isOpera && userAgent.indexOf('MSIE') != -1,
94
+ IE: !isOpera && userAgent.indexOf('MSIE') != -1,
79
95
  /**
80
96
  * @type {boolean}
81
97
  */
82
- IS_WEBKIT: !isOpera && userAgent.indexOf('WebKit') != -1
98
+ WEBKIT: !isOpera && userAgent.indexOf('WebKit') != -1
83
99
  };
84
100
  })();
85
101
  }
86
102
 
87
103
  if (!goog.asserts) {
88
104
  goog.asserts = {
89
- fail: function () {}
105
+ /**
106
+ * @param {*} condition Condition to check.
107
+ */
108
+ assert: function (condition) {
109
+ if (!condition) {
110
+ throw Error('Assertion error');
111
+ }
112
+ },
113
+ /**
114
+ * @param {...*} var_args
115
+ */
116
+ fail: function (var_args) {}
90
117
  };
91
118
  }
92
119
 
93
120
 
94
121
  // Stub out the document wrapper used by renderAs*.
95
122
  if (!goog.dom) {
96
- goog.dom = {
97
- DomHelper: function (d) {
98
- d = d || document;
99
- return {
100
- createElement: function (name) { return d.createElement(name); },
101
- createDocumentFragment: function () {
102
- return d.createDocumentFragment();
103
- }
104
- };
105
- }
123
+ goog.dom = {};
124
+ /**
125
+ * @param {Document=} d
126
+ * @constructor
127
+ */
128
+ goog.dom.DomHelper = function(d) {
129
+ this.document_ = d || document;
130
+ };
131
+ /**
132
+ * @return {!Document}
133
+ */
134
+ goog.dom.DomHelper.prototype.getDocument = function() {
135
+ return this.document_;
136
+ };
137
+ /**
138
+ * Creates a new element.
139
+ * @param {string} name Tag name.
140
+ * @return {!Element}
141
+ */
142
+ goog.dom.DomHelper.prototype.createElement = function(name) {
143
+ return this.document_.createElement(name);
144
+ };
145
+ /**
146
+ * Creates a new document fragment.
147
+ * @return {!DocumentFragment}
148
+ */
149
+ goog.dom.DomHelper.prototype.createDocumentFragment = function() {
150
+ return this.document_.createDocumentFragment();
106
151
  };
107
152
  }
108
153
 
@@ -195,66 +240,177 @@ if (!goog.format) {
195
240
  },
196
241
  /**
197
242
  * String inserted as a word break by insertWordBreaks(). Safari requires
198
- * <wbr></wbr>, Opera needs the 'shy' entity, though this will give a
199
- * visible hyphen at breaks. Other browsers just use <wbr>.
243
+ * <wbr></wbr>, Opera needs the &shy; entity, though this will give a
244
+ * visible hyphen at breaks. IE8+ use a zero width space. Other browsers
245
+ * just use <wbr>.
200
246
  * @type {string}
201
247
  * @private
202
248
  */
203
- WORD_BREAK: goog.userAgent.IS_WEBKIT
204
- ? '<wbr></wbr>' : goog.userAgent.IS_OPERA ? '&shy;' : '<wbr>'
249
+ WORD_BREAK:
250
+ goog.userAgent.WEBKIT ? '<wbr></wbr>' :
251
+ goog.userAgent.OPERA ? '&shy;' :
252
+ goog.userAgent.IE ? '&#8203;' :
253
+ '<wbr>'
205
254
  };
206
255
  }
207
256
 
208
257
 
209
258
  if (!goog.i18n) {
210
259
  goog.i18n = {
211
- /**
212
- * Utility class for formatting text for display in a potentially
213
- * opposite-directionality context without garbling. Provides the following
214
- * functionality:
215
- *
216
- * @param {goog.i18n.bidi.Dir|number|boolean} contextDir The context
217
- * directionality as a number
218
- * (positive = LRT, negative = RTL, 0 = unknown).
219
- * @constructor
220
- */
221
- BidiFormatter: function (dir) {
222
- this.dir_ = dir;
223
- },
224
- bidi: {
225
- /**
226
- * Check the directionality of a piece of text, return true if the piece
227
- * of text should be laid out in RTL direction.
228
- * @param {string} text The piece of text that need to be detected.
229
- * @param {boolean=} opt_isHtml Whether {@code text} is HTML/HTML-escaped.
230
- * Default: false.
231
- * @return {boolean}
232
- * @private
233
- */
234
- detectRtlDirectionality: function(text, opt_isHtml) {
235
- text = soyshim.$$bidiStripHtmlIfNecessary_(text, opt_isHtml);
236
- return soyshim.$$bidiRtlWordRatio_(text)
237
- > soyshim.$$bidiRtlDetectionThreshold_;
238
- }
239
- }
260
+ bidi: {}
240
261
  };
241
262
  }
242
263
 
243
264
 
244
265
  /**
245
- * Returns "dir=ltr" or "dir=rtl", depending on {@code text}'s estimated
246
- * directionality, if it is not the same as the context directionality.
247
- * Otherwise, returns the empty string.
266
+ * Constant that defines whether or not the current locale is an RTL locale.
248
267
  *
249
- * @param {string} text Text whose directionality is to be estimated.
250
- * @param {boolean=} opt_isHtml Whether {@code text} is HTML / HTML-escaped.
268
+ * @type {boolean}
269
+ */
270
+ goog.i18n.bidi.IS_RTL = false;
271
+
272
+
273
+ /**
274
+ * Directionality enum.
275
+ * @enum {number}
276
+ */
277
+ goog.i18n.bidi.Dir = {
278
+ /**
279
+ * Left-to-right.
280
+ */
281
+ LTR: 1,
282
+
283
+ /**
284
+ * Right-to-left.
285
+ */
286
+ RTL: -1,
287
+
288
+ /**
289
+ * Neither left-to-right nor right-to-left.
290
+ */
291
+ NEUTRAL: 0,
292
+
293
+ /**
294
+ * A historical misnomer for NEUTRAL.
295
+ * @deprecated For "neutral", use NEUTRAL; for "unknown", use null.
296
+ */
297
+ UNKNOWN: 0
298
+ };
299
+
300
+
301
+ /**
302
+ * Convert a directionality given in various formats to a goog.i18n.bidi.Dir
303
+ * constant. Useful for interaction with different standards of directionality
304
+ * representation.
305
+ *
306
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} givenDir Directionality given
307
+ * in one of the following formats:
308
+ * 1. A goog.i18n.bidi.Dir constant.
309
+ * 2. A number (positive = LTR, negative = RTL, 0 = neutral).
310
+ * 3. A boolean (true = RTL, false = LTR).
311
+ * 4. A null for unknown directionality.
312
+ * @param {boolean=} opt_noNeutral Whether a givenDir of zero or
313
+ * goog.i18n.bidi.Dir.NEUTRAL should be treated as null, i.e. unknown, in
314
+ * order to preserve legacy behavior.
315
+ * @return {?goog.i18n.bidi.Dir} A goog.i18n.bidi.Dir constant matching the
316
+ * given directionality. If given null, returns null (i.e. unknown).
317
+ */
318
+ goog.i18n.bidi.toDir = function(givenDir, opt_noNeutral) {
319
+ if (typeof givenDir == 'number') {
320
+ // This includes the non-null goog.i18n.bidi.Dir case.
321
+ return givenDir > 0 ? goog.i18n.bidi.Dir.LTR :
322
+ givenDir < 0 ? goog.i18n.bidi.Dir.RTL :
323
+ opt_noNeutral ? null : goog.i18n.bidi.Dir.NEUTRAL;
324
+ } else if (givenDir == null) {
325
+ return null;
326
+ } else {
327
+ // Must be typeof givenDir == 'boolean'.
328
+ return givenDir ? goog.i18n.bidi.Dir.RTL : goog.i18n.bidi.Dir.LTR;
329
+ }
330
+ };
331
+
332
+
333
+ /**
334
+ * Estimates the directionality of a string based on relative word counts.
335
+ * If the number of RTL words is above a certain percentage of the total number
336
+ * of strongly directional words, returns RTL.
337
+ * Otherwise, if any words are strongly or weakly LTR, returns LTR.
338
+ * Otherwise, returns NEUTRAL.
339
+ * Numbers are counted as weakly LTR.
340
+ * @param {string} str The string to be checked.
341
+ * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
251
342
  * Default: false.
252
- * @return {string} "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR
253
- * text in non-LTR context; else, the empty string.
343
+ * @return {goog.i18n.bidi.Dir} Estimated overall directionality of {@code str}.
254
344
  */
255
- goog.i18n.BidiFormatter.prototype.dirAttr = function (text, opt_isHtml) {
256
- var dir = soy.$$bidiTextDir(text, opt_isHtml);
257
- return dir && dir != this.dir_ ? dir < 0 ? 'dir=rtl' : 'dir=ltr' : '';
345
+ goog.i18n.bidi.estimateDirection = function(str, opt_isHtml) {
346
+ var rtlCount = 0;
347
+ var totalCount = 0;
348
+ var hasWeaklyLtr = false;
349
+ var tokens = soyshim.$$bidiStripHtmlIfNecessary_(str, opt_isHtml).
350
+ split(soyshim.$$bidiWordSeparatorRe_);
351
+ for (var i = 0; i < tokens.length; i++) {
352
+ var token = tokens[i];
353
+ if (soyshim.$$bidiRtlDirCheckRe_.test(token)) {
354
+ rtlCount++;
355
+ totalCount++;
356
+ } else if (soyshim.$$bidiIsRequiredLtrRe_.test(token)) {
357
+ hasWeaklyLtr = true;
358
+ } else if (soyshim.$$bidiLtrCharRe_.test(token)) {
359
+ totalCount++;
360
+ } else if (soyshim.$$bidiHasNumeralsRe_.test(token)) {
361
+ hasWeaklyLtr = true;
362
+ }
363
+ }
364
+
365
+ return totalCount == 0 ?
366
+ (hasWeaklyLtr ? goog.i18n.bidi.Dir.LTR : goog.i18n.bidi.Dir.NEUTRAL) :
367
+ (rtlCount / totalCount > soyshim.$$bidiRtlDetectionThreshold_ ?
368
+ goog.i18n.bidi.Dir.RTL : goog.i18n.bidi.Dir.LTR);
369
+ };
370
+
371
+
372
+ /**
373
+ * Utility class for formatting text for display in a potentially
374
+ * opposite-directionality context without garbling. Provides the following
375
+ * functionality:
376
+ *
377
+ * @param {goog.i18n.bidi.Dir|number|boolean|null} dir The context
378
+ * directionality, in one of the following formats:
379
+ * 1. A goog.i18n.bidi.Dir constant. NEUTRAL is treated the same as null,
380
+ * i.e. unknown, for backward compatibility with legacy calls.
381
+ * 2. A number (positive = LTR, negative = RTL, 0 = unknown).
382
+ * 3. A boolean (true = RTL, false = LTR).
383
+ * 4. A null for unknown directionality.
384
+ * @constructor
385
+ */
386
+ goog.i18n.BidiFormatter = function(dir) {
387
+ /**
388
+ * The overall directionality of the context in which the formatter is being
389
+ * used.
390
+ * @type {?goog.i18n.bidi.Dir}
391
+ * @private
392
+ */
393
+ this.dir_ = goog.i18n.bidi.toDir(dir, true /* opt_noNeutral */);
394
+ };
395
+
396
+ /**
397
+ * @return {?goog.i18n.bidi.Dir} The context directionality.
398
+ */
399
+ goog.i18n.BidiFormatter.prototype.getContextDir = function() {
400
+ return this.dir_;
401
+ };
402
+
403
+ /**
404
+ * Returns 'dir="ltr"' or 'dir="rtl"', depending on the given directionality, if
405
+ * it is not the same as the context directionality. Otherwise, returns the
406
+ * empty string.
407
+ *
408
+ * @param {goog.i18n.bidi.Dir} dir A directionality.
409
+ * @return {string} 'dir="rtl"' for RTL text in non-RTL context; 'dir="ltr"' for
410
+ * LTR text in non-LTR context; else, the empty string.
411
+ */
412
+ goog.i18n.BidiFormatter.prototype.knownDirAttr = function(dir) {
413
+ return !dir || dir == this.dir_ ? '' : dir < 0 ? 'dir="rtl"' : 'dir="ltr"';
258
414
  };
259
415
 
260
416
  /**
@@ -269,7 +425,7 @@ goog.i18n.BidiFormatter.prototype.endEdge = function () {
269
425
  /**
270
426
  * Returns the Unicode BiDi mark matching the context directionality (LRM for
271
427
  * LTR context directionality, RLM for RTL context directionality), or the
272
- * empty string for neutral / unknown context directionality.
428
+ * empty string for unknown context directionality.
273
429
  *
274
430
  * @return {string} LRM for LTR context directionality and RLM for RTL context
275
431
  * directionality.
@@ -282,37 +438,55 @@ goog.i18n.BidiFormatter.prototype.mark = function () {
282
438
  };
283
439
 
284
440
  /**
285
- * Returns a Unicode BiDi mark matching the context directionality (LRM or RLM)
441
+ * Returns a Unicode bidi mark matching the context directionality (LRM or RLM)
286
442
  * if the directionality or the exit directionality of {@code text} are opposite
287
443
  * to the context directionality. Otherwise returns the empty string.
288
- *
289
- * @param {string} text The input text.
290
- * @param {boolean=} opt_isHtml Whether {@code text} is HTML / HTML-escaped.
444
+ * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
445
+ * in text, making the logic suitable for HTML and HTML-escaped text.
446
+ * @param {?goog.i18n.bidi.Dir} textDir {@code text}'s overall directionality,
447
+ * or null if unknown and needs to be estimated.
448
+ * @param {string} text The text whose directionality is to be estimated.
449
+ * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
291
450
  * Default: false.
292
- * @return {string} A Unicode bidi mark matching the global directionality or
293
- * the empty string.
294
- */
295
- goog.i18n.BidiFormatter.prototype.markAfter = function (text, opt_isHtml) {
296
- var dir = soy.$$bidiTextDir(text, opt_isHtml);
297
- return soyshim.$$bidiMarkAfterKnownDir_(this.dir_, dir, text, opt_isHtml);
451
+ * @return {string} A Unicode bidi mark matching the context directionality, or
452
+ * the empty string when either the context directionality is unknown or
453
+ * neither the text's overall nor its exit directionality is opposite to it.
454
+ */
455
+ goog.i18n.BidiFormatter.prototype.markAfterKnownDir = function (
456
+ textDir, text, opt_isHtml) {
457
+ if (textDir == null) {
458
+ textDir = goog.i18n.bidi.estimateDirection(text, opt_isHtml);
459
+ }
460
+ return (
461
+ this.dir_ > 0 && (textDir < 0 ||
462
+ soyshim.$$bidiIsRtlExitText_(text, opt_isHtml)) ? '\u200E' : // LRM
463
+ this.dir_ < 0 && (textDir > 0 ||
464
+ soyshim.$$bidiIsLtrExitText_(text, opt_isHtml)) ? '\u200F' : // RLM
465
+ '');
298
466
  };
299
467
 
300
468
  /**
301
- * Formats a string of unknown directionality for use in HTML output of the
302
- * context directionality, so an opposite-directionality string is neither
303
- * garbled nor garbles what follows it.
469
+ * Formats an HTML string for use in HTML output of the context directionality,
470
+ * so an opposite-directionality string is neither garbled nor garbles what
471
+ * follows it.
304
472
  *
305
- * @param {string} str The input text.
306
- * @return {string} Input text after applying the above processing.
307
- */
308
- goog.i18n.BidiFormatter.prototype.spanWrap = function(str) {
309
- str = String(str);
310
- var textDir = soy.$$bidiTextDir(str, true);
311
- var reset = soyshim.$$bidiMarkAfterKnownDir_(this.dir_, textDir, str, true);
473
+ * @param {?goog.i18n.bidi.Dir} textDir {@code str}'s overall directionality, or
474
+ * null if unknown and needs to be estimated.
475
+ * @param {string} str The input text (HTML or HTML-escaped).
476
+ * @param {boolean=} placeholder This argument exists for consistency with the
477
+ * Closure Library. Specifying it has no effect.
478
+ * @return {string} The input text after applying the above processing.
479
+ */
480
+ goog.i18n.BidiFormatter.prototype.spanWrapWithKnownDir = function(
481
+ textDir, str, placeholder) {
482
+ if (textDir == null) {
483
+ textDir = goog.i18n.bidi.estimateDirection(str, true);
484
+ }
485
+ var reset = this.markAfterKnownDir(textDir, str, true);
312
486
  if (textDir > 0 && this.dir_ <= 0) {
313
- str = '<span dir=ltr>' + str + '</span>';
487
+ str = '<span dir="ltr">' + str + '</span>';
314
488
  } else if (textDir < 0 && this.dir_ >= 0) {
315
- str = '<span dir=rtl>' + str + '</span>';
489
+ str = '<span dir="rtl">' + str + '</span>';
316
490
  }
317
491
  return str + reset;
318
492
  };
@@ -327,20 +501,26 @@ goog.i18n.BidiFormatter.prototype.startEdge = function () {
327
501
  };
328
502
 
329
503
  /**
330
- * Formats a string of unknown directionality for use in plain-text output of
331
- * the context directionality, so an opposite-directionality string is neither
332
- * garbled nor garbles what follows it.
333
- * As opposed to {@link #spanWrap}, this makes use of unicode BiDi formatting
334
- * characters. In HTML, its *only* valid use is inside of elements that do not
335
- * allow mark-up, e.g. an 'option' tag.
504
+ * Formats an HTML-escaped string for use in HTML output of the context
505
+ * directionality, so an opposite-directionality string is neither garbled nor
506
+ * garbles what follows it.
507
+ * As opposed to {@link #spanWrapWithKnownDir}, this makes use of unicode bidi
508
+ * formatting characters. In HTML, it should only be used inside attribute
509
+ * values and elements that do not allow markup, e.g. an 'option' tag.
336
510
  *
337
- * @param {string} str The input text.
338
- * @return {string} Input text after applying the above processing.
511
+ * @param {?goog.i18n.bidi.Dir} textDir {@code str}'s overall directionality, or
512
+ * null if unknown and needs to be estimated.
513
+ * @param {string} str The input text (HTML-escaped).
514
+ * @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.
515
+ * Default: false.
516
+ * @return {string} The input text after applying the above processing.
339
517
  */
340
- goog.i18n.BidiFormatter.prototype.unicodeWrap = function(str) {
341
- str = String(str);
342
- var textDir = soy.$$bidiTextDir(str, true);
343
- var reset = soyshim.$$bidiMarkAfterKnownDir_(this.dir_, textDir, str, true);
518
+ goog.i18n.BidiFormatter.prototype.unicodeWrapWithKnownDir = function(
519
+ textDir, str, opt_isHtml) {
520
+ if (textDir == null) {
521
+ textDir = goog.i18n.bidi.estimateDirection(str, opt_isHtml);
522
+ }
523
+ var reset = this.markAfterKnownDir(textDir, str, opt_isHtml);
344
524
  if (textDir > 0 && this.dir_ <= 0) {
345
525
  str = '\u202A' + str + '\u202C';
346
526
  } else if (textDir < 0 && this.dir_ >= 0) {
@@ -350,55 +530,58 @@ goog.i18n.BidiFormatter.prototype.unicodeWrap = function(str) {
350
530
  };
351
531
 
352
532
 
353
- goog.string = {
354
- /**
355
- * Utility class to facilitate much faster string concatenation in IE,
356
- * using Array.join() rather than the '+' operator. For other browsers
357
- * we simply use the '+' operator.
358
- *
359
- * @param {Object|number|string|boolean=} opt_a1 Optional first initial item
360
- * to append.
361
- * @param {...Object|number|string|boolean} var_args Other initial items to
362
- * append, e.g., new goog.string.StringBuffer('foo', 'bar').
363
- * @constructor
364
- */
365
- StringBuffer: function(opt_a1, var_args) {
366
-
533
+ if (!goog.string) {
534
+ goog.string = {
367
535
  /**
368
- * Internal buffer for the string to be concatenated.
369
- * @type {string|Array}
370
- * @private
536
+ * Converts \r\n, \r, and \n to <br>s
537
+ * @param {*} str The string in which to convert newlines.
538
+ * @param {boolean=} opt_xml Whether to use XML compatible tags.
539
+ * @return {string} A copy of {@code str} with converted newlines.
371
540
  */
372
- this.buffer_ = goog.userAgent.HAS_JSCRIPT ? [] : '';
541
+ newLineToBr: function(str, opt_xml) {
373
542
 
374
- if (opt_a1 != null) {
375
- this.append.apply(this, arguments);
376
- }
377
- },
378
- /**
379
- * Converts \r\n, \r, and \n to <br>s
380
- * @param {*} str The string in which to convert newlines.
381
- * @return {string} A copy of {@code str} with converted newlines.
382
- */
383
- newlineToBr: function(str) {
543
+ str = String(str);
384
544
 
385
- str = String(str);
545
+ // This quick test helps in the case when there are no chars to replace,
546
+ // in the worst case this makes barely a difference to the time taken.
547
+ if (!goog.string.NEWLINE_TO_BR_RE_.test(str)) {
548
+ return str;
549
+ }
386
550
 
387
- // This quick test helps in the case when there are no chars to replace,
388
- // in the worst case this makes barely a difference to the time taken.
389
- if (!goog.string.NEWLINE_TO_BR_RE_.test(str)) {
390
- return str;
391
- }
551
+ return str.replace(/(\r\n|\r|\n)/g, opt_xml ? '<br />' : '<br>');
552
+ },
553
+ urlEncode: encodeURIComponent,
554
+ /**
555
+ * Regular expression used within newlineToBr().
556
+ * @type {RegExp}
557
+ * @private
558
+ */
559
+ NEWLINE_TO_BR_RE_: /[\r\n]/
560
+ };
561
+ }
392
562
 
393
- return str.replace(/(\r\n|\r|\n)/g, '<br>');
394
- },
395
- urlEncode: encodeURIComponent,
563
+ /**
564
+ * Utility class to facilitate much faster string concatenation in IE,
565
+ * using Array.join() rather than the '+' operator. For other browsers
566
+ * we simply use the '+' operator.
567
+ *
568
+ * @param {Object|number|string|boolean=} opt_a1 Optional first initial item
569
+ * to append.
570
+ * @param {...Object|number|string|boolean} var_args Other initial items to
571
+ * append, e.g., new goog.string.StringBuffer('foo', 'bar').
572
+ * @constructor
573
+ */
574
+ goog.string.StringBuffer = function(opt_a1, var_args) {
396
575
  /**
397
- * Regular expression used within newlineToBr().
398
- * @type {RegExp}
576
+ * Internal buffer for the string to be concatenated.
577
+ * @type {string|Array}
399
578
  * @private
400
579
  */
401
- NEWLINE_TO_BR_RE: /[\r\n]/
580
+ this.buffer_ = goog.userAgent.jscript.HAS_JSCRIPT ? [] : '';
581
+
582
+ if (opt_a1 != null) {
583
+ this.append.apply(this, arguments);
584
+ }
402
585
  };
403
586
 
404
587
 
@@ -423,13 +606,13 @@ goog.string.StringBuffer.prototype.bufferLength_ = 0;
423
606
  */
424
607
  goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
425
608
 
426
- if (goog.userAgent.HAS_JSCRIPT) {
609
+ if (goog.userAgent.jscript.HAS_JSCRIPT) {
427
610
  if (opt_a2 == null) { // no second argument (note: undefined == null)
428
- // Array assignment is 2x faster than Array push. Also, use a1
611
+ // Array assignment is 2x faster than Array push. Also, use a1
429
612
  // directly to avoid arguments instantiation, another 2x improvement.
430
613
  this.buffer_[this.bufferLength_++] = a1;
431
614
  } else {
432
- var arr = /**@type {Array.<number|string|boolean>}*/this.buffer_;
615
+ var arr = /**@type {Array.<number|string|boolean>}*/(this.buffer_);
433
616
  arr.push.apply(arr, arguments);
434
617
  this.bufferLength_ = this.buffer_.length;
435
618
  }
@@ -454,7 +637,7 @@ goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) {
454
637
  */
455
638
  goog.string.StringBuffer.prototype.clear = function() {
456
639
 
457
- if (goog.userAgent.HAS_JSCRIPT) {
640
+ if (goog.userAgent.jscript.HAS_JSCRIPT) {
458
641
  this.buffer_.length = 0; // reuse array to avoid creating new object
459
642
  this.bufferLength_ = 0;
460
643
 
@@ -471,7 +654,7 @@ goog.string.StringBuffer.prototype.clear = function() {
471
654
  */
472
655
  goog.string.StringBuffer.prototype.toString = function() {
473
656
 
474
- if (goog.userAgent.HAS_JSCRIPT) {
657
+ if (goog.userAgent.jscript.HAS_JSCRIPT) {
475
658
  var str = this.buffer_.join('');
476
659
  // Given a string with the entire contents, simplify the StringBuilder by
477
660
  // setting its contents to only be this string, rather than many fragments.
@@ -495,15 +678,16 @@ if (!goog.soy) goog.soy = {
495
678
  * innerHTML in your hand-written code, so that it will be easier
496
679
  * to audit the code for cross-site scripting vulnerabilities.
497
680
  *
498
- * @param {Element} element The element whose content we are rendering.
499
681
  * @param {Function} template The Soy template defining element's content.
500
682
  * @param {Object=} opt_templateData The data for the template.
501
683
  * @param {Object=} opt_injectedData The injected data for the template.
684
+ * @param {(goog.dom.DomHelper|Document)=} opt_dom The context in which DOM
685
+ * nodes will be created.
502
686
  */
503
687
  renderAsElement: function(
504
- template, opt_templateData, opt_injectedData, opt_document) {
688
+ template, opt_templateData, opt_injectedData, opt_dom) {
505
689
  return /** @type {!Element} */ (soyshim.$$renderWithWrapper_(
506
- template, opt_templateData, opt_document, true /* asElement */,
690
+ template, opt_templateData, opt_dom, true /* asElement */,
507
691
  opt_injectedData));
508
692
  },
509
693
  /**
@@ -515,15 +699,15 @@ if (!goog.soy) goog.soy = {
515
699
  *
516
700
  * @param {Function} template The Soy template defining element's content.
517
701
  * @param {Object=} opt_templateData The data for the template.
518
- * @param {Document=} opt_document The document used to create DOM nodes.
519
- * If not specified, global document object is used.
520
702
  * @param {Object=} opt_injectedData The injected data for the template.
703
+ * @param {(goog.dom.DomHelper|Document)=} opt_dom The context in which DOM
704
+ * nodes will be created.
521
705
  * @return {!Node} The resulting node or document fragment.
522
706
  */
523
707
  renderAsFragment: function(
524
- template, opt_templateData, opt_injectedData, opt_document) {
708
+ template, opt_templateData, opt_injectedData, opt_dom) {
525
709
  return soyshim.$$renderWithWrapper_(
526
- template, opt_templateData, opt_document, false /* asElement */,
710
+ template, opt_templateData, opt_dom, false /* asElement */,
527
711
  opt_injectedData);
528
712
  },
529
713
  /**
@@ -542,13 +726,129 @@ if (!goog.soy) goog.soy = {
542
726
  renderElement: function(
543
727
  element, template, opt_templateData, opt_injectedData) {
544
728
  element.innerHTML = template(opt_templateData, null, opt_injectedData);
545
- }
729
+ },
730
+ data: {}
731
+ };
732
+
733
+
734
+ /**
735
+ * A type of textual content.
736
+ *
737
+ * This is an enum of type Object so that these values are unforgeable.
738
+ *
739
+ * @enum {!Object}
740
+ */
741
+ goog.soy.data.SanitizedContentKind = {
742
+
743
+ /**
744
+ * A snippet of HTML that does not start or end inside a tag, comment, entity,
745
+ * or DOCTYPE; and that does not contain any executable code
746
+ * (JS, {@code <object>}s, etc.) from a different trust domain.
747
+ */
748
+ HTML: goog.DEBUG ? {sanitizedContentKindHtml: true} : {},
749
+
750
+ /**
751
+ * Executable Javascript code or expression, safe for insertion in a
752
+ * script-tag or event handler context, known to be free of any
753
+ * attacker-controlled scripts. This can either be side-effect-free
754
+ * Javascript (such as JSON) or Javascript that's entirely under Google's
755
+ * control.
756
+ */
757
+ JS: goog.DEBUG ? {sanitizedContentJsChars: true} : {},
758
+
759
+ /**
760
+ * A sequence of code units that can appear between quotes (either kind) in a
761
+ * JS program without causing a parse error, and without causing any side
762
+ * effects.
763
+ * <p>
764
+ * The content should not contain unescaped quotes, newlines, or anything else
765
+ * that would cause parsing to fail or to cause a JS parser to finish the
766
+ * string its parsing inside the content.
767
+ * <p>
768
+ * The content must also not end inside an escape sequence ; no partial octal
769
+ * escape sequences or odd number of '{@code \}'s at the end.
770
+ */
771
+ JS_STR_CHARS: goog.DEBUG ? {sanitizedContentJsStrChars: true} : {},
772
+
773
+ /** A properly encoded portion of a URI. */
774
+ URI: goog.DEBUG ? {sanitizedContentUri: true} : {},
775
+
776
+ /**
777
+ * Repeated attribute names and values. For example,
778
+ * {@code dir="ltr" foo="bar" onclick="trustedFunction()" checked}.
779
+ */
780
+ ATTRIBUTES: goog.DEBUG ? {sanitizedContentHtmlAttribute: true} : {},
781
+
782
+ // TODO: Consider separating rules, declarations, and values into
783
+ // separate types, but for simplicity, we'll treat explicitly blessed
784
+ // SanitizedContent as allowed in all of these contexts.
785
+ /**
786
+ * A CSS3 declaration, property, value or group of semicolon separated
787
+ * declarations.
788
+ */
789
+ CSS: goog.DEBUG ? {sanitizedContentCss: true} : {},
790
+
791
+ /**
792
+ * Unsanitized plain-text content.
793
+ *
794
+ * This is effectively the "null" entry of this enum, and is sometimes used
795
+ * to explicitly mark content that should never be used unescaped. Since any
796
+ * string is safe to use as text, being of ContentKind.TEXT makes no
797
+ * guarantees about its safety in any other context such as HTML.
798
+ */
799
+ TEXT: goog.DEBUG ? {sanitizedContentKindText: true} : {}
800
+ };
801
+
802
+
803
+
804
+ /**
805
+ * A string-like object that carries a content-type and a content direction.
806
+ *
807
+ * IMPORTANT! Do not create these directly, nor instantiate the subclasses.
808
+ * Instead, use a trusted, centrally reviewed library as endorsed by your team
809
+ * to generate these objects. Otherwise, you risk accidentally creating
810
+ * SanitizedContent that is attacker-controlled and gets evaluated unescaped in
811
+ * templates.
812
+ *
813
+ * @constructor
814
+ */
815
+ goog.soy.data.SanitizedContent = function() {
816
+ throw Error('Do not instantiate directly');
817
+ };
818
+
819
+
820
+ /**
821
+ * The context in which this content is safe from XSS attacks.
822
+ * @type {goog.soy.data.SanitizedContentKind}
823
+ */
824
+ goog.soy.data.SanitizedContent.prototype.contentKind;
825
+
826
+
827
+ /**
828
+ * The content's direction; null if unknown and thus to be estimated when
829
+ * necessary.
830
+ * @type {?goog.i18n.bidi.Dir}
831
+ */
832
+ goog.soy.data.SanitizedContent.prototype.contentDir = null;
833
+
834
+
835
+ /**
836
+ * The already-safe content.
837
+ * @type {string}
838
+ */
839
+ goog.soy.data.SanitizedContent.prototype.content;
840
+
841
+
842
+ /** @override */
843
+ goog.soy.data.SanitizedContent.prototype.toString = function() {
844
+ return this.content;
546
845
  };
547
846
 
548
847
 
549
848
  var soy = { esc: {} };
550
849
  var soydata = {};
551
- var soyshim = {};
850
+ soydata.VERY_UNSAFE = {};
851
+ var soyshim = { $$DEFAULT_TEMPLATE_DATA_: {} };
552
852
  /**
553
853
  * Helper function to render a Soy template into a single node or a document
554
854
  * fragment. If the rendered HTML string represents a single node, then that
@@ -557,8 +857,8 @@ var soyshim = {};
557
857
  *
558
858
  * @param {Function} template The Soy template defining the element's content.
559
859
  * @param {Object=} opt_templateData The data for the template.
560
- * @param {Document=} opt_document The document used to create DOM nodes. If
561
- * not specified, global document object is used.
860
+ * @param {(goog.dom.DomHelper|Document)=} opt_dom The context in which DOM
861
+ * nodes will be created.
562
862
  * @param {boolean=} opt_asElement Whether to wrap the fragment in an
563
863
  * element if the template does not render a single element. If true,
564
864
  * result is always an Element.
@@ -567,10 +867,10 @@ var soyshim = {};
567
867
  * @private
568
868
  */
569
869
  soyshim.$$renderWithWrapper_ = function(
570
- template, opt_templateData, opt_document, opt_asElement, opt_injectedData) {
870
+ template, opt_templateData, opt_dom, opt_asElement, opt_injectedData) {
571
871
 
572
- var doc = opt_document || document;
573
- var wrapper = doc.createElement('div');
872
+ var dom = opt_dom || document;
873
+ var wrapper = dom.createElement('div');
574
874
  wrapper.innerHTML = template(
575
875
  opt_templateData || soyshim.$$DEFAULT_TEMPLATE_DATA_, undefined,
576
876
  opt_injectedData);
@@ -589,7 +889,7 @@ soyshim.$$renderWithWrapper_ = function(
589
889
  }
590
890
 
591
891
  // Otherwise, create and return a fragment.
592
- var fragment = doc.createDocumentFragment();
892
+ var fragment = dom.createDocumentFragment();
593
893
  while (wrapper.firstChild) {
594
894
  fragment.appendChild(wrapper.firstChild);
595
895
  }
@@ -597,38 +897,11 @@ soyshim.$$renderWithWrapper_ = function(
597
897
  };
598
898
 
599
899
 
600
- /**
601
- * Returns a Unicode BiDi mark matching bidiGlobalDir (LRM or RLM) if the
602
- * directionality or the exit directionality of text are opposite to
603
- * bidiGlobalDir. Otherwise returns the empty string.
604
- * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
605
- * in text, making the logic suitable for HTML and HTML-escaped text.
606
- * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
607
- * if rtl, 0 if unknown.
608
- * @param {number} dir text's directionality: 1 if ltr, -1 if rtl, 0 if unknown.
609
- * @param {string} text The text whose directionality is to be estimated.
610
- * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
611
- * Default: false.
612
- * @return {string} A Unicode bidi mark matching bidiGlobalDir, or
613
- * the empty string when text's overall and exit directionalities both match
614
- * bidiGlobalDir, or bidiGlobalDir is 0 (unknown).
615
- * @private
616
- */
617
- soyshim.$$bidiMarkAfterKnownDir_ = function(
618
- bidiGlobalDir, dir, text, opt_isHtml) {
619
- return (
620
- bidiGlobalDir > 0 && (dir < 0 ||
621
- soyshim.$$bidiIsRtlExitText_(text, opt_isHtml)) ? '\u200E' : // LRM
622
- bidiGlobalDir < 0 && (dir > 0 ||
623
- soyshim.$$bidiIsLtrExitText_(text, opt_isHtml)) ? '\u200F' : // RLM
624
- '');
625
- };
626
-
627
-
628
900
  /**
629
901
  * Strips str of any HTML mark-up and escapes. Imprecise in several ways, but
630
902
  * precision is not very important, since the result is only meant to be used
631
903
  * for directionality detection.
904
+ * Based on goog.i18n.bidi.stripHtmlIfNeeded_().
632
905
  * @param {string} str The string to be stripped.
633
906
  * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
634
907
  * Default: false.
@@ -636,7 +909,7 @@ soyshim.$$bidiMarkAfterKnownDir_ = function(
636
909
  * @private
637
910
  */
638
911
  soyshim.$$bidiStripHtmlIfNecessary_ = function(str, opt_isHtml) {
639
- return opt_isHtml ? str.replace(soyshim.$$BIDI_HTML_SKIP_RE_, ' ') : str;
912
+ return opt_isHtml ? str.replace(soyshim.$$BIDI_HTML_SKIP_RE_, '') : str;
640
913
  };
641
914
 
642
915
 
@@ -644,6 +917,7 @@ soyshim.$$bidiStripHtmlIfNecessary_ = function(str, opt_isHtml) {
644
917
  * Simplified regular expression for am HTML tag (opening or closing) or an HTML
645
918
  * escape - the things we want to skip over in order to ignore their ltr
646
919
  * characters.
920
+ * Copied from goog.i18n.bidi.htmlSkipReg_.
647
921
  * @type {RegExp}
648
922
  * @private
649
923
  */
@@ -654,38 +928,30 @@ soyshim.$$BIDI_HTML_SKIP_RE_ = /<[^>]*>|&[^;]+;/g;
654
928
  * A practical pattern to identify strong LTR character. This pattern is not
655
929
  * theoretically correct according to unicode standard. It is simplified for
656
930
  * performance and small code size.
931
+ * Copied from goog.i18n.bidi.ltrChars_.
657
932
  * @type {string}
658
933
  * @private
659
934
  */
660
935
  soyshim.$$bidiLtrChars_ =
661
936
  'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' +
662
- '\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF';
663
-
664
-
665
- /**
666
- * A practical pattern to identify strong neutral and weak character. This
667
- * pattern is not theoretically correct according to unicode standard. It is
668
- * simplified for performance and small code size.
669
- * @type {string}
670
- * @private
671
- */
672
- soyshim.$$bidiNeutralChars_ =
673
- '\u0000-\u0020!-@[-`{-\u00BF\u00D7\u00F7\u02B9-\u02FF\u2000-\u2BFF';
937
+ '\u200E\u2C00-\uFB1C\uFE00-\uFE6F\uFEFD-\uFFFF';
674
938
 
675
939
 
676
940
  /**
677
941
  * A practical pattern to identify strong RTL character. This pattern is not
678
942
  * theoretically correct according to unicode standard. It is simplified for
679
943
  * performance and small code size.
944
+ * Copied from goog.i18n.bidi.rtlChars_.
680
945
  * @type {string}
681
946
  * @private
682
947
  */
683
- soyshim.$$bidiRtlChars_ = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';
948
+ soyshim.$$bidiRtlChars_ = '\u0591-\u07FF\u200F\uFB1D-\uFDFF\uFE70-\uFEFC';
684
949
 
685
950
 
686
951
  /**
687
952
  * Regular expressions to check if a piece of text is of RTL directionality
688
953
  * on first character with strong directionality.
954
+ * Based on goog.i18n.bidi.rtlDirCheckRe_.
689
955
  * @type {RegExp}
690
956
  * @private
691
957
  */
@@ -694,73 +960,59 @@ soyshim.$$bidiRtlDirCheckRe_ = new RegExp(
694
960
 
695
961
 
696
962
  /**
697
- * Regular expressions to check if a piece of text is of neutral directionality.
698
- * Url are considered as neutral.
963
+ * Regular expression to check for LTR characters.
964
+ * Based on goog.i18n.bidi.ltrCharReg_.
699
965
  * @type {RegExp}
700
966
  * @private
701
967
  */
702
- soyshim.$$bidiNeutralDirCheckRe_ = new RegExp(
703
- '^[' + soyshim.$$bidiNeutralChars_ + ']*$|^http://');
968
+ soyshim.$$bidiLtrCharRe_ = new RegExp('[' + soyshim.$$bidiLtrChars_ + ']');
704
969
 
705
970
 
706
971
  /**
707
- * Check the directionality of the a piece of text based on the first character
708
- * with strong directionality.
709
- * @param {string} str string being checked.
710
- * @return {boolean} return true if rtl directionality is being detected.
972
+ * Regular expression to check if a string looks like something that must
973
+ * always be LTR even in RTL text, e.g. a URL. When estimating the
974
+ * directionality of text containing these, we treat these as weakly LTR,
975
+ * like numbers.
976
+ * Copied from goog.i18n.bidi.isRequiredLtrRe_.
977
+ * @type {RegExp}
711
978
  * @private
712
979
  */
713
- soyshim.$$bidiIsRtlText_ = function(str) {
714
- return soyshim.$$bidiRtlDirCheckRe_.test(str);
715
- };
980
+ soyshim.$$bidiIsRequiredLtrRe_ = /^http:\/\/.*/;
716
981
 
717
982
 
718
983
  /**
719
- * Check the directionality of the a piece of text based on the first character
720
- * with strong directionality.
721
- * @param {string} str string being checked.
722
- * @return {boolean} true if all characters have neutral directionality.
984
+ * Regular expression to check if a string contains any numerals. Used to
985
+ * differentiate between completely neutral strings and those containing
986
+ * numbers, which are weakly LTR.
987
+ * Copied from goog.i18n.bidi.hasNumeralsRe_.
988
+ * @type {RegExp}
723
989
  * @private
724
990
  */
725
- soyshim.$$bidiIsNeutralText_ = function(str) {
726
- return soyshim.$$bidiNeutralDirCheckRe_.test(str);
727
- };
991
+ soyshim.$$bidiHasNumeralsRe_ = /\d/;
728
992
 
729
993
 
730
994
  /**
731
- * This constant controls threshold of rtl directionality.
732
- * @type {number}
995
+ * Regular expression to split a string into "words" for directionality
996
+ * estimation based on relative word counts.
997
+ * Copied from goog.i18n.bidi.wordSeparatorRe_.
998
+ * @type {RegExp}
733
999
  * @private
734
1000
  */
735
- soyshim.$$bidiRtlDetectionThreshold_ = 0.40;
1001
+ soyshim.$$bidiWordSeparatorRe_ = /\s+/;
736
1002
 
737
1003
 
738
1004
  /**
739
- * Returns the RTL ratio based on word count.
740
- * @param {string} str the string that need to be checked.
741
- * @return {number} the ratio of RTL words among all words with directionality.
1005
+ * This constant controls threshold of rtl directionality.
1006
+ * Copied from goog.i18n.bidi.rtlDetectionThreshold_.
1007
+ * @type {number}
742
1008
  * @private
743
1009
  */
744
- soyshim.$$bidiRtlWordRatio_ = function(str) {
745
- var rtlCount = 0;
746
- var totalCount = 0;
747
- var tokens = str.split(' ');
748
- for (var i = 0; i < tokens.length; i++) {
749
- if (soyshim.$$bidiIsRtlText_(tokens[i])) {
750
- rtlCount++;
751
- totalCount++;
752
- } else if (!soyshim.$$bidiIsNeutralText_(tokens[i])) {
753
- totalCount++;
754
- }
755
- }
756
-
757
- return totalCount == 0 ? 0 : rtlCount / totalCount;
758
- };
759
-
1010
+ soyshim.$$bidiRtlDetectionThreshold_ = 0.40;
760
1011
 
761
1012
  /**
762
1013
  * Regular expressions to check if the last strongly-directional character in a
763
1014
  * piece of text is LTR.
1015
+ * Based on goog.i18n.bidi.ltrExitDirCheckRe_.
764
1016
  * @type {RegExp}
765
1017
  * @private
766
1018
  */
@@ -771,6 +1023,7 @@ soyshim.$$bidiLtrExitDirCheckRe_ = new RegExp(
771
1023
  /**
772
1024
  * Regular expressions to check if the last strongly-directional character in a
773
1025
  * piece of text is RTL.
1026
+ * Based on goog.i18n.bidi.rtlExitDirCheckRe_.
774
1027
  * @type {RegExp}
775
1028
  * @private
776
1029
  */
@@ -781,6 +1034,7 @@ soyshim.$$bidiRtlExitDirCheckRe_ = new RegExp(
781
1034
  /**
782
1035
  * Check if the exit directionality a piece of text is LTR, i.e. if the last
783
1036
  * strongly-directional character in the string is LTR.
1037
+ * Based on goog.i18n.bidi.endsWithLtr().
784
1038
  * @param {string} str string being checked.
785
1039
  * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
786
1040
  * Default: false.
@@ -796,6 +1050,7 @@ soyshim.$$bidiIsLtrExitText_ = function(str, opt_isHtml) {
796
1050
  /**
797
1051
  * Check if the exit directionality a piece of text is RTL, i.e. if the last
798
1052
  * strongly-directional character in the string is RTL.
1053
+ * Based on goog.i18n.bidi.endsWithRtl().
799
1054
  * @param {string} str string being checked.
800
1055
  * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
801
1056
  * Default: false.
@@ -818,7 +1073,7 @@ soyshim.$$bidiIsRtlExitText_ = function(str, opt_isHtml) {
818
1073
 
819
1074
  /**
820
1075
  * Utility class to facilitate much faster string concatenation in IE,
821
- * using Array.join() rather than the '+' operator. For other browsers
1076
+ * using Array.join() rather than the '+' operator. For other browsers
822
1077
  * we simply use the '+' operator.
823
1078
  *
824
1079
  * @param {Object} var_args Initial items to append,
@@ -835,131 +1090,441 @@ soy.StringBuilder = goog.string.StringBuffer;
835
1090
 
836
1091
  /**
837
1092
  * A type of textual content.
838
- * @enum {number}
1093
+ *
1094
+ * This is an enum of type Object so that these values are unforgeable.
1095
+ *
1096
+ * @enum {!Object}
839
1097
  */
840
- soydata.SanitizedContentKind = {
1098
+ soydata.SanitizedContentKind = goog.soy.data.SanitizedContentKind;
841
1099
 
842
- /**
843
- * A snippet of HTML that does not start or end inside a tag, comment, entity,
844
- * or DOCTYPE; and that does not contain any executable code
845
- * (JS, {@code <object>}s, etc.) from a different trust domain.
846
- */
847
- HTML: 0,
848
1100
 
849
- /**
850
- * A sequence of code units that can appear between quotes (either kind) in a
851
- * JS program without causing a parse error, and without causing any side
852
- * effects.
853
- * <p>
854
- * The content should not contain unescaped quotes, newlines, or anything else
855
- * that would cause parsing to fail or to cause a JS parser to finish the
856
- * string its parsing inside the content.
857
- * <p>
858
- * The content must also not end inside an escape sequence ; no partial octal
859
- * escape sequences or odd number of '{@code \}'s at the end.
860
- */
861
- JS_STR_CHARS: 1,
1101
+ /**
1102
+ * Checks whether a given value is of a given content kind.
1103
+ *
1104
+ * @param {*} value The value to be examined.
1105
+ * @param {soydata.SanitizedContentKind} contentKind The desired content
1106
+ * kind.
1107
+ * @return {boolean} Whether the given value is of the given kind.
1108
+ * @private
1109
+ */
1110
+ soydata.isContentKind = function(value, contentKind) {
1111
+ // TODO(user): This function should really include the assert on
1112
+ // value.constructor that is currently sprinkled at most of the call sites.
1113
+ // Unfortunately, that would require a (debug-mode-only) switch statement.
1114
+ // TODO(user): Perhaps we should get rid of the contentKind property
1115
+ // altogether and only at the constructor.
1116
+ return value != null && value.contentKind === contentKind;
1117
+ };
1118
+
1119
+
1120
+ /**
1121
+ * Returns a given value's contentDir property, constrained to a
1122
+ * goog.i18n.bidi.Dir value or null. Returns null if the value is null,
1123
+ * undefined, a primitive or does not have a contentDir property, or the
1124
+ * property's value is not 1 (for LTR), -1 (for RTL), or 0 (for neutral).
1125
+ *
1126
+ * @param {*} value The value whose contentDir property, if any, is to
1127
+ * be returned.
1128
+ * @return {?goog.i18n.bidi.Dir} The contentDir property.
1129
+ */
1130
+ soydata.getContentDir = function(value) {
1131
+ if (value != null) {
1132
+ switch (value.contentDir) {
1133
+ case goog.i18n.bidi.Dir.LTR:
1134
+ return goog.i18n.bidi.Dir.LTR;
1135
+ case goog.i18n.bidi.Dir.RTL:
1136
+ return goog.i18n.bidi.Dir.RTL;
1137
+ case goog.i18n.bidi.Dir.NEUTRAL:
1138
+ return goog.i18n.bidi.Dir.NEUTRAL;
1139
+ }
1140
+ }
1141
+ return null;
1142
+ };
1143
+
1144
+
1145
+ /**
1146
+ * Content of type {@link soydata.SanitizedContentKind.HTML}.
1147
+ *
1148
+ * The content is a string of HTML that can safely be embedded in a PCDATA
1149
+ * context in your app. If you would be surprised to find that an HTML
1150
+ * sanitizer produced {@code s} (e.g. it runs code or fetches bad URLs) and
1151
+ * you wouldn't write a template that produces {@code s} on security or privacy
1152
+ * grounds, then don't pass {@code s} here. The default content direction is
1153
+ * unknown, i.e. to be estimated when necessary.
1154
+ *
1155
+ * @constructor
1156
+ * @extends {goog.soy.data.SanitizedContent}
1157
+ */
1158
+ soydata.SanitizedHtml = function() {
1159
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1160
+ };
1161
+ goog.inherits(soydata.SanitizedHtml, goog.soy.data.SanitizedContent);
1162
+
1163
+ /** @override */
1164
+ soydata.SanitizedHtml.prototype.contentKind = soydata.SanitizedContentKind.HTML;
1165
+
1166
+ /**
1167
+ * Returns a SanitizedHtml object for a particular value. The content direction
1168
+ * is preserved.
1169
+ *
1170
+ * This HTML-escapes the value unless it is already SanitizedHtml.
1171
+ *
1172
+ * @param {*} value The value to convert. If it is already a SanitizedHtml
1173
+ * object, it is left alone.
1174
+ * @return {!soydata.SanitizedHtml} A SanitizedHtml object derived from the
1175
+ * stringified value. It is escaped unless the input is SanitizedHtml.
1176
+ */
1177
+ soydata.SanitizedHtml.from = function(value) {
1178
+ // The check is soydata.isContentKind() inlined for performance.
1179
+ if (value != null &&
1180
+ value.contentKind === soydata.SanitizedContentKind.HTML) {
1181
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtml);
1182
+ return /** @type {!soydata.SanitizedHtml} */ (value);
1183
+ }
1184
+ return soydata.VERY_UNSAFE.ordainSanitizedHtml(
1185
+ soy.esc.$$escapeHtmlHelper(String(value)), soydata.getContentDir(value));
1186
+ };
1187
+
1188
+
1189
+ /**
1190
+ * Content of type {@link soydata.SanitizedContentKind.JS}.
1191
+ *
1192
+ * The content is Javascript source that when evaluated does not execute any
1193
+ * attacker-controlled scripts. The content direction is LTR.
1194
+ *
1195
+ * @constructor
1196
+ * @extends {goog.soy.data.SanitizedContent}
1197
+ */
1198
+ soydata.SanitizedJs = function() {
1199
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1200
+ };
1201
+ goog.inherits(soydata.SanitizedJs, goog.soy.data.SanitizedContent);
1202
+
1203
+ /** @override */
1204
+ soydata.SanitizedJs.prototype.contentKind =
1205
+ soydata.SanitizedContentKind.JS;
1206
+
1207
+ /** @override */
1208
+ soydata.SanitizedJs.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
1209
+
1210
+
1211
+ /**
1212
+ * Content of type {@link soydata.SanitizedContentKind.JS_STR_CHARS}.
1213
+ *
1214
+ * The content can be safely inserted as part of a single- or double-quoted
1215
+ * string without terminating the string. The default content direction is
1216
+ * unknown, i.e. to be estimated when necessary.
1217
+ *
1218
+ * @constructor
1219
+ * @extends {goog.soy.data.SanitizedContent}
1220
+ */
1221
+ soydata.SanitizedJsStrChars = function() {
1222
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1223
+ };
1224
+ goog.inherits(soydata.SanitizedJsStrChars, goog.soy.data.SanitizedContent);
1225
+
1226
+ /** @override */
1227
+ soydata.SanitizedJsStrChars.prototype.contentKind =
1228
+ soydata.SanitizedContentKind.JS_STR_CHARS;
1229
+
1230
+ /**
1231
+ * Content of type {@link soydata.SanitizedContentKind.URI}.
1232
+ *
1233
+ * The content is a URI chunk that the caller knows is safe to emit in a
1234
+ * template. The content direction is LTR.
1235
+ *
1236
+ * @constructor
1237
+ * @extends {goog.soy.data.SanitizedContent}
1238
+ */
1239
+ soydata.SanitizedUri = function() {
1240
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1241
+ };
1242
+ goog.inherits(soydata.SanitizedUri, goog.soy.data.SanitizedContent);
1243
+
1244
+ /** @override */
1245
+ soydata.SanitizedUri.prototype.contentKind = soydata.SanitizedContentKind.URI;
1246
+
1247
+ /** @override */
1248
+ soydata.SanitizedUri.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
1249
+
1250
+
1251
+ /**
1252
+ * Content of type {@link soydata.SanitizedContentKind.ATTRIBUTES}.
1253
+ *
1254
+ * The content should be safely embeddable within an open tag, such as a
1255
+ * key="value" pair. The content direction is LTR.
1256
+ *
1257
+ * @constructor
1258
+ * @extends {goog.soy.data.SanitizedContent}
1259
+ */
1260
+ soydata.SanitizedHtmlAttribute = function() {
1261
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1262
+ };
1263
+ goog.inherits(soydata.SanitizedHtmlAttribute, goog.soy.data.SanitizedContent);
1264
+
1265
+ /** @override */
1266
+ soydata.SanitizedHtmlAttribute.prototype.contentKind =
1267
+ soydata.SanitizedContentKind.ATTRIBUTES;
1268
+
1269
+ /** @override */
1270
+ soydata.SanitizedHtmlAttribute.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
1271
+
1272
+
1273
+ /**
1274
+ * Content of type {@link soydata.SanitizedContentKind.CSS}.
1275
+ *
1276
+ * The content is non-attacker-exploitable CSS, such as {@code color:#c3d9ff}.
1277
+ * The content direction is LTR.
1278
+ *
1279
+ * @constructor
1280
+ * @extends {goog.soy.data.SanitizedContent}
1281
+ */
1282
+ soydata.SanitizedCss = function() {
1283
+ goog.soy.data.SanitizedContent.call(this); // Throws an exception.
1284
+ };
1285
+ goog.inherits(soydata.SanitizedCss, goog.soy.data.SanitizedContent);
1286
+
1287
+ /** @override */
1288
+ soydata.SanitizedCss.prototype.contentKind =
1289
+ soydata.SanitizedContentKind.CSS;
1290
+
1291
+ /** @override */
1292
+ soydata.SanitizedCss.prototype.contentDir = goog.i18n.bidi.Dir.LTR;
1293
+
1294
+
1295
+ /**
1296
+ * Unsanitized plain text string.
1297
+ *
1298
+ * While all strings are effectively safe to use as a plain text, there are no
1299
+ * guarantees about safety in any other context such as HTML. This is
1300
+ * sometimes used to mark that should never be used unescaped.
1301
+ *
1302
+ * @param {*} content Plain text with no guarantees.
1303
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1304
+ * unknown and thus to be estimated when necessary. Default: null.
1305
+ * @constructor
1306
+ * @extends {goog.soy.data.SanitizedContent}
1307
+ */
1308
+ soydata.UnsanitizedText = function(content, opt_contentDir) {
1309
+ /** @override */
1310
+ this.content = String(content);
1311
+ this.contentDir = opt_contentDir != null ? opt_contentDir : null;
1312
+ };
1313
+ goog.inherits(soydata.UnsanitizedText, goog.soy.data.SanitizedContent);
1314
+
1315
+ /** @override */
1316
+ soydata.UnsanitizedText.prototype.contentKind =
1317
+ soydata.SanitizedContentKind.TEXT;
1318
+
1319
+
1320
+ /**
1321
+ * Empty string, used as a type in Soy templates.
1322
+ * @enum {string}
1323
+ * @private
1324
+ */
1325
+ soydata.$$EMPTY_STRING_ = {
1326
+ VALUE: ''
1327
+ };
862
1328
 
863
- /** A properly encoded portion of a URI. */
864
- URI: 2,
865
1329
 
866
- /** An attribute name and value such as {@code dir="ltr"}. */
867
- HTML_ATTRIBUTE: 3
1330
+ /**
1331
+ * Creates a factory for SanitizedContent types.
1332
+ *
1333
+ * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can
1334
+ * instantiate Sanitized* classes, without making the Sanitized* constructors
1335
+ * publicly usable. Requiring all construction to use the VERY_UNSAFE names
1336
+ * helps callers and their reviewers easily tell that creating SanitizedContent
1337
+ * is not always safe and calls for careful review.
1338
+ *
1339
+ * @param {function(new: T)} ctor A constructor.
1340
+ * @return {!function(*, ?goog.i18n.bidi.Dir=): T} A factory that takes
1341
+ * content and an optional content direction and returns a new instance. If
1342
+ * the content direction is undefined, ctor.prototype.contentDir is used.
1343
+ * @template T
1344
+ * @private
1345
+ */
1346
+ soydata.$$makeSanitizedContentFactory_ = function(ctor) {
1347
+ /** @type {function(new: goog.soy.data.SanitizedContent)} */
1348
+ function InstantiableCtor() {}
1349
+ InstantiableCtor.prototype = ctor.prototype;
1350
+ /**
1351
+ * Creates a ctor-type SanitizedContent instance.
1352
+ *
1353
+ * @param {*} content The content to put in the instance.
1354
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If
1355
+ * undefined, ctor.prototype.contentDir is used.
1356
+ * @return {goog.soy.data.SanitizedContent} The new instance. It is actually
1357
+ * of type T above (ctor's type, a descendant of SanitizedContent), but
1358
+ * there is no way to express that here.
1359
+ */
1360
+ function sanitizedContentFactory(content, opt_contentDir) {
1361
+ var result = new InstantiableCtor();
1362
+ result.content = String(content);
1363
+ if (opt_contentDir !== undefined) {
1364
+ result.contentDir = opt_contentDir;
1365
+ }
1366
+ return result;
1367
+ }
1368
+ return sanitizedContentFactory;
868
1369
  };
869
1370
 
870
1371
 
871
1372
  /**
872
- * A string-like object that carries a content-type.
873
- * @param {string} content
874
- * @constructor
1373
+ * Creates a factory for SanitizedContent types that should always have their
1374
+ * default directionality.
1375
+ *
1376
+ * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can
1377
+ * instantiate Sanitized* classes, without making the Sanitized* constructors
1378
+ * publicly usable. Requiring all construction to use the VERY_UNSAFE names
1379
+ * helps callers and their reviewers easily tell that creating SanitizedContent
1380
+ * is not always safe and calls for careful review.
1381
+ *
1382
+ * @param {function(new: T, string)} ctor A constructor.
1383
+ * @return {!function(*): T} A factory that takes content and returns a new
1384
+ * instance (with default directionality, i.e. ctor.prototype.contentDir).
1385
+ * @template T
875
1386
  * @private
876
1387
  */
877
- soydata.SanitizedContent = function(content) {
1388
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_ = function(ctor) {
1389
+ /** @type {function(new: goog.soy.data.SanitizedContent)} */
1390
+ function InstantiableCtor() {}
1391
+ InstantiableCtor.prototype = ctor.prototype;
878
1392
  /**
879
- * The textual content.
880
- * @type {string}
1393
+ * Creates a ctor-type SanitizedContent instance.
1394
+ *
1395
+ * @param {*} content The content to put in the instance.
1396
+ * @return {goog.soy.data.SanitizedContent} The new instance. It is actually
1397
+ * of type T above (ctor's type, a descendant of SanitizedContent), but
1398
+ * there is no way to express that here.
881
1399
  */
882
- this.content = content;
1400
+ function sanitizedContentFactory(content) {
1401
+ var result = new InstantiableCtor();
1402
+ result.content = String(content);
1403
+ return result;
1404
+ }
1405
+ return sanitizedContentFactory;
883
1406
  };
884
1407
 
885
- /** @type {soydata.SanitizedContentKind} */
886
- soydata.SanitizedContent.prototype.contentKind;
887
1408
 
888
- /** @override */
889
- soydata.SanitizedContent.prototype.toString = function() {
890
- return this.content;
1409
+ // -----------------------------------------------------------------------------
1410
+ // Sanitized content ordainers. Please use these with extreme caution (with the
1411
+ // exception of markUnsanitizedText). A good recommendation is to limit usage
1412
+ // of these to just a handful of files in your source tree where usages can be
1413
+ // carefully audited.
1414
+
1415
+
1416
+ /**
1417
+ * Protects a string from being used in an noAutoescaped context.
1418
+ *
1419
+ * This is useful for content where there is significant risk of accidental
1420
+ * unescaped usage in a Soy template. A great case is for user-controlled
1421
+ * data that has historically been a source of vulernabilities.
1422
+ *
1423
+ * @param {*} content Text to protect.
1424
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1425
+ * unknown and thus to be estimated when necessary. Default: null.
1426
+ * @return {!soydata.UnsanitizedText} A wrapper that is rejected by the
1427
+ * Soy noAutoescape print directive.
1428
+ */
1429
+ soydata.markUnsanitizedText = function(content, opt_contentDir) {
1430
+ return new soydata.UnsanitizedText(content, opt_contentDir);
891
1431
  };
892
1432
 
893
1433
 
894
1434
  /**
895
- * Content of type {@link soydata.SanitizedContentKind.HTML}.
896
- * @param {string} content A string of HTML that can safely be embedded in
897
- * a PCDATA context in your app. If you would be surprised to find that an
1435
+ * Takes a leap of faith that the provided content is "safe" HTML.
1436
+ *
1437
+ * @param {*} content A string of HTML that can safely be embedded in
1438
+ * a PCDATA context in your app. If you would be surprised to find that an
898
1439
  * HTML sanitizer produced {@code s} (e.g. it runs code or fetches bad URLs)
899
1440
  * and you wouldn't write a template that produces {@code s} on security or
900
1441
  * privacy grounds, then don't pass {@code s} here.
901
- * @constructor
902
- * @extends {soydata.SanitizedContent}
1442
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1443
+ * unknown and thus to be estimated when necessary. Default: null.
1444
+ * @return {!soydata.SanitizedHtml} Sanitized content wrapper that
1445
+ * indicates to Soy not to escape when printed as HTML.
903
1446
  */
904
- soydata.SanitizedHtml = function(content) {
905
- soydata.SanitizedContent.call(this, content);
906
- };
907
- goog.inherits(soydata.SanitizedHtml, soydata.SanitizedContent);
908
-
909
- /** @override */
910
- soydata.SanitizedHtml.prototype.contentKind = soydata.SanitizedContentKind.HTML;
1447
+ soydata.VERY_UNSAFE.ordainSanitizedHtml =
1448
+ soydata.$$makeSanitizedContentFactory_(soydata.SanitizedHtml);
911
1449
 
912
1450
 
913
1451
  /**
914
- * Content of type {@link soydata.SanitizedContentKind.JS_STR_CHARS}.
915
- * @param {string} content A string of JS that when evaled, produces a
916
- * value that does not depend on any sensitive data and has no side effects
917
- * <b>OR</b> a string of JS that does not reference any variables or have
918
- * any side effects not known statically to the app authors.
919
- * @constructor
920
- * @extends {soydata.SanitizedContent}
1452
+ * Takes a leap of faith that the provided content is "safe" (non-attacker-
1453
+ * controlled, XSS-free) Javascript.
1454
+ *
1455
+ * @param {*} content Javascript source that when evaluated does not
1456
+ * execute any attacker-controlled scripts.
1457
+ * @return {!soydata.SanitizedJs} Sanitized content wrapper that indicates to
1458
+ * Soy not to escape when printed as Javascript source.
921
1459
  */
922
- soydata.SanitizedJsStrChars = function(content) {
923
- soydata.SanitizedContent.call(this, content);
924
- };
925
- goog.inherits(soydata.SanitizedJsStrChars, soydata.SanitizedContent);
1460
+ soydata.VERY_UNSAFE.ordainSanitizedJs =
1461
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
1462
+ soydata.SanitizedJs);
926
1463
 
927
- /** @override */
928
- soydata.SanitizedJsStrChars.prototype.contentKind =
929
- soydata.SanitizedContentKind.JS_STR_CHARS;
1464
+
1465
+ // TODO: This function is probably necessary, either externally or internally
1466
+ // as an implementation detail. Generally, plain text will always work here,
1467
+ // as there's no harm to unescaping the string and then re-escaping when
1468
+ // finally printed.
1469
+ /**
1470
+ * Takes a leap of faith that the provided content can be safely embedded in
1471
+ * a Javascript string without re-esacping.
1472
+ *
1473
+ * @param {*} content Content that can be safely inserted as part of a
1474
+ * single- or double-quoted string without terminating the string.
1475
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1476
+ * unknown and thus to be estimated when necessary. Default: null.
1477
+ * @return {!soydata.SanitizedJsStrChars} Sanitized content wrapper that
1478
+ * indicates to Soy not to escape when printed in a JS string.
1479
+ */
1480
+ soydata.VERY_UNSAFE.ordainSanitizedJsStrChars =
1481
+ soydata.$$makeSanitizedContentFactory_(soydata.SanitizedJsStrChars);
930
1482
 
931
1483
 
932
1484
  /**
933
- * Content of type {@link soydata.SanitizedContentKind.URI}.
934
- * @param {string} content A chunk of URI that the caller knows is safe to
1485
+ * Takes a leap of faith that the provided content is "safe" to use as a URI
1486
+ * in a Soy template.
1487
+ *
1488
+ * This creates a Soy SanitizedContent object which indicates to Soy there is
1489
+ * no need to escape it when printed as a URI (e.g. in an href or src
1490
+ * attribute), such as if it's already been encoded or if it's a Javascript:
1491
+ * URI.
1492
+ *
1493
+ * @param {*} content A chunk of URI that the caller knows is safe to
935
1494
  * emit in a template.
936
- * @constructor
937
- * @extends {soydata.SanitizedContent}
1495
+ * @return {!soydata.SanitizedUri} Sanitized content wrapper that indicates to
1496
+ * Soy not to escape or filter when printed in URI context.
938
1497
  */
939
- soydata.SanitizedUri = function(content) {
940
- soydata.SanitizedContent.call(this, content);
941
- };
942
- goog.inherits(soydata.SanitizedUri, soydata.SanitizedContent);
943
-
944
- /** @override */
945
- soydata.SanitizedUri.prototype.contentKind = soydata.SanitizedContentKind.URI;
1498
+ soydata.VERY_UNSAFE.ordainSanitizedUri =
1499
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
1500
+ soydata.SanitizedUri);
946
1501
 
947
1502
 
948
1503
  /**
949
- * Content of type {@link soydata.SanitizedContentKind.HTML_ATTRIBUTE}.
950
- * @param {string} content An attribute name and value, such as
1504
+ * Takes a leap of faith that the provided content is "safe" to use as an
1505
+ * HTML attribute.
1506
+ *
1507
+ * @param {*} content An attribute name and value, such as
951
1508
  * {@code dir="ltr"}.
952
- * @constructor
953
- * @extends {soydata.SanitizedContent}
1509
+ * @return {!soydata.SanitizedHtmlAttribute} Sanitized content wrapper that
1510
+ * indicates to Soy not to escape when printed as an HTML attribute.
954
1511
  */
955
- soydata.SanitizedHtmlAttribute = function(content) {
956
- soydata.SanitizedContent.call(this, content);
957
- };
958
- goog.inherits(soydata.SanitizedHtmlAttribute, soydata.SanitizedContent);
1512
+ soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute =
1513
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
1514
+ soydata.SanitizedHtmlAttribute);
959
1515
 
960
- /** @override */
961
- soydata.SanitizedHtmlAttribute.prototype.contentKind =
962
- soydata.SanitizedContentKind.HTML_ATTRIBUTE;
1516
+
1517
+ /**
1518
+ * Takes a leap of faith that the provided content is "safe" to use as CSS
1519
+ * in a style attribute or block.
1520
+ *
1521
+ * @param {*} content CSS, such as {@code color:#c3d9ff}.
1522
+ * @return {!soydata.SanitizedCss} Sanitized CSS wrapper that indicates to
1523
+ * Soy there is no need to escape or filter when printed in CSS context.
1524
+ */
1525
+ soydata.VERY_UNSAFE.ordainSanitizedCss =
1526
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_(
1527
+ soydata.SanitizedCss);
963
1528
 
964
1529
 
965
1530
  // -----------------------------------------------------------------------------
@@ -1036,32 +1601,54 @@ soy.renderAsElement = function(
1036
1601
 
1037
1602
 
1038
1603
  /**
1039
- * Builds an augmented data object to be passed when a template calls another,
1040
- * and needs to pass both original data and additional params. The returned
1041
- * object will contain both the original data and the additional params. If the
1042
- * same key appears in both, then the value from the additional params will be
1043
- * visible, while the value from the original data will be hidden. The original
1044
- * data object will be used, but not modified.
1604
+ * Whether the locale is right-to-left.
1605
+ *
1606
+ * @type {boolean}
1607
+ */
1608
+ soy.$$IS_LOCALE_RTL = goog.i18n.bidi.IS_RTL;
1609
+
1610
+
1611
+ /**
1612
+ * Builds an augmented map. The returned map will contain mappings from both
1613
+ * the base map and the additional map. If the same key appears in both, then
1614
+ * the value from the additional map will be visible, while the value from the
1615
+ * base map will be hidden. The base map will be used, but not modified.
1045
1616
  *
1046
- * @param {!Object} origData The original data to pass.
1047
- * @param {Object} additionalParams The additional params to pass.
1048
- * @return {Object} An augmented data object containing both the original data
1049
- * and the additional params.
1617
+ * @param {!Object} baseMap The original map to augment.
1618
+ * @param {!Object} additionalMap A map containing the additional mappings.
1619
+ * @return {!Object} An augmented map containing both the original and
1620
+ * additional mappings.
1050
1621
  */
1051
- soy.$$augmentData = function(origData, additionalParams) {
1622
+ soy.$$augmentMap = function(baseMap, additionalMap) {
1052
1623
 
1053
- // Create a new object whose '__proto__' field is set to origData.
1624
+ // Create a new map whose '__proto__' field is set to baseMap.
1054
1625
  /** @constructor */
1055
1626
  function TempCtor() {}
1056
- TempCtor.prototype = origData;
1057
- var newData = new TempCtor();
1627
+ TempCtor.prototype = baseMap;
1628
+ var augmentedMap = new TempCtor();
1058
1629
 
1059
- // Add the additional params to the new object.
1060
- for (var key in additionalParams) {
1061
- newData[key] = additionalParams[key];
1630
+ // Add the additional mappings to the new map.
1631
+ for (var key in additionalMap) {
1632
+ augmentedMap[key] = additionalMap[key];
1062
1633
  }
1063
1634
 
1064
- return newData;
1635
+ return augmentedMap;
1636
+ };
1637
+
1638
+
1639
+ /**
1640
+ * Checks that the given map key is a string.
1641
+ * @param {*} key Key to check.
1642
+ * @return {string} The given key.
1643
+ */
1644
+ soy.$$checkMapKey = function(key) {
1645
+ // TODO: Support map literal with nonstring key.
1646
+ if ((typeof key) != 'string') {
1647
+ throw Error(
1648
+ 'Map literal\'s key expression must evaluate to string' +
1649
+ ' (encountered type "' + (typeof key) + '").');
1650
+ }
1651
+ return key;
1065
1652
  };
1066
1653
 
1067
1654
 
@@ -1097,13 +1684,13 @@ soy.$$getMapKeys = function(map) {
1097
1684
  *
1098
1685
  * @consistentIdGenerator
1099
1686
  */
1100
- soy.$$getDelegateId = function(delTemplateName) {
1687
+ soy.$$getDelTemplateId = function(delTemplateName) {
1101
1688
  return delTemplateName;
1102
1689
  };
1103
1690
 
1104
1691
 
1105
1692
  /**
1106
- * Map from registered delegate template id/name to the priority of the
1693
+ * Map from registered delegate template key to the priority of the
1107
1694
  * implementation.
1108
1695
  * @type {Object}
1109
1696
  * @private
@@ -1111,7 +1698,7 @@ soy.$$getDelegateId = function(delTemplateName) {
1111
1698
  soy.$$DELEGATE_REGISTRY_PRIORITIES_ = {};
1112
1699
 
1113
1700
  /**
1114
- * Map from registered delegate template id/name to the implementation function.
1701
+ * Map from registered delegate template key to the implementation function.
1115
1702
  * @type {Object}
1116
1703
  * @private
1117
1704
  */
@@ -1119,17 +1706,21 @@ soy.$$DELEGATE_REGISTRY_FUNCTIONS_ = {};
1119
1706
 
1120
1707
 
1121
1708
  /**
1122
- * Registers a delegate implementation. If the same delegate template id/name
1123
- * has been registered previously, then priority values are compared and only
1124
- * the higher priority implementation is stored (if priorities are equal, an
1125
- * error is thrown).
1709
+ * Registers a delegate implementation. If the same delegate template key (id
1710
+ * and variant) has been registered previously, then priority values are
1711
+ * compared and only the higher priority implementation is stored (if
1712
+ * priorities are equal, an error is thrown).
1126
1713
  *
1127
- * @param {string} delTemplateId The delegate template id/name to register.
1714
+ * @param {string} delTemplateId The delegate template id.
1715
+ * @param {string} delTemplateVariant The delegate template variant (can be
1716
+ * empty string).
1128
1717
  * @param {number} delPriority The implementation's priority value.
1129
1718
  * @param {Function} delFn The implementation function.
1130
1719
  */
1131
- soy.$$registerDelegateFn = function(delTemplateId, delPriority, delFn) {
1132
- var mapKey = 'key_' + delTemplateId;
1720
+ soy.$$registerDelegateFn = function(
1721
+ delTemplateId, delTemplateVariant, delPriority, delFn) {
1722
+
1723
+ var mapKey = 'key_' + delTemplateId + ':' + delTemplateVariant;
1133
1724
  var currPriority = soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey];
1134
1725
  if (currPriority === undefined || delPriority > currPriority) {
1135
1726
  // Registering new or higher-priority function: replace registry entry.
@@ -1138,8 +1729,8 @@ soy.$$registerDelegateFn = function(delTemplateId, delPriority, delFn) {
1138
1729
  } else if (delPriority == currPriority) {
1139
1730
  // Registering same-priority function: error.
1140
1731
  throw Error(
1141
- 'Encountered two active delegates with same priority (id/name "' +
1142
- delTemplateId + '").');
1732
+ 'Encountered two active delegates with the same priority ("' +
1733
+ delTemplateId + ':' + delTemplateVariant + '").');
1143
1734
  } else {
1144
1735
  // Registering lower-priority function: do nothing.
1145
1736
  }
@@ -1148,16 +1739,38 @@ soy.$$registerDelegateFn = function(delTemplateId, delPriority, delFn) {
1148
1739
 
1149
1740
  /**
1150
1741
  * Retrieves the (highest-priority) implementation that has been registered for
1151
- * a given delegate template id/name. If no implementation has been registered
1152
- * for the id/name, then returns an implementation that is equivalent to an
1153
- * empty template (i.e. rendered output would be empty string).
1742
+ * a given delegate template key (id and variant). If no implementation has
1743
+ * been registered for the key, then the fallback is the same id with empty
1744
+ * variant. If the fallback is also not registered, and allowsEmptyDefault is
1745
+ * true, then returns an implementation that is equivalent to an empty template
1746
+ * (i.e. rendered output would be empty string).
1154
1747
  *
1155
- * @param {string} delTemplateId The delegate template id/name to get.
1748
+ * @param {string} delTemplateId The delegate template id.
1749
+ * @param {string|number} delTemplateVariant The delegate template variant (can
1750
+ * be an empty string, or a number when a global is used).
1751
+ * @param {boolean} allowsEmptyDefault Whether to default to the empty template
1752
+ * function if there's no active implementation.
1156
1753
  * @return {Function} The retrieved implementation function.
1157
1754
  */
1158
- soy.$$getDelegateFn = function(delTemplateId) {
1159
- var delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId];
1160
- return delFn ? delFn : soy.$$EMPTY_TEMPLATE_FN_;
1755
+ soy.$$getDelegateFn = function(
1756
+ delTemplateId, delTemplateVariant, allowsEmptyDefault) {
1757
+
1758
+ var delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_[
1759
+ 'key_' + delTemplateId + ':' + delTemplateVariant];
1760
+ if (! delFn && delTemplateVariant != '') {
1761
+ // Fallback to empty variant.
1762
+ delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId + ':'];
1763
+ }
1764
+
1765
+ if (delFn) {
1766
+ return delFn;
1767
+ } else if (allowsEmptyDefault) {
1768
+ return soy.$$EMPTY_TEMPLATE_FN_;
1769
+ } else {
1770
+ throw Error(
1771
+ 'Found no active impl for delegate call to "' + delTemplateId + ':' +
1772
+ delTemplateVariant + '" (and not allowemptydefault="true").');
1773
+ }
1161
1774
  };
1162
1775
 
1163
1776
 
@@ -1176,26 +1789,220 @@ soy.$$EMPTY_TEMPLATE_FN_ = function(opt_data, opt_sb, opt_ijData) {
1176
1789
  };
1177
1790
 
1178
1791
 
1792
+ // -----------------------------------------------------------------------------
1793
+ // Internal sanitized content wrappers.
1794
+
1795
+
1796
+ /**
1797
+ * Creates a SanitizedContent factory for SanitizedContent types for internal
1798
+ * Soy let and param blocks.
1799
+ *
1800
+ * This is a hack within Soy so that SanitizedContent objects created via let
1801
+ * and param blocks will truth-test as false if they are empty string.
1802
+ * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is
1803
+ * not possible, and changing the Soy compiler to wrap every boolean statement
1804
+ * for just this purpose is impractical. Instead, we just avoid wrapping empty
1805
+ * string as SanitizedContent, since it's a no-op for empty strings anyways.
1806
+ *
1807
+ * @param {function(new: T)} ctor A constructor.
1808
+ * @return {!function(*, ?goog.i18n.bidi.Dir=): (T|soydata.$$EMPTY_STRING_)}
1809
+ * A factory that takes content and an optional content direction and
1810
+ * returns a new instance, or an empty string. If the content direction is
1811
+ * undefined, ctor.prototype.contentDir is used.
1812
+ * @template T
1813
+ * @private
1814
+ */
1815
+ soydata.$$makeSanitizedContentFactoryForInternalBlocks_ = function(ctor) {
1816
+ /** @type {function(new: goog.soy.data.SanitizedContent)} */
1817
+ function InstantiableCtor() {}
1818
+ InstantiableCtor.prototype = ctor.prototype;
1819
+ /**
1820
+ * Creates a ctor-type SanitizedContent instance.
1821
+ *
1822
+ * @param {*} content The content to put in the instance.
1823
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If
1824
+ * undefined, ctor.prototype.contentDir is used.
1825
+ * @return {goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new
1826
+ * instance, or an empty string. A new instance is actually of type T
1827
+ * above (ctor's type, a descendant of SanitizedContent), but there's no
1828
+ * way to express that here.
1829
+ */
1830
+ function sanitizedContentFactory(content, opt_contentDir) {
1831
+ var contentString = String(content);
1832
+ if (!contentString) {
1833
+ return soydata.$$EMPTY_STRING_.VALUE;
1834
+ }
1835
+ var result = new InstantiableCtor();
1836
+ result.content = String(content);
1837
+ if (opt_contentDir !== undefined) {
1838
+ result.contentDir = opt_contentDir;
1839
+ }
1840
+ return result;
1841
+ }
1842
+ return sanitizedContentFactory;
1843
+ };
1844
+
1845
+
1846
+ /**
1847
+ * Creates a SanitizedContent factory for SanitizedContent types that should
1848
+ * always have their default directionality for internal Soy let and param
1849
+ * blocks.
1850
+ *
1851
+ * This is a hack within Soy so that SanitizedContent objects created via let
1852
+ * and param blocks will truth-test as false if they are empty string.
1853
+ * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is
1854
+ * not possible, and changing the Soy compiler to wrap every boolean statement
1855
+ * for just this purpose is impractical. Instead, we just avoid wrapping empty
1856
+ * string as SanitizedContent, since it's a no-op for empty strings anyways.
1857
+ *
1858
+ * @param {function(new: T)} ctor A constructor.
1859
+ * @return {!function(*): (T|soydata.$$EMPTY_STRING_)} A
1860
+ * factory that takes content and returns a
1861
+ * new instance (with default directionality, i.e.
1862
+ * ctor.prototype.contentDir), or an empty string.
1863
+ * @template T
1864
+ * @private
1865
+ */
1866
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_ =
1867
+ function(ctor) {
1868
+ /** @type {function(new: goog.soy.data.SanitizedContent)} */
1869
+ function InstantiableCtor() {}
1870
+ InstantiableCtor.prototype = ctor.prototype;
1871
+ /**
1872
+ * Creates a ctor-type SanitizedContent instance.
1873
+ *
1874
+ * @param {*} content The content to put in the instance.
1875
+ * @return {goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new
1876
+ * instance, or an empty string. A new instance is actually of type T
1877
+ * above (ctor's type, a descendant of SanitizedContent), but there's no
1878
+ * way to express that here.
1879
+ */
1880
+ function sanitizedContentFactory(content) {
1881
+ var contentString = String(content);
1882
+ if (!contentString) {
1883
+ return soydata.$$EMPTY_STRING_.VALUE;
1884
+ }
1885
+ var result = new InstantiableCtor();
1886
+ result.content = String(content);
1887
+ return result;
1888
+ }
1889
+ return sanitizedContentFactory;
1890
+ };
1891
+
1892
+
1893
+ /**
1894
+ * Creates kind="text" block contents (internal use only).
1895
+ *
1896
+ * @param {*} content Text.
1897
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1898
+ * unknown and thus to be estimated when necessary. Default: null.
1899
+ * @return {!soydata.UnsanitizedText|soydata.$$EMPTY_STRING_} Wrapped result.
1900
+ */
1901
+ soydata.$$markUnsanitizedTextForInternalBlocks = function(
1902
+ content, opt_contentDir) {
1903
+ var contentString = String(content);
1904
+ if (!contentString) {
1905
+ return soydata.$$EMPTY_STRING_.VALUE;
1906
+ }
1907
+ return new soydata.UnsanitizedText(contentString, opt_contentDir);
1908
+ };
1909
+
1910
+
1911
+ /**
1912
+ * Creates kind="html" block contents (internal use only).
1913
+ *
1914
+ * @param {*} content Text.
1915
+ * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if
1916
+ * unknown and thus to be estimated when necessary. Default: null.
1917
+ * @return {soydata.SanitizedHtml|soydata.$$EMPTY_STRING_} Wrapped result.
1918
+ */
1919
+ soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks =
1920
+ soydata.$$makeSanitizedContentFactoryForInternalBlocks_(
1921
+ soydata.SanitizedHtml);
1922
+
1923
+
1924
+ /**
1925
+ * Creates kind="js" block contents (internal use only).
1926
+ *
1927
+ * @param {*} content Text.
1928
+ * @return {soydata.SanitizedJs|soydata.$$EMPTY_STRING_} Wrapped result.
1929
+ */
1930
+ soydata.VERY_UNSAFE.$$ordainSanitizedJsForInternalBlocks =
1931
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
1932
+ soydata.SanitizedJs);
1933
+
1934
+
1935
+ /**
1936
+ * Creates kind="uri" block contents (internal use only).
1937
+ *
1938
+ * @param {*} content Text.
1939
+ * @return {soydata.SanitizedUri|soydata.$$EMPTY_STRING_} Wrapped result.
1940
+ */
1941
+ soydata.VERY_UNSAFE.$$ordainSanitizedUriForInternalBlocks =
1942
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
1943
+ soydata.SanitizedUri);
1944
+
1945
+
1946
+ /**
1947
+ * Creates kind="attributes" block contents (internal use only).
1948
+ *
1949
+ * @param {*} content Text.
1950
+ * @return {soydata.SanitizedHtmlAttribute|soydata.$$EMPTY_STRING_} Wrapped
1951
+ * result.
1952
+ */
1953
+ soydata.VERY_UNSAFE.$$ordainSanitizedAttributesForInternalBlocks =
1954
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
1955
+ soydata.SanitizedHtmlAttribute);
1956
+
1957
+
1958
+ /**
1959
+ * Creates kind="css" block contents (internal use only).
1960
+ *
1961
+ * @param {*} content Text.
1962
+ * @return {soydata.SanitizedCss|soydata.$$EMPTY_STRING_} Wrapped result.
1963
+ */
1964
+ soydata.VERY_UNSAFE.$$ordainSanitizedCssForInternalBlocks =
1965
+ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_(
1966
+ soydata.SanitizedCss);
1967
+
1968
+
1179
1969
  // -----------------------------------------------------------------------------
1180
1970
  // Escape/filter/normalize.
1181
1971
 
1182
1972
 
1183
1973
  /**
1184
- * Escapes HTML special characters in a string. Escapes double quote '"' in
1185
- * addition to '&', '<', and '>' so that a string can be included in an HTML
1186
- * tag attribute value within double quotes.
1187
- * Will emit known safe HTML as-is.
1974
+ * Returns a SanitizedHtml object for a particular value. The content direction
1975
+ * is preserved.
1188
1976
  *
1189
- * @param {*} value The string-like value to be escaped. May not be a string,
1190
- * but the value will be coerced to a string.
1191
- * @return {string} An escaped version of value.
1977
+ * This HTML-escapes the value unless it is already SanitizedHtml. Escapes
1978
+ * double quote '"' in addition to '&', '<', and '>' so that a string can be
1979
+ * included in an HTML tag attribute value within double quotes.
1980
+ *
1981
+ * @param {*} value The value to convert. If it is already a SanitizedHtml
1982
+ * object, it is left alone.
1983
+ * @return {!soydata.SanitizedHtml} An escaped version of value.
1192
1984
  */
1193
1985
  soy.$$escapeHtml = function(value) {
1194
- if (typeof value === 'object' && value &&
1195
- value.contentKind === soydata.SanitizedContentKind.HTML) {
1196
- return value.content;
1986
+ return soydata.SanitizedHtml.from(value);
1987
+ };
1988
+
1989
+
1990
+ /**
1991
+ * Strips unsafe tags to convert a string of untrusted HTML into HTML that
1992
+ * is safe to embed. The content direction is preserved.
1993
+ *
1994
+ * @param {*} value The string-like value to be escaped. May not be a string,
1995
+ * but the value will be coerced to a string.
1996
+ * @return {!soydata.SanitizedHtml} A sanitized and normalized version of value.
1997
+ */
1998
+ soy.$$cleanHtml = function(value) {
1999
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2000
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtml);
2001
+ return /** @type {!soydata.SanitizedHtml} */ (value);
1197
2002
  }
1198
- return soy.esc.$$escapeHtmlHelper(value);
2003
+ return soydata.VERY_UNSAFE.ordainSanitizedHtml(
2004
+ soy.$$stripHtmlTags(value, soy.esc.$$SAFE_TAG_WHITELIST_),
2005
+ soydata.getContentDir(value));
1199
2006
  };
1200
2007
 
1201
2008
 
@@ -1204,7 +2011,7 @@ soy.$$escapeHtml = function(value) {
1204
2011
  * RCDATA.
1205
2012
  * <p>
1206
2013
  * Escapes HTML special characters so that the value will not prematurely end
1207
- * the body of a tag like {@code <textarea>} or {@code <title>}. RCDATA tags
2014
+ * the body of a tag like {@code <textarea>} or {@code <title>}. RCDATA tags
1208
2015
  * cannot contain other HTML entities, so it is not strictly necessary to escape
1209
2016
  * HTML special characters except when part of that text looks like an HTML
1210
2017
  * entity or like a close tag : {@code </textarea>}.
@@ -1213,13 +2020,13 @@ soy.$$escapeHtml = function(value) {
1213
2020
  * contain an innocuous {@code </textarea>} don't prematurely end an RCDATA
1214
2021
  * element.
1215
2022
  *
1216
- * @param {*} value The string-like value to be escaped. May not be a string,
2023
+ * @param {*} value The string-like value to be escaped. May not be a string,
1217
2024
  * but the value will be coerced to a string.
1218
2025
  * @return {string} An escaped version of value.
1219
2026
  */
1220
2027
  soy.$$escapeHtmlRcdata = function(value) {
1221
- if (typeof value === 'object' && value &&
1222
- value.contentKind === soydata.SanitizedContentKind.HTML) {
2028
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2029
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtml);
1223
2030
  return soy.esc.$$normalizeHtmlHelper(value.content);
1224
2031
  }
1225
2032
  return soy.esc.$$escapeHtmlHelper(value);
@@ -1227,29 +2034,133 @@ soy.$$escapeHtmlRcdata = function(value) {
1227
2034
 
1228
2035
 
1229
2036
  /**
1230
- * Removes HTML tags from a string of known safe HTML so it can be used as an
1231
- * attribute value.
2037
+ * Matches any/only HTML5 void elements' start tags.
2038
+ * See http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
2039
+ * @type {RegExp}
2040
+ * @private
2041
+ */
2042
+ soy.$$HTML5_VOID_ELEMENTS_ = new RegExp(
2043
+ '^<(?:area|base|br|col|command|embed|hr|img|input' +
2044
+ '|keygen|link|meta|param|source|track|wbr)\\b');
2045
+
2046
+
2047
+ /**
2048
+ * Removes HTML tags from a string of known safe HTML.
2049
+ * If opt_tagWhitelist is not specified or is empty, then
2050
+ * the result can be used as an attribute value.
1232
2051
  *
1233
- * @param {*} value The HTML to be escaped. May not be a string, but the
2052
+ * @param {*} value The HTML to be escaped. May not be a string, but the
1234
2053
  * value will be coerced to a string.
1235
- * @return {string} A representation of value without tags, HTML comments, or
1236
- * other content.
2054
+ * @param {Object.<string, number>=} opt_tagWhitelist Has an own property whose
2055
+ * name is a lower-case tag name and whose value is {@code 1} for
2056
+ * each element that is allowed in the output.
2057
+ * @return {string} A representation of value without disallowed tags,
2058
+ * HTML comments, or other non-text content.
2059
+ */
2060
+ soy.$$stripHtmlTags = function(value, opt_tagWhitelist) {
2061
+ if (!opt_tagWhitelist) {
2062
+ // If we have no white-list, then use a fast track which elides all tags.
2063
+ return String(value).replace(soy.esc.$$HTML_TAG_REGEX_, '')
2064
+ // This is just paranoia since callers should normalize the result
2065
+ // anyway, but if they didn't, it would be necessary to ensure that
2066
+ // after the first replace non-tag uses of < do not recombine into
2067
+ // tags as in "<<foo>script>alert(1337)</<foo>script>".
2068
+ .replace(soy.esc.$$LT_REGEX_, '&lt;');
2069
+ }
2070
+
2071
+ // Escapes '[' so that we can use [123] below to mark places where tags
2072
+ // have been removed.
2073
+ var html = String(value).replace(/\[/g, '&#91;');
2074
+
2075
+ // Consider all uses of '<' and replace whitelisted tags with markers like
2076
+ // [1] which are indices into a list of approved tag names.
2077
+ // Replace all other uses of < and > with entities.
2078
+ var tags = [];
2079
+ html = html.replace(
2080
+ soy.esc.$$HTML_TAG_REGEX_,
2081
+ function(tok, tagName) {
2082
+ if (tagName) {
2083
+ tagName = tagName.toLowerCase();
2084
+ if (opt_tagWhitelist.hasOwnProperty(tagName) &&
2085
+ opt_tagWhitelist[tagName]) {
2086
+ var start = tok.charAt(1) === '/' ? '</' : '<';
2087
+ var index = tags.length;
2088
+ tags[index] = start + tagName + '>';
2089
+ return '[' + index + ']';
2090
+ }
2091
+ }
2092
+ return '';
2093
+ });
2094
+
2095
+ // Escape HTML special characters. Now there are no '<' in html that could
2096
+ // start a tag.
2097
+ html = soy.esc.$$normalizeHtmlHelper(html);
2098
+
2099
+ var finalCloseTags = soy.$$balanceTags_(tags);
2100
+
2101
+ // Now html contains no tags or less-than characters that could become
2102
+ // part of a tag via a replacement operation and tags only contains
2103
+ // approved tags.
2104
+ // Reinsert the white-listed tags.
2105
+ html = html.replace(
2106
+ /\[(\d+)\]/g, function(_, index) { return tags[index]; });
2107
+
2108
+ // Close any still open tags.
2109
+ // This prevents unclosed formatting elements like <ol> and <table> from
2110
+ // breaking the layout of containing HTML.
2111
+ return html + finalCloseTags;
2112
+ };
2113
+
2114
+
2115
+ /**
2116
+ * Throw out any close tags that don't correspond to start tags.
2117
+ * If {@code <table>} is used for formatting, embedded HTML shouldn't be able
2118
+ * to use a mismatched {@code </table>} to break page layout.
2119
+ *
2120
+ * @param {Array.<string>} tags an array of tags that will be modified in place
2121
+ * include tags, the empty string, or concatenations of empty tags.
2122
+ * @return {string} zero or more closed tags that close all elements that are
2123
+ * opened in tags but not closed.
2124
+ * @private
1237
2125
  */
1238
- soy.$$stripHtmlTags = function(value) {
1239
- return String(value).replace(soy.esc.$$HTML_TAG_REGEX_, '');
2126
+ soy.$$balanceTags_ = function(tags) {
2127
+ var open = [];
2128
+ for (var i = 0, n = tags.length; i < n; ++i) {
2129
+ var tag = tags[i];
2130
+ if (tag.charAt(1) === '/') {
2131
+ var openTagIndex = open.length - 1;
2132
+ // NOTE: This is essentially lastIndexOf, but it's not supported in IE.
2133
+ while (openTagIndex >= 0 && open[openTagIndex] != tag) {
2134
+ openTagIndex--;
2135
+ }
2136
+ if (openTagIndex < 0) {
2137
+ tags[i] = ''; // Drop close tag.
2138
+ } else {
2139
+ tags[i] = open.slice(openTagIndex).reverse().join('');
2140
+ open.length = openTagIndex;
2141
+ }
2142
+ } else if (!soy.$$HTML5_VOID_ELEMENTS_.test(tag)) {
2143
+ open.push('</' + tag.substring(1));
2144
+ }
2145
+ }
2146
+ return open.reverse().join('');
1240
2147
  };
1241
2148
 
1242
2149
 
1243
2150
  /**
1244
2151
  * Escapes HTML special characters in an HTML attribute value.
1245
2152
  *
1246
- * @param {*} value The HTML to be escaped. May not be a string, but the
2153
+ * @param {*} value The HTML to be escaped. May not be a string, but the
1247
2154
  * value will be coerced to a string.
1248
2155
  * @return {string} An escaped version of value.
1249
2156
  */
1250
2157
  soy.$$escapeHtmlAttribute = function(value) {
1251
- if (typeof value === 'object' && value &&
1252
- value.contentKind === soydata.SanitizedContentKind.HTML) {
2158
+ // NOTE: We don't accept ATTRIBUTES here because ATTRIBUTES is actually not
2159
+ // the attribute value context, but instead k/v pairs.
2160
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2161
+ // NOTE: After removing tags, we also escape quotes ("normalize") so that
2162
+ // the HTML can be embedded in attribute context.
2163
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtml);
1253
2164
  return soy.esc.$$normalizeHtmlHelper(soy.$$stripHtmlTags(value.content));
1254
2165
  }
1255
2166
  return soy.esc.$$escapeHtmlHelper(value);
@@ -1260,13 +2171,13 @@ soy.$$escapeHtmlAttribute = function(value) {
1260
2171
  * Escapes HTML special characters in a string including space and other
1261
2172
  * characters that can end an unquoted HTML attribute value.
1262
2173
  *
1263
- * @param {*} value The HTML to be escaped. May not be a string, but the
2174
+ * @param {*} value The HTML to be escaped. May not be a string, but the
1264
2175
  * value will be coerced to a string.
1265
2176
  * @return {string} An escaped version of value.
1266
2177
  */
1267
2178
  soy.$$escapeHtmlAttributeNospace = function(value) {
1268
- if (typeof value === 'object' && value &&
1269
- value.contentKind === soydata.SanitizedContentKind.HTML) {
2179
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2180
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtml);
1270
2181
  return soy.esc.$$normalizeHtmlNospaceHelper(
1271
2182
  soy.$$stripHtmlTags(value.content));
1272
2183
  }
@@ -1277,29 +2188,46 @@ soy.$$escapeHtmlAttributeNospace = function(value) {
1277
2188
  /**
1278
2189
  * Filters out strings that cannot be a substring of a valid HTML attribute.
1279
2190
  *
1280
- * @param {*} value The value to escape. May not be a string, but the value
2191
+ * Note the input is expected to be key=value pairs.
2192
+ *
2193
+ * @param {*} value The value to escape. May not be a string, but the value
1281
2194
  * will be coerced to a string.
1282
2195
  * @return {string} A valid HTML attribute name part or name/value pair.
1283
2196
  * {@code "zSoyz"} if the input is invalid.
1284
2197
  */
1285
- soy.$$filterHtmlAttribute = function(value) {
1286
- if (typeof value === 'object' && value &&
1287
- value.contentKind === soydata.SanitizedContentKind.HTML_ATTRIBUTE) {
1288
- return value.content.replace(/=([^"']*)$/, '="$1"');
2198
+ soy.$$filterHtmlAttributes = function(value) {
2199
+ // NOTE: Explicitly no support for SanitizedContentKind.HTML, since that is
2200
+ // meaningless in this context, which is generally *between* html attributes.
2201
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.ATTRIBUTES)) {
2202
+ goog.asserts.assert(value.constructor === soydata.SanitizedHtmlAttribute);
2203
+ // Add a space at the end to ensure this won't get merged into following
2204
+ // attributes, unless the interpretation is unambiguous (ending with quotes
2205
+ // or a space).
2206
+ return value.content.replace(/([^"'\s])$/, '$1 ');
1289
2207
  }
1290
- return soy.esc.$$filterHtmlAttributeHelper(value);
2208
+ // TODO: Dynamically inserting attributes that aren't marked as trusted is
2209
+ // probably unnecessary. Any filtering done here will either be inadequate
2210
+ // for security or not flexible enough. Having clients use kind="attributes"
2211
+ // in parameters seems like a wiser idea.
2212
+ return soy.esc.$$filterHtmlAttributesHelper(value);
1291
2213
  };
1292
2214
 
1293
2215
 
1294
2216
  /**
1295
2217
  * Filters out strings that cannot be a substring of a valid HTML element name.
1296
2218
  *
1297
- * @param {*} value The value to escape. May not be a string, but the value
2219
+ * @param {*} value The value to escape. May not be a string, but the value
1298
2220
  * will be coerced to a string.
1299
2221
  * @return {string} A valid HTML element name part.
1300
2222
  * {@code "zSoyz"} if the input is invalid.
1301
2223
  */
1302
2224
  soy.$$filterHtmlElementName = function(value) {
2225
+ // NOTE: We don't accept any SanitizedContent here. HTML indicates valid
2226
+ // PCDATA, not tag names. A sloppy developer shouldn't be able to cause an
2227
+ // exploit:
2228
+ // ... {let userInput}script src=http://evil.com/evil.js{/let} ...
2229
+ // ... {param tagName kind="html"}{$userInput}{/param} ...
2230
+ // ... <{$tagName}>Hello World</{$tagName}>
1303
2231
  return soy.esc.$$filterHtmlElementNameHelper(value);
1304
2232
  };
1305
2233
 
@@ -1308,7 +2236,7 @@ soy.$$filterHtmlElementName = function(value) {
1308
2236
  * Escapes characters in the value to make it valid content for a JS string
1309
2237
  * literal.
1310
2238
  *
1311
- * @param {*} value The value to escape. May not be a string, but the value
2239
+ * @param {*} value The value to escape. May not be a string, but the value
1312
2240
  * will be coerced to a string.
1313
2241
  * @return {string} An escaped version of value.
1314
2242
  * @deprecated
@@ -1322,13 +2250,15 @@ soy.$$escapeJs = function(value) {
1322
2250
  * Escapes characters in the value to make it valid content for a JS string
1323
2251
  * literal.
1324
2252
  *
1325
- * @param {*} value The value to escape. May not be a string, but the value
2253
+ * @param {*} value The value to escape. May not be a string, but the value
1326
2254
  * will be coerced to a string.
1327
2255
  * @return {string} An escaped version of value.
1328
2256
  */
1329
2257
  soy.$$escapeJsString = function(value) {
1330
- if (typeof value === 'object' &&
1331
- value.contentKind === soydata.SanitizedContentKind.JS_STR_CHARS) {
2258
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.JS_STR_CHARS)) {
2259
+ // TODO: It might still be worthwhile to normalize it to remove
2260
+ // unescaped quotes, null, etc: replace(/(?:^|[^\])['"]/g, '\\$
2261
+ goog.asserts.assert(value.constructor === soydata.SanitizedJsStrChars);
1332
2262
  return value.content;
1333
2263
  }
1334
2264
  return soy.esc.$$escapeJsStringHelper(value);
@@ -1338,7 +2268,7 @@ soy.$$escapeJsString = function(value) {
1338
2268
  /**
1339
2269
  * Encodes a value as a JavaScript literal.
1340
2270
  *
1341
- * @param {*} value The value to escape. May not be a string, but the value
2271
+ * @param {*} value The value to escape. May not be a string, but the value
1342
2272
  * will be coerced to a string.
1343
2273
  * @return {string} A JavaScript code representation of the input.
1344
2274
  */
@@ -1353,6 +2283,10 @@ soy.$$escapeJsValue = function(value) {
1353
2283
  // distinct undefined value.
1354
2284
  return ' null ';
1355
2285
  }
2286
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.JS)) {
2287
+ goog.asserts.assert(value.constructor === soydata.SanitizedJs);
2288
+ return value.content;
2289
+ }
1356
2290
  switch (typeof value) {
1357
2291
  case 'boolean': case 'number':
1358
2292
  return ' ' + value + ' ';
@@ -1366,7 +2300,7 @@ soy.$$escapeJsValue = function(value) {
1366
2300
  * Escapes characters in the string to make it valid content for a JS regular
1367
2301
  * expression literal.
1368
2302
  *
1369
- * @param {*} value The value to escape. May not be a string, but the value
2303
+ * @param {*} value The value to escape. May not be a string, but the value
1370
2304
  * will be coerced to a string.
1371
2305
  * @return {string} An escaped version of value.
1372
2306
  */
@@ -1400,13 +2334,13 @@ soy.$$pctEncode_ = function(ch) {
1400
2334
  /**
1401
2335
  * Escapes a string so that it can be safely included in a URI.
1402
2336
  *
1403
- * @param {*} value The value to escape. May not be a string, but the value
2337
+ * @param {*} value The value to escape. May not be a string, but the value
1404
2338
  * will be coerced to a string.
1405
2339
  * @return {string} An escaped version of value.
1406
2340
  */
1407
2341
  soy.$$escapeUri = function(value) {
1408
- if (typeof value === 'object' &&
1409
- value.contentKind === soydata.SanitizedContentKind.URI) {
2342
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.URI)) {
2343
+ goog.asserts.assert(value.constructor === soydata.SanitizedUri);
1410
2344
  return soy.$$normalizeUri(value);
1411
2345
  }
1412
2346
  // Apostophes and parentheses are not matched by encodeURIComponent.
@@ -1425,7 +2359,7 @@ soy.$$escapeUri = function(value) {
1425
2359
  /**
1426
2360
  * Removes rough edges from a URI by escaping any raw HTML/JS string delimiters.
1427
2361
  *
1428
- * @param {*} value The value to escape. May not be a string, but the value
2362
+ * @param {*} value The value to escape. May not be a string, but the value
1429
2363
  * will be coerced to a string.
1430
2364
  * @return {string} An escaped version of value.
1431
2365
  */
@@ -1438,19 +2372,37 @@ soy.$$normalizeUri = function(value) {
1438
2372
  * Vets a URI's protocol and removes rough edges from a URI by escaping
1439
2373
  * any raw HTML/JS string delimiters.
1440
2374
  *
1441
- * @param {*} value The value to escape. May not be a string, but the value
2375
+ * @param {*} value The value to escape. May not be a string, but the value
1442
2376
  * will be coerced to a string.
1443
2377
  * @return {string} An escaped version of value.
1444
2378
  */
1445
2379
  soy.$$filterNormalizeUri = function(value) {
2380
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.URI)) {
2381
+ goog.asserts.assert(value.constructor === soydata.SanitizedUri);
2382
+ return soy.$$normalizeUri(value);
2383
+ }
1446
2384
  return soy.esc.$$filterNormalizeUriHelper(value);
1447
2385
  };
1448
2386
 
1449
2387
 
2388
+ /**
2389
+ * Allows only data-protocol image URI's.
2390
+ *
2391
+ * @param {*} value The value to process. May not be a string, but the value
2392
+ * will be coerced to a string.
2393
+ * @return {!soydata.SanitizedUri} An escaped version of value.
2394
+ */
2395
+ soy.$$filterImageDataUri = function(value) {
2396
+ // NOTE: Even if it's a SanitizedUri, we will still filter it.
2397
+ return soydata.VERY_UNSAFE.ordainSanitizedUri(
2398
+ soy.esc.$$filterImageDataUriHelper(value));
2399
+ };
2400
+
2401
+
1450
2402
  /**
1451
2403
  * Escapes a string so it can safely be included inside a quoted CSS string.
1452
2404
  *
1453
- * @param {*} value The value to escape. May not be a string, but the value
2405
+ * @param {*} value The value to escape. May not be a string, but the value
1454
2406
  * will be coerced to a string.
1455
2407
  * @return {string} An escaped version of value.
1456
2408
  */
@@ -1462,11 +2414,15 @@ soy.$$escapeCssString = function(value) {
1462
2414
  /**
1463
2415
  * Encodes a value as a CSS identifier part, keyword, or quantity.
1464
2416
  *
1465
- * @param {*} value The value to escape. May not be a string, but the value
2417
+ * @param {*} value The value to escape. May not be a string, but the value
1466
2418
  * will be coerced to a string.
1467
2419
  * @return {string} A safe CSS identifier part, keyword, or quanitity.
1468
2420
  */
1469
2421
  soy.$$filterCssValue = function(value) {
2422
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.CSS)) {
2423
+ goog.asserts.assert(value.constructor === soydata.SanitizedCss);
2424
+ return value.content;
2425
+ }
1470
2426
  // Uses == to intentionally match null and undefined for Java compatibility.
1471
2427
  if (value == null) {
1472
2428
  return '';
@@ -1475,17 +2431,47 @@ soy.$$filterCssValue = function(value) {
1475
2431
  };
1476
2432
 
1477
2433
 
2434
+ /**
2435
+ * Sanity-checks noAutoescape input for explicitly tainted content.
2436
+ *
2437
+ * SanitizedContentKind.TEXT is used to explicitly mark input that was never
2438
+ * meant to be used unescaped.
2439
+ *
2440
+ * @param {*} value The value to filter.
2441
+ * @return {*} The value, that we dearly hope will not cause an attack.
2442
+ */
2443
+ soy.$$filterNoAutoescape = function(value) {
2444
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.TEXT)) {
2445
+ // Fail in development mode.
2446
+ goog.asserts.fail(
2447
+ 'Tainted SanitizedContentKind.TEXT for |noAutoescape: `%s`',
2448
+ [value.content]);
2449
+ // Return innocuous data in production.
2450
+ return 'zSoyz';
2451
+ }
2452
+
2453
+ return value;
2454
+ };
2455
+
2456
+
1478
2457
  // -----------------------------------------------------------------------------
1479
2458
  // Basic directives/functions.
1480
2459
 
1481
2460
 
1482
2461
  /**
1483
2462
  * Converts \r\n, \r, and \n to <br>s
1484
- * @param {*} str The string in which to convert newlines.
1485
- * @return {string} A copy of {@code str} with converted newlines.
1486
- */
1487
- soy.$$changeNewlineToBr = function(str) {
1488
- return goog.string.newLineToBr(String(str), false);
2463
+ * @param {*} value The string in which to convert newlines.
2464
+ * @return {string|!soydata.SanitizedHtml} A copy of {@code value} with
2465
+ * converted newlines. If {@code value} is SanitizedHtml, the return value
2466
+ * is also SanitizedHtml, of the same known directionality.
2467
+ */
2468
+ soy.$$changeNewlineToBr = function(value) {
2469
+ var result = goog.string.newLineToBr(String(value), false);
2470
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2471
+ return soydata.VERY_UNSAFE.ordainSanitizedHtml(
2472
+ result, soydata.getContentDir(value));
2473
+ }
2474
+ return result;
1489
2475
  };
1490
2476
 
1491
2477
 
@@ -1495,14 +2481,22 @@ soy.$$changeNewlineToBr = function(str) {
1495
2481
  * HTML tags or entities. Entites count towards the character count; HTML tags
1496
2482
  * do not.
1497
2483
  *
1498
- * @param {*} str The HTML string to insert word breaks into. Can be other
2484
+ * @param {*} value The HTML string to insert word breaks into. Can be other
1499
2485
  * types, but the value will be coerced to a string.
1500
2486
  * @param {number} maxCharsBetweenWordBreaks Maximum number of non-space
1501
2487
  * characters to allow before adding a word break.
1502
- * @return {string} The string including word breaks.
1503
- */
1504
- soy.$$insertWordBreaks = function(str, maxCharsBetweenWordBreaks) {
1505
- return goog.format.insertWordBreaks(String(str), maxCharsBetweenWordBreaks);
2488
+ * @return {string|!soydata.SanitizedHtml} The string including word
2489
+ * breaks. If {@code value} is SanitizedHtml, the return value
2490
+ * is also SanitizedHtml, of the same known directionality.
2491
+ */
2492
+ soy.$$insertWordBreaks = function(value, maxCharsBetweenWordBreaks) {
2493
+ var result = goog.format.insertWordBreaks(
2494
+ String(value), maxCharsBetweenWordBreaks);
2495
+ if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
2496
+ return soydata.VERY_UNSAFE.ordainSanitizedHtml(
2497
+ result, soydata.getContentDir(value));
2498
+ }
2499
+ return result;
1506
2500
  };
1507
2501
 
1508
2502
 
@@ -1604,37 +2598,53 @@ soy.$$getBidiFormatterInstance_ = function(bidiGlobalDir) {
1604
2598
  * Estimate the overall directionality of text. If opt_isHtml, makes sure to
1605
2599
  * ignore the LTR nature of the mark-up and escapes in text, making the logic
1606
2600
  * suitable for HTML and HTML-escaped text.
1607
- * @param {string} text The text whose directionality is to be estimated.
2601
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
2602
+ * estimating the directionality.
2603
+ *
2604
+ * @param {*} text The content whose directionality is to be estimated.
1608
2605
  * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
1609
2606
  * Default: false.
1610
2607
  * @return {number} 1 if text is LTR, -1 if it is RTL, and 0 if it is neutral.
1611
2608
  */
1612
2609
  soy.$$bidiTextDir = function(text, opt_isHtml) {
1613
- if (!text) {
1614
- return 0;
2610
+ var contentDir = soydata.getContentDir(text);
2611
+ if (contentDir != null) {
2612
+ return contentDir;
1615
2613
  }
1616
- return goog.i18n.bidi.detectRtlDirectionality(text, opt_isHtml) ? -1 : 1;
2614
+ var isHtml = opt_isHtml ||
2615
+ soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
2616
+ return goog.i18n.bidi.estimateDirection(text + '', isHtml);
1617
2617
  };
1618
2618
 
1619
2619
 
1620
2620
  /**
1621
- * Returns "dir=ltr" or "dir=rtl", depending on text's estimated
2621
+ * Returns 'dir="ltr"' or 'dir="rtl"', depending on text's estimated
1622
2622
  * directionality, if it is not the same as bidiGlobalDir.
1623
2623
  * Otherwise, returns the empty string.
1624
2624
  * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
1625
2625
  * in text, making the logic suitable for HTML and HTML-escaped text.
2626
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
2627
+ * estimating the directionality.
2628
+ *
1626
2629
  * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
1627
2630
  * if rtl, 0 if unknown.
1628
- * @param {string} text The text whose directionality is to be estimated.
2631
+ * @param {*} text The content whose directionality is to be estimated.
1629
2632
  * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
1630
2633
  * Default: false.
1631
- * @return {soydata.SanitizedHtmlAttribute} "dir=rtl" for RTL text in non-RTL
1632
- * context; "dir=ltr" for LTR text in non-LTR context;
2634
+ * @return {soydata.SanitizedHtmlAttribute} 'dir="rtl"' for RTL text in non-RTL
2635
+ * context; 'dir="ltr"' for LTR text in non-LTR context;
1633
2636
  * else, the empty string.
1634
2637
  */
1635
2638
  soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
1636
- return new soydata.SanitizedHtmlAttribute(
1637
- soy.$$getBidiFormatterInstance_(bidiGlobalDir).dirAttr(text, opt_isHtml));
2639
+ var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
2640
+ var contentDir = soydata.getContentDir(text);
2641
+ if (contentDir == null) {
2642
+ var isHtml = opt_isHtml ||
2643
+ soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
2644
+ contentDir = goog.i18n.bidi.estimateDirection(text + '', isHtml);
2645
+ }
2646
+ return soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute(
2647
+ formatter.knownDirAttr(contentDir));
1638
2648
  };
1639
2649
 
1640
2650
 
@@ -1644,9 +2654,12 @@ soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
1644
2654
  * bidiGlobalDir. Otherwise returns the empty string.
1645
2655
  * If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
1646
2656
  * in text, making the logic suitable for HTML and HTML-escaped text.
2657
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
2658
+ * estimating the directionality.
2659
+ *
1647
2660
  * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
1648
2661
  * if rtl, 0 if unknown.
1649
- * @param {string} text The text whose directionality is to be estimated.
2662
+ * @param {*} text The content whose directionality is to be estimated.
1650
2663
  * @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
1651
2664
  * Default: false.
1652
2665
  * @return {string} A Unicode bidi mark matching bidiGlobalDir, or the empty
@@ -1655,44 +2668,112 @@ soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
1655
2668
  */
1656
2669
  soy.$$bidiMarkAfter = function(bidiGlobalDir, text, opt_isHtml) {
1657
2670
  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
1658
- return formatter.markAfter(text, opt_isHtml);
2671
+ var isHtml = opt_isHtml ||
2672
+ soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
2673
+ return formatter.markAfterKnownDir(soydata.getContentDir(text), text + '',
2674
+ isHtml);
1659
2675
  };
1660
2676
 
1661
2677
 
1662
2678
  /**
1663
- * Returns str wrapped in a <span dir=ltr|rtl> according to its directionality -
1664
- * but only if that is neither neutral nor the same as the global context.
1665
- * Otherwise, returns str unchanged.
1666
- * Always treats str as HTML/HTML-escaped, i.e. ignores mark-up and escapes when
1667
- * estimating str's directionality.
2679
+ * Returns text wrapped in a <span dir="ltr|rtl"> according to its
2680
+ * directionality - but only if that is neither neutral nor the same as the
2681
+ * global context. Otherwise, returns text unchanged.
2682
+ * Always treats text as HTML/HTML-escaped, i.e. ignores mark-up and escapes
2683
+ * when estimating text's directionality.
2684
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
2685
+ * estimating the directionality.
2686
+ *
1668
2687
  * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
1669
2688
  * if rtl, 0 if unknown.
1670
- * @param {*} str The string to be wrapped. Can be other types, but the value
2689
+ * @param {*} text The string to be wrapped. Can be other types, but the value
1671
2690
  * will be coerced to a string.
1672
- * @return {string} The wrapped string.
2691
+ * @return {!goog.soy.data.SanitizedContent|string} The wrapped text.
1673
2692
  */
1674
- soy.$$bidiSpanWrap = function(bidiGlobalDir, str) {
2693
+ soy.$$bidiSpanWrap = function(bidiGlobalDir, text) {
1675
2694
  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
1676
- return formatter.spanWrap(str + '', true);
2695
+
2696
+ // We always treat the value as HTML, because span-wrapping is only useful
2697
+ // when its output will be treated as HTML (without escaping), and because
2698
+ // |bidiSpanWrap is not itself specified to do HTML escaping in Soy. (Both
2699
+ // explicit and automatic HTML escaping, if any, is done before calling
2700
+ // |bidiSpanWrap because the BidiSpanWrapDirective Java class implements
2701
+ // SanitizedContentOperator, but this does not mean that the input has to be
2702
+ // HTML SanitizedContent. In legacy usage, a string that is not
2703
+ // SanitizedContent is often printed in an autoescape="false" template or by
2704
+ // a print with a |noAutoescape, in which case our input is just SoyData.) If
2705
+ // the output will be treated as HTML, the input had better be safe
2706
+ // HTML/HTML-escaped (even if it isn't HTML SanitizedData), or we have an XSS
2707
+ // opportunity and a much bigger problem than bidi garbling.
2708
+ var wrappedText = formatter.spanWrapWithKnownDir(
2709
+ soydata.getContentDir(text), text + '', true /* opt_isHtml */);
2710
+
2711
+ // Like other directives whose Java class implements SanitizedContentOperator,
2712
+ // |bidiSpanWrap is called after the escaping (if any) has already been done,
2713
+ // and thus there is no need for it to produce actual SanitizedContent.
2714
+ return wrappedText;
1677
2715
  };
1678
2716
 
1679
2717
 
1680
2718
  /**
1681
- * Returns str wrapped in Unicode BiDi formatting characters according to its
2719
+ * Returns text wrapped in Unicode BiDi formatting characters according to its
1682
2720
  * directionality, i.e. either LRE or RLE at the beginning and PDF at the end -
1683
- * but only if str's directionality is neither neutral nor the same as the
1684
- * global context. Otherwise, returns str unchanged.
1685
- * Always treats str as HTML/HTML-escaped, i.e. ignores mark-up and escapes when
1686
- * estimating str's directionality.
2721
+ * but only if text's directionality is neither neutral nor the same as the
2722
+ * global context. Otherwise, returns text unchanged.
2723
+ * Only treats soydata.SanitizedHtml as HTML/HTML-escaped, i.e. ignores mark-up
2724
+ * and escapes when estimating text's directionality.
2725
+ * If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
2726
+ * estimating the directionality.
2727
+ *
1687
2728
  * @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
1688
2729
  * if rtl, 0 if unknown.
1689
- * @param {*} str The string to be wrapped. Can be other types, but the value
2730
+ * @param {*} text The string to be wrapped. Can be other types, but the value
1690
2731
  * will be coerced to a string.
1691
- * @return {string} The wrapped string.
2732
+ * @return {!goog.soy.data.SanitizedContent|string} The wrapped string.
1692
2733
  */
1693
- soy.$$bidiUnicodeWrap = function(bidiGlobalDir, str) {
2734
+ soy.$$bidiUnicodeWrap = function(bidiGlobalDir, text) {
1694
2735
  var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
1695
- return formatter.unicodeWrap(str + '', true);
2736
+
2737
+ // We treat the value as HTML if and only if it says it's HTML, even though in
2738
+ // legacy usage, we sometimes have an HTML string (not SanitizedContent) that
2739
+ // is passed to an autoescape="false" template or a {print $foo|noAutoescape},
2740
+ // with the output going into an HTML context without escaping. We simply have
2741
+ // no way of knowing if this is what is happening when we get
2742
+ // non-SanitizedContent input, and most of the time it isn't.
2743
+ var isHtml = soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
2744
+ var wrappedText = formatter.unicodeWrapWithKnownDir(
2745
+ soydata.getContentDir(text), text + '', isHtml);
2746
+
2747
+ // Bidi-wrapping a value converts it to the context directionality. Since it
2748
+ // does not cost us anything, we will indicate this known direction in the
2749
+ // output SanitizedContent, even though the intended consumer of that
2750
+ // information - a bidi wrapping directive - has already been run.
2751
+ var wrappedTextDir = formatter.getContextDir();
2752
+
2753
+ // Unicode-wrapping UnsanitizedText gives UnsanitizedText.
2754
+ // Unicode-wrapping safe HTML or JS string data gives valid, safe HTML or JS
2755
+ // string data.
2756
+ // ATTENTION: Do these need to be ...ForInternalBlocks()?
2757
+ if (soydata.isContentKind(text, soydata.SanitizedContentKind.TEXT)) {
2758
+ return new soydata.UnsanitizedText(wrappedText, wrappedTextDir);
2759
+ }
2760
+ if (isHtml) {
2761
+ return soydata.VERY_UNSAFE.ordainSanitizedHtml(wrappedText, wrappedTextDir);
2762
+ }
2763
+ if (soydata.isContentKind(text, soydata.SanitizedContentKind.JS_STR_CHARS)) {
2764
+ return soydata.VERY_UNSAFE.ordainSanitizedJsStrChars(
2765
+ wrappedText, wrappedTextDir);
2766
+ }
2767
+
2768
+ // Unicode-wrapping does not conform to the syntax of the other types of
2769
+ // content. For lack of anything better to do, we we do not declare a content
2770
+ // kind at all by falling through to the non-SanitizedContent case below.
2771
+ // TODO(user): Consider throwing a runtime error on receipt of
2772
+ // SanitizedContent other than TEXT, HTML, or JS_STR_CHARS.
2773
+
2774
+ // The input was not SanitizedContent, so our output isn't SanitizedContent
2775
+ // either.
2776
+ return wrappedText;
1696
2777
  };
1697
2778
 
1698
2779
 
@@ -1702,6 +2783,10 @@ soy.$$bidiUnicodeWrap = function(bidiGlobalDir, str) {
1702
2783
 
1703
2784
 
1704
2785
 
2786
+
2787
+
2788
+
2789
+
1705
2790
  // START GENERATED CODE FOR ESCAPERS.
1706
2791
 
1707
2792
  /**
@@ -1712,7 +2797,7 @@ soy.esc.$$escapeUriHelper = function(v) {
1712
2797
  };
1713
2798
 
1714
2799
  /**
1715
- * Maps charcters to the escaped versions for the named escape directives.
2800
+ * Maps characters to the escaped versions for the named escape directives.
1716
2801
  * @type {Object.<string, string>}
1717
2802
  * @private
1718
2803
  */
@@ -1740,7 +2825,7 @@ soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_HTML__AND__NORMALIZE_HTML__AND__ESCAPE_HTML_NOSP
1740
2825
  };
1741
2826
 
1742
2827
  /**
1743
- * A function that can be used with String.replace..
2828
+ * A function that can be used with String.replace.
1744
2829
  * @param {string} ch A single character matched by a compatible matcher.
1745
2830
  * @return {string} A token in the output language.
1746
2831
  * @private
@@ -1750,7 +2835,7 @@ soy.esc.$$REPLACER_FOR_ESCAPE_HTML__AND__NORMALIZE_HTML__AND__ESCAPE_HTML_NOSPAC
1750
2835
  };
1751
2836
 
1752
2837
  /**
1753
- * Maps charcters to the escaped versions for the named escape directives.
2838
+ * Maps characters to the escaped versions for the named escape directives.
1754
2839
  * @type {Object.<string, string>}
1755
2840
  * @private
1756
2841
  */
@@ -1792,7 +2877,7 @@ soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_ = {
1792
2877
  };
1793
2878
 
1794
2879
  /**
1795
- * A function that can be used with String.replace..
2880
+ * A function that can be used with String.replace.
1796
2881
  * @param {string} ch A single character matched by a compatible matcher.
1797
2882
  * @return {string} A token in the output language.
1798
2883
  * @private
@@ -1802,7 +2887,7 @@ soy.esc.$$REPLACER_FOR_ESCAPE_JS_STRING__AND__ESCAPE_JS_REGEX_ = function(ch) {
1802
2887
  };
1803
2888
 
1804
2889
  /**
1805
- * Maps charcters to the escaped versions for the named escape directives.
2890
+ * Maps characters to the escaped versions for the named escape directives.
1806
2891
  * @type {Object.<string, string>}
1807
2892
  * @private
1808
2893
  */
@@ -1837,7 +2922,7 @@ soy.esc.$$ESCAPE_MAP_FOR_ESCAPE_CSS_STRING_ = {
1837
2922
  };
1838
2923
 
1839
2924
  /**
1840
- * A function that can be used with String.replace..
2925
+ * A function that can be used with String.replace.
1841
2926
  * @param {string} ch A single character matched by a compatible matcher.
1842
2927
  * @return {string} A token in the output language.
1843
2928
  * @private
@@ -1847,7 +2932,7 @@ soy.esc.$$REPLACER_FOR_ESCAPE_CSS_STRING_ = function(ch) {
1847
2932
  };
1848
2933
 
1849
2934
  /**
1850
- * Maps charcters to the escaped versions for the named escape directives.
2935
+ * Maps characters to the escaped versions for the named escape directives.
1851
2936
  * @type {Object.<string, string>}
1852
2937
  * @private
1853
2938
  */
@@ -1920,7 +3005,7 @@ soy.esc.$$ESCAPE_MAP_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI_ = {
1920
3005
  };
1921
3006
 
1922
3007
  /**
1923
- * A function that can be used with String.replace..
3008
+ * A function that can be used with String.replace.
1924
3009
  * @param {string} ch A single character matched by a compatible matcher.
1925
3010
  * @return {string} A token in the output language.
1926
3011
  * @private
@@ -2004,7 +3089,14 @@ soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_URI_ = /^(?:(?:https?|mailto):|[^&:\/?#]*(
2004
3089
  * @type RegExp
2005
3090
  * @private
2006
3091
  */
2007
- soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTE_ = /^(?!style|on|action|archive|background|cite|classid|codebase|data|dsync|href|longdesc|src|usemap)(?:[a-z0-9_$:-]*)$/i;
3092
+ soy.esc.$$FILTER_FOR_FILTER_IMAGE_DATA_URI_ = /^data:image\/(?:bmp|gif|jpe?g|png|tiff|webp);base64,[a-z0-9+\/]+=*$/i;
3093
+
3094
+ /**
3095
+ * A pattern that vets values produced by the named directives.
3096
+ * @type RegExp
3097
+ * @private
3098
+ */
3099
+ soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTES_ = /^(?!style|on|action|archive|background|cite|classid|codebase|data|dsync|href|longdesc|src|usemap)(?:[a-z0-9_$:-]*)$/i;
2008
3100
 
2009
3101
  /**
2010
3102
  * A pattern that vets values produced by the named directives.
@@ -2130,7 +3222,7 @@ soy.esc.$$normalizeUriHelper = function(value) {
2130
3222
  soy.esc.$$filterNormalizeUriHelper = function(value) {
2131
3223
  var str = String(value);
2132
3224
  if (!soy.esc.$$FILTER_FOR_FILTER_NORMALIZE_URI_.test(str)) {
2133
- return 'zSoyz';
3225
+ return '#zSoyz';
2134
3226
  }
2135
3227
  return str.replace(
2136
3228
  soy.esc.$$MATCHER_FOR_NORMALIZE_URI__AND__FILTER_NORMALIZE_URI_,
@@ -2138,13 +3230,26 @@ soy.esc.$$filterNormalizeUriHelper = function(value) {
2138
3230
  };
2139
3231
 
2140
3232
  /**
2141
- * A helper for the Soy directive |filterHtmlAttribute
3233
+ * A helper for the Soy directive |filterImageDataUri
3234
+ * @param {*} value Can be of any type but will be coerced to a string.
3235
+ * @return {string} The escaped text.
3236
+ */
3237
+ soy.esc.$$filterImageDataUriHelper = function(value) {
3238
+ var str = String(value);
3239
+ if (!soy.esc.$$FILTER_FOR_FILTER_IMAGE_DATA_URI_.test(str)) {
3240
+ return 'data:image/gif;base64,zSoyz';
3241
+ }
3242
+ return str;
3243
+ };
3244
+
3245
+ /**
3246
+ * A helper for the Soy directive |filterHtmlAttributes
2142
3247
  * @param {*} value Can be of any type but will be coerced to a string.
2143
3248
  * @return {string} The escaped text.
2144
3249
  */
2145
- soy.esc.$$filterHtmlAttributeHelper = function(value) {
3250
+ soy.esc.$$filterHtmlAttributesHelper = function(value) {
2146
3251
  var str = String(value);
2147
- if (!soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTE_.test(str)) {
3252
+ if (!soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTES_.test(str)) {
2148
3253
  return 'zSoyz';
2149
3254
  }
2150
3255
  return str;
@@ -2165,10 +3270,29 @@ soy.esc.$$filterHtmlElementNameHelper = function(value) {
2165
3270
 
2166
3271
  /**
2167
3272
  * Matches all tags, HTML comments, and DOCTYPEs in tag soup HTML.
3273
+ * By removing these, and replacing any '<' or '>' characters with
3274
+ * entities we guarantee that the result can be embedded into a
3275
+ * an attribute without introducing a tag boundary.
3276
+ *
3277
+ * @type {RegExp}
3278
+ * @private
3279
+ */
3280
+ soy.esc.$$HTML_TAG_REGEX_ = /<(?:!|\/?([a-zA-Z][a-zA-Z0-9:\-]*))(?:[^>'"]|"[^"]*"|'[^']*')*>/g;
3281
+
3282
+ /**
3283
+ * Matches all occurrences of '<'.
2168
3284
  *
2169
3285
  * @type {RegExp}
2170
3286
  * @private
2171
3287
  */
2172
- soy.esc.$$HTML_TAG_REGEX_ = /<(?:!|\/?[a-zA-Z])(?:[^>'"]|"[^"]*"|'[^']*')*>/g;
3288
+ soy.esc.$$LT_REGEX_ = /</g;
3289
+
3290
+ /**
3291
+ * Maps lower-case names of innocuous tags to 1.
3292
+ *
3293
+ * @type {Object.<string,number>}
3294
+ * @private
3295
+ */
3296
+ soy.esc.$$SAFE_TAG_WHITELIST_ = {'b': 1, 'br': 1, 'em': 1, 'i': 1, 's': 1, 'sub': 1, 'sup': 1, 'u': 1};
2173
3297
 
2174
3298
  // END GENERATED CODE