Path: blob/trunk/third_party/closure/goog/html/safehtmlformatter.js
4102 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/567goog.module('goog.html.SafeHtmlFormatter');8goog.module.declareLegacyNamespace();910const SafeHtml = goog.require('goog.html.SafeHtml');11const {ENABLE_ASSERTS, assert} = goog.require('goog.asserts');12const {getRandomString, htmlEscape} = goog.require('goog.string');13const {isVoidTag} = goog.require('goog.dom.tags');1415/**16* Formatter producing SafeHtml from a plain text format and HTML fragments.17* Example usage:18* var formatter = new SafeHtmlFormatter();19* var safeHtml = formatter.format(20* formatter.startTag('b') +21* 'User input:' +22* formatter.endTag('b') +23* ' ' +24* formatter.text(userInput));25* The most common usage is with goog.getMsg:26* var MSG_USER_INPUT = goog.getMsg(27* '{$startLink}Learn more{$endLink} about {$userInput}', {28* 'startLink': formatter.startTag('a', {'href': url}),29* 'endLink': formatter.endTag('a'),30* 'userInput': formatter.text(userInput)31* });32* var safeHtml = formatter.format(MSG_USER_INPUT);33* The formatting string should be constant with all variables processed by34* formatter.text().35* @final36*/37class SafeHtmlFormatter {38constructor() {39/**40* Mapping from a marker to a replacement.41* @private {!Object<string, !SafeHtmlFormatter.Replacement>}42*/43this.replacements_ = {};4445/** @private {number} Number of stored replacements. */46this.replacementsCount_ = 0;47}4849/**50* Formats a plain text string with markers holding HTML fragments to51* SafeHtml.52* @param {string} format Plain text format, will be HTML-escaped.53* @return {!SafeHtml}54*/55format(format) {56const openedTags = [];57const marker = htmlEscape(MARKER);58const html = htmlEscape(format).replace(59new RegExp(`\\{${marker}[\\w&#;]+\\}`, 'g'),60goog.bind(this.replaceFormattingString_, this, openedTags));61assert(62openedTags.length == 0,63'Expected no unclosed tags, got <' + openedTags.join('>, <') + '>.');64return SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(html);65}6667/**68* Replaces found formatting strings with saved tags.69* @param {!Array<string>} openedTags The tags opened so far, modified by this70* function.71* @param {string} match72* @return {string}73* @private74*/75replaceFormattingString_(openedTags, match) {76const replacement = this.replacements_[match];77if (!replacement) {78// Someone included a string looking like our internal marker in the79// format.80return match;81}82let result = '';83if (replacement.startTag) {84result += '<' + replacement.startTag + replacement.attributes + '>';85if (ENABLE_ASSERTS) {86if (!isVoidTag(replacement.startTag.toLowerCase())) {87openedTags.push(replacement.startTag.toLowerCase());88}89}90}91if (replacement.html) {92result += replacement.html;93}94if (replacement.endTag) {95result += '</' + replacement.endTag + '>';96if (ENABLE_ASSERTS) {97const lastTag = openedTags.pop();98assert(99lastTag == replacement.endTag.toLowerCase(),100`Expected </${lastTag}>, got </` + replacement.endTag + '>.');101}102}103return result;104}105106/**107* Saves a start tag and returns its marker.108* @param {string} tagName109* @param {?Object<string, ?SafeHtml.AttributeValue>=} attributes110* Mapping from attribute names to their values. Only attribute names111* consisting of [a-zA-Z0-9-] are allowed. Value of null or undefined112* causes the attribute to be omitted.113* @return {string} Marker.114* @throws {!Error} If invalid tag name, attribute name, or attribute value is115* provided. This function accepts the same tags and attributes as116* {@link SafeHtml.create}.117*/118startTag(tagName, attributes = undefined) {119SafeHtml.verifyTagName(tagName);120return this.storeReplacement_({121startTag: tagName,122attributes: SafeHtml.stringifyAttributes(tagName, attributes),123});124}125126/**127* Saves an end tag and returns its marker.128* @param {string} tagName129* @return {string} Marker.130* @throws {!Error} If invalid tag name, attribute name, or attribute value is131* provided. This function accepts the same tags as {@link132* SafeHtml.create}.133*/134endTag(tagName) {135SafeHtml.verifyTagName(tagName);136return this.storeReplacement_({endTag: tagName});137}138139/**140* Escapes a text, saves it and returns its marker.141*142* Wrapping any user input to .text() prevents the attacker with access to143* the random number generator to duplicate tags used elsewhere in the format.144*145* @param {string} text146* @return {string} Marker.147*/148text(text) {149return this.storeReplacement_({html: htmlEscape(text)});150}151152/**153* Saves SafeHtml and returns its marker.154* @param {!SafeHtml} safeHtml155* @return {string} Marker.156*/157safeHtml(safeHtml) {158return this.storeReplacement_({159html: SafeHtml.unwrap(safeHtml),160});161}162163/**164* Stores a replacement and returns its marker.165* @param {!SafeHtmlFormatter.Replacement} replacement166* @return {string} Marker.167* @private168*/169storeReplacement_(replacement) {170this.replacementsCount_++;171const marker =172`{${MARKER}` + this.replacementsCount_ + '_' + getRandomString() + '}';173this.replacements_[htmlEscape(marker)] = replacement;174return marker;175}176}177178179/**180* @typedef {?{181* startTag: (string|undefined),182* attributes: (string|undefined),183* endTag: (string|undefined),184* html: (string|undefined)185* }}186*/187SafeHtmlFormatter.Replacement;188189190/** @const {string} Marker used for replacements. */191const MARKER = 'SafeHtmlFormatter:';192193194exports = SafeHtmlFormatter;195196197