closure 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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