Path: blob/trunk/third_party/closure/goog/html/safehtmlformatter.js
2868 views
// Copyright 2016 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415goog.provide('goog.html.SafeHtmlFormatter');1617goog.require('goog.asserts');18goog.require('goog.dom.tags');19goog.require('goog.html.SafeHtml');20goog.require('goog.string');21222324/**25* Formatter producing SafeHtml from a plain text format and HTML fragments.26*27* Example usage:28*29* var formatter = new goog.html.SafeHtmlFormatter();30* var safeHtml = formatter.format(31* formatter.startTag('b') +32* 'User input:' +33* formatter.endTag('b') +34* ' ' +35* formatter.text(userInput));36*37* The most common usage is with goog.getMsg:38*39* var MSG_USER_INPUT = goog.getMsg(40* '{$startLink}Learn more{$endLink} about {$userInput}', {41* 'startLink': formatter.startTag('a', {'href': url}),42* 'endLink': formatter.endTag('a'),43* 'userInput': formatter.text(userInput)44* });45* var safeHtml = formatter.format(MSG_USER_INPUT);46*47* The formatting string should be constant with all variables processed by48* formatter.text().49*50* @constructor51* @struct52* @final53*/54goog.html.SafeHtmlFormatter = function() {55/**56* Mapping from a marker to a replacement.57* @private {!Object<string, !goog.html.SafeHtmlFormatter.Replacement>}58*/59this.replacements_ = {};6061/** @private {number} Number of stored replacements. */62this.replacementsCount_ = 0;63};646566/**67* @typedef {?{68* startTag: (string|undefined),69* attributes: (string|undefined),70* endTag: (string|undefined),71* html: (string|undefined)72* }}73*/74goog.html.SafeHtmlFormatter.Replacement;757677/**78* Formats a plain text string with markers holding HTML fragments to SafeHtml.79* @param {string} format Plain text format, will be HTML-escaped.80* @return {!goog.html.SafeHtml}81*/82goog.html.SafeHtmlFormatter.prototype.format = function(format) {83var openedTags = [];84var html = goog.string.htmlEscape(format).replace(85/\{SafeHtmlFormatter:\w+\}/g,86goog.bind(this.replaceFormattingString_, this, openedTags));87goog.asserts.assert(openedTags.length == 0,88'Expected no unclosed tags, got <' + openedTags.join('>, <') + '>.');89return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(90html, null);91};929394/**95* Replaces found formatting strings with saved tags.96* @param {!Array<string>} openedTags The tags opened so far, modified by this97* function.98* @param {string} match99* @return {string}100* @private101*/102goog.html.SafeHtmlFormatter.prototype.replaceFormattingString_ =103function(openedTags, match) {104var replacement = this.replacements_[match];105if (!replacement) {106// Someone included a string looking like our internal marker in the format.107return match;108}109var result = '';110if (replacement.startTag) {111result += '<' + replacement.startTag + replacement.attributes + '>';112if (goog.asserts.ENABLE_ASSERTS) {113if (!goog.dom.tags.isVoidTag(replacement.startTag.toLowerCase())) {114openedTags.push(replacement.startTag.toLowerCase());115}116}117}118if (replacement.html) {119result += replacement.html;120}121if (replacement.endTag) {122result += '</' + replacement.endTag + '>';123if (goog.asserts.ENABLE_ASSERTS) {124var lastTag = openedTags.pop();125goog.asserts.assert(lastTag == replacement.endTag.toLowerCase(),126'Expected </' + lastTag + '>, got </' + replacement.endTag + '>.');127}128}129return result;130};131132133/**134* Saves a start tag and returns its marker.135* @param {string} tagName136* @param {?Object<string, ?goog.html.SafeHtml.AttributeValue>=} opt_attributes137* Mapping from attribute names to their values. Only attribute names138* consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined causes139* the attribute to be omitted.140* @return {string} Marker.141* @throws {Error} If invalid tag name, attribute name, or attribute value is142* provided. This function accepts the same tags and attributes as143* {@link goog.html.SafeHtml.create}.144*/145goog.html.SafeHtmlFormatter.prototype.startTag = function(146tagName, opt_attributes) {147goog.html.SafeHtml.verifyTagName(tagName);148return this.storeReplacement_({149startTag: tagName,150attributes: goog.html.SafeHtml.stringifyAttributes(tagName, opt_attributes)151});152};153154155/**156* Saves an end tag and returns its marker.157* @param {string} tagName158* @return {string} Marker.159* @throws {Error} If invalid tag name, attribute name, or attribute value is160* provided. This function accepts the same tags and attributes as161* {@link goog.html.SafeHtml.create}.162*/163goog.html.SafeHtmlFormatter.prototype.endTag = function(tagName) {164goog.html.SafeHtml.verifyTagName(tagName);165return this.storeReplacement_({endTag: tagName});166};167168169/**170* Escapes a text, saves it and returns its marker.171*172* Wrapping any user input to .text() prevents the attacker with access to173* the random number generator to duplicate tags used elsewhere in the format.174*175* @param {string} text176* @return {string} Marker.177*/178goog.html.SafeHtmlFormatter.prototype.text = function(text) {179return this.storeReplacement_({html: goog.string.htmlEscape(text)});180};181182183/**184* Saves SafeHtml and returns its marker.185* @param {!goog.html.SafeHtml} safeHtml186* @return {string} Marker.187*/188goog.html.SafeHtmlFormatter.prototype.safeHtml = function(safeHtml) {189return this.storeReplacement_({190html: goog.html.SafeHtml.unwrap(safeHtml)191});192};193194195/**196* Stores a replacement and returns its marker.197* @param {!goog.html.SafeHtmlFormatter.Replacement} replacement198* @return {string} Marker.199* @private200*/201goog.html.SafeHtmlFormatter.prototype.storeReplacement_ = function(202replacement) {203this.replacementsCount_++;204var marker = '{SafeHtmlFormatter:' + this.replacementsCount_ + '_' +205goog.string.getRandomString() + '}';206this.replacements_[marker] = replacement;207return marker;208};209210211