Path: blob/trunk/third_party/closure/goog/i18n/bidiformatter.js
2868 views
// Copyright 2009 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.1314/**15* @fileoverview Utility for formatting text for display in a potentially16* opposite-directionality context without garbling.17* Mostly a port of http://go/formatter.cc.18*/192021goog.provide('goog.i18n.BidiFormatter');2223goog.require('goog.html.SafeHtml');24goog.require('goog.i18n.bidi');25goog.require('goog.i18n.bidi.Dir');26goog.require('goog.i18n.bidi.Format');27282930/**31* Utility class for formatting text for display in a potentially32* opposite-directionality context without garbling. Provides the following33* functionality:34*35* 1. BiDi Wrapping36* When text in one language is mixed into a document in another, opposite-37* directionality language, e.g. when an English business name is embedded in a38* Hebrew web page, both the inserted string and the text following it may be39* displayed incorrectly unless the inserted string is explicitly separated40* from the surrounding text in a "wrapper" that declares its directionality at41* the start and then resets it back at the end. This wrapping can be done in42* HTML mark-up (e.g. a 'span dir="rtl"' tag) or - only in contexts where43* mark-up can not be used - in Unicode BiDi formatting codes (LRE|RLE and PDF).44* Providing such wrapping services is the basic purpose of the BiDi formatter.45*46* 2. Directionality estimation47* How does one know whether a string about to be inserted into surrounding48* text has the same directionality? Well, in many cases, one knows that this49* must be the case when writing the code doing the insertion, e.g. when a50* localized message is inserted into a localized page. In such cases there is51* no need to involve the BiDi formatter at all. In the remaining cases, e.g.52* when the string is user-entered or comes from a database, the language of53* the string (and thus its directionality) is not known a priori, and must be54* estimated at run-time. The BiDi formatter does this automatically.55*56* 3. Escaping57* When wrapping plain text - i.e. text that is not already HTML or HTML-58* escaped - in HTML mark-up, the text must first be HTML-escaped to prevent XSS59* attacks and other nasty business. This of course is always true, but the60* escaping can not be done after the string has already been wrapped in61* mark-up, so the BiDi formatter also serves as a last chance and includes62* escaping services.63*64* Thus, in a single call, the formatter will escape the input string as65* specified, determine its directionality, and wrap it as necessary. It is66* then up to the caller to insert the return value in the output.67*68* See http://wiki/Main/TemplatesAndBiDi for more information.69*70* @param {goog.i18n.bidi.Dir|number|boolean|null} contextDir The context71* directionality, in one of the following formats:72* 1. A goog.i18n.bidi.Dir constant. NEUTRAL is treated the same as null,73* i.e. unknown, for backward compatibility with legacy calls.74* 2. A number (positive = LTR, negative = RTL, 0 = unknown).75* 3. A boolean (true = RTL, false = LTR).76* 4. A null for unknown directionality.77* @param {boolean=} opt_alwaysSpan Whether {@link #spanWrap} should always78* use a 'span' tag, even when the input directionality is neutral or79* matches the context, so that the DOM structure of the output does not80* depend on the combination of directionalities. Default: false.81* @constructor82* @final83*/84goog.i18n.BidiFormatter = function(contextDir, opt_alwaysSpan) {85/**86* The overall directionality of the context in which the formatter is being87* used.88* @type {?goog.i18n.bidi.Dir}89* @private90*/91this.contextDir_ = goog.i18n.bidi.toDir(contextDir, true /* opt_noNeutral */);9293/**94* Whether {@link #spanWrap} and similar methods should always use the same95* span structure, regardless of the combination of directionalities, for a96* stable DOM structure.97* @type {boolean}98* @private99*/100this.alwaysSpan_ = !!opt_alwaysSpan;101};102103104/**105* @return {?goog.i18n.bidi.Dir} The context directionality.106*/107goog.i18n.BidiFormatter.prototype.getContextDir = function() {108return this.contextDir_;109};110111112/**113* @return {boolean} Whether alwaysSpan is set.114*/115goog.i18n.BidiFormatter.prototype.getAlwaysSpan = function() {116return this.alwaysSpan_;117};118119120/**121* @param {goog.i18n.bidi.Dir|number|boolean|null} contextDir The context122* directionality, in one of the following formats:123* 1. A goog.i18n.bidi.Dir constant. NEUTRAL is treated the same as null,124* i.e. unknown.125* 2. A number (positive = LTR, negative = RTL, 0 = unknown).126* 3. A boolean (true = RTL, false = LTR).127* 4. A null for unknown directionality.128*/129goog.i18n.BidiFormatter.prototype.setContextDir = function(contextDir) {130this.contextDir_ = goog.i18n.bidi.toDir(contextDir, true /* opt_noNeutral */);131};132133134/**135* @param {boolean} alwaysSpan Whether {@link #spanWrap} should always use a136* 'span' tag, even when the input directionality is neutral or matches the137* context, so that the DOM structure of the output does not depend on the138* combination of directionalities.139*/140goog.i18n.BidiFormatter.prototype.setAlwaysSpan = function(alwaysSpan) {141this.alwaysSpan_ = alwaysSpan;142};143144145/**146* Returns the directionality of input argument {@code str}.147* Identical to {@link goog.i18n.bidi.estimateDirection}.148*149* @param {string} str The input text.150* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.151* Default: false.152* @return {goog.i18n.bidi.Dir} Estimated overall directionality of {@code str}.153*/154goog.i18n.BidiFormatter.prototype.estimateDirection =155goog.i18n.bidi.estimateDirection;156157158/**159* Returns true if two given directionalities are opposite.160* Note: the implementation is based on the numeric values of the Dir enum.161*162* @param {?goog.i18n.bidi.Dir} dir1 1st directionality.163* @param {?goog.i18n.bidi.Dir} dir2 2nd directionality.164* @return {boolean} Whether the directionalities are opposite.165* @private166*/167goog.i18n.BidiFormatter.prototype.areDirectionalitiesOpposite_ = function(168dir1, dir2) {169return Number(dir1) * Number(dir2) < 0;170};171172173/**174* Returns a unicode BiDi mark matching the context directionality (LRM or175* RLM) if {@code opt_dirReset}, and if either the directionality or the exit176* directionality of {@code str} is opposite to the context directionality.177* Otherwise returns the empty string.178*179* @param {string} str The input text.180* @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.181* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.182* Default: false.183* @param {boolean=} opt_dirReset Whether to perform the reset. Default: false.184* @return {string} A unicode BiDi mark or the empty string.185* @private186*/187goog.i18n.BidiFormatter.prototype.dirResetIfNeeded_ = function(188str, dir, opt_isHtml, opt_dirReset) {189// endsWithRtl and endsWithLtr are called only if needed (short-circuit).190if (opt_dirReset &&191(this.areDirectionalitiesOpposite_(dir, this.contextDir_) ||192(this.contextDir_ == goog.i18n.bidi.Dir.LTR &&193goog.i18n.bidi.endsWithRtl(str, opt_isHtml)) ||194(this.contextDir_ == goog.i18n.bidi.Dir.RTL &&195goog.i18n.bidi.endsWithLtr(str, opt_isHtml)))) {196return this.contextDir_ == goog.i18n.bidi.Dir.LTR ?197goog.i18n.bidi.Format.LRM :198goog.i18n.bidi.Format.RLM;199} else {200return '';201}202};203204205/**206* Returns "rtl" if {@code str}'s estimated directionality is RTL, and "ltr" if207* it is LTR. In case it's NEUTRAL, returns "rtl" if the context directionality208* is RTL, and "ltr" otherwise.209* Needed for GXP, which can't handle dirAttr.210* Example use case:211* <td expr:dir='bidiFormatter.dirAttrValue(foo)'>212* <gxp:eval expr='foo'>213* </td>214*215* @param {string} str Text whose directionality is to be estimated.216* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.217* Default: false.218* @return {string} "rtl" or "ltr", according to the logic described above.219*/220goog.i18n.BidiFormatter.prototype.dirAttrValue = function(str, opt_isHtml) {221return this.knownDirAttrValue(this.estimateDirection(str, opt_isHtml));222};223224225/**226* Returns "rtl" if the given directionality is RTL, and "ltr" if it is LTR. In227* case it's NEUTRAL, returns "rtl" if the context directionality is RTL, and228* "ltr" otherwise.229*230* @param {goog.i18n.bidi.Dir} dir A directionality.231* @return {string} "rtl" or "ltr", according to the logic described above.232*/233goog.i18n.BidiFormatter.prototype.knownDirAttrValue = function(dir) {234var resolvedDir = dir == goog.i18n.bidi.Dir.NEUTRAL ? this.contextDir_ : dir;235return resolvedDir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';236};237238239/**240* Returns 'dir="ltr"' or 'dir="rtl"', depending on {@code str}'s estimated241* directionality, if it is not the same as the context directionality.242* Otherwise, returns the empty string.243*244* @param {string} str Text whose directionality is to be estimated.245* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.246* Default: false.247* @return {string} 'dir="rtl"' for RTL text in non-RTL context; 'dir="ltr"' for248* LTR text in non-LTR context; else, the empty string.249*/250goog.i18n.BidiFormatter.prototype.dirAttr = function(str, opt_isHtml) {251return this.knownDirAttr(this.estimateDirection(str, opt_isHtml));252};253254255/**256* Returns 'dir="ltr"' or 'dir="rtl"', depending on the given directionality, if257* it is not the same as the context directionality. Otherwise, returns the258* empty string.259*260* @param {goog.i18n.bidi.Dir} dir A directionality.261* @return {string} 'dir="rtl"' for RTL text in non-RTL context; 'dir="ltr"' for262* LTR text in non-LTR context; else, the empty string.263*/264goog.i18n.BidiFormatter.prototype.knownDirAttr = function(dir) {265if (dir != this.contextDir_) {266return dir == goog.i18n.bidi.Dir.RTL ?267'dir="rtl"' :268dir == goog.i18n.bidi.Dir.LTR ? 'dir="ltr"' : '';269}270return '';271};272273274/**275* Formats a string of unknown directionality for use in HTML output of the276* context directionality, so an opposite-directionality string is neither277* garbled nor garbles what follows it.278* The algorithm: estimates the directionality of input argument {@code html}.279* In case its directionality doesn't match the context directionality, wraps it280* with a 'span' tag and adds a "dir" attribute (either 'dir="rtl"' or281* 'dir="ltr"'). If setAlwaysSpan(true) was used, the input is always wrapped282* with 'span', skipping just the dir attribute when it's not needed.283*284* If {@code opt_dirReset}, and if the overall directionality or the exit285* directionality of {@code str} are opposite to the context directionality, a286* trailing unicode BiDi mark matching the context directionality is appened287* (LRM or RLM).288*289* @param {!goog.html.SafeHtml} html The input HTML.290* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark291* matching the context directionality, when needed, to prevent the possible292* garbling of whatever may follow {@code html}. Default: true.293* @return {!goog.html.SafeHtml} Input text after applying the processing.294*/295goog.i18n.BidiFormatter.prototype.spanWrapSafeHtml = function(296html, opt_dirReset) {297return this.spanWrapSafeHtmlWithKnownDir(null, html, opt_dirReset);298};299300301/**302* Formats a string of given directionality for use in HTML output of the303* context directionality, so an opposite-directionality string is neither304* garbled nor garbles what follows it.305* The algorithm: If {@code dir} doesn't match the context directionality, wraps306* {@code html} with a 'span' tag and adds a "dir" attribute (either 'dir="rtl"'307* or 'dir="ltr"'). If setAlwaysSpan(true) was used, the input is always wrapped308* with 'span', skipping just the dir attribute when it's not needed.309*310* If {@code opt_dirReset}, and if {@code dir} or the exit directionality of311* {@code html} are opposite to the context directionality, a trailing unicode312* BiDi mark matching the context directionality is appened (LRM or RLM).313*314* @param {?goog.i18n.bidi.Dir} dir {@code html}'s overall directionality, or315* null if unknown and needs to be estimated.316* @param {!goog.html.SafeHtml} html The input HTML.317* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark318* matching the context directionality, when needed, to prevent the possible319* garbling of whatever may follow {@code html}. Default: true.320* @return {!goog.html.SafeHtml} Input text after applying the processing.321*/322goog.i18n.BidiFormatter.prototype.spanWrapSafeHtmlWithKnownDir = function(323dir, html, opt_dirReset) {324if (dir == null) {325dir = this.estimateDirection(goog.html.SafeHtml.unwrap(html), true);326}327return this.spanWrapWithKnownDir_(dir, html, opt_dirReset);328};329330331/**332* The internal implementation of spanWrapSafeHtmlWithKnownDir for non-null dir,333* to help the compiler optimize.334*335* @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.336* @param {!goog.html.SafeHtml} html The input HTML.337* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark338* matching the context directionality, when needed, to prevent the possible339* garbling of whatever may follow {@code str}. Default: true.340* @return {!goog.html.SafeHtml} Input text after applying the above processing.341* @private342*/343goog.i18n.BidiFormatter.prototype.spanWrapWithKnownDir_ = function(344dir, html, opt_dirReset) {345opt_dirReset = opt_dirReset || (opt_dirReset == undefined);346347var result;348// Whether to add the "dir" attribute.349var dirCondition =350dir != goog.i18n.bidi.Dir.NEUTRAL && dir != this.contextDir_;351if (this.alwaysSpan_ || dirCondition) { // Wrap is needed352var dirAttribute;353if (dirCondition) {354dirAttribute = dir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';355}356result = goog.html.SafeHtml.create('span', {'dir': dirAttribute}, html);357} else {358result = html;359}360var str = goog.html.SafeHtml.unwrap(html);361result = goog.html.SafeHtml.concatWithDir(362goog.i18n.bidi.Dir.NEUTRAL, result,363this.dirResetIfNeeded_(str, dir, true, opt_dirReset));364return result;365};366367368/**369* Formats a string of unknown directionality for use in plain-text output of370* the context directionality, so an opposite-directionality string is neither371* garbled nor garbles what follows it.372* As opposed to {@link #spanWrap}, this makes use of unicode BiDi formatting373* characters. In HTML, its *only* valid use is inside of elements that do not374* allow mark-up, e.g. an 'option' tag.375* The algorithm: estimates the directionality of input argument {@code str}.376* In case it doesn't match the context directionality, wraps it with Unicode377* BiDi formatting characters: RLE{@code str}PDF for RTL text, and378* LRE{@code str}PDF for LTR text.379*380* If {@code opt_dirReset}, and if the overall directionality or the exit381* directionality of {@code str} are opposite to the context directionality, a382* trailing unicode BiDi mark matching the context directionality is appended383* (LRM or RLM).384*385* Does *not* do HTML-escaping regardless of the value of {@code opt_isHtml}.386* The return value can be HTML-escaped as necessary.387*388* @param {string} str The input text.389* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.390* Default: false.391* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark392* matching the context directionality, when needed, to prevent the possible393* garbling of whatever may follow {@code str}. Default: true.394* @return {string} Input text after applying the above processing.395*/396goog.i18n.BidiFormatter.prototype.unicodeWrap = function(397str, opt_isHtml, opt_dirReset) {398return this.unicodeWrapWithKnownDir(null, str, opt_isHtml, opt_dirReset);399};400401402/**403* Formats a string of given directionality for use in plain-text output of the404* context directionality, so an opposite-directionality string is neither405* garbled nor garbles what follows it.406* As opposed to {@link #spanWrapWithKnownDir}, makes use of unicode BiDi407* formatting characters. In HTML, its *only* valid use is inside of elements408* that do not allow mark-up, e.g. an 'option' tag.409* The algorithm: If {@code dir} doesn't match the context directionality, wraps410* {@code str} with Unicode BiDi formatting characters: RLE{@code str}PDF for411* RTL text, and LRE{@code str}PDF for LTR text.412*413* If {@code opt_dirReset}, and if the overall directionality or the exit414* directionality of {@code str} are opposite to the context directionality, a415* trailing unicode BiDi mark matching the context directionality is appended416* (LRM or RLM).417*418* Does *not* do HTML-escaping regardless of the value of {@code opt_isHtml}.419* The return value can be HTML-escaped as necessary.420*421* @param {?goog.i18n.bidi.Dir} dir {@code str}'s overall directionality, or422* null if unknown and needs to be estimated.423* @param {string} str The input text.424* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.425* Default: false.426* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark427* matching the context directionality, when needed, to prevent the possible428* garbling of whatever may follow {@code str}. Default: true.429* @return {string} Input text after applying the above processing.430*/431goog.i18n.BidiFormatter.prototype.unicodeWrapWithKnownDir = function(432dir, str, opt_isHtml, opt_dirReset) {433if (dir == null) {434dir = this.estimateDirection(str, opt_isHtml);435}436return this.unicodeWrapWithKnownDir_(dir, str, opt_isHtml, opt_dirReset);437};438439440/**441* The internal implementation of unicodeWrapWithKnownDir for non-null dir, to442* help the compiler optimize.443*444* @param {goog.i18n.bidi.Dir} dir {@code str}'s overall directionality.445* @param {string} str The input text.446* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.447* Default: false.448* @param {boolean=} opt_dirReset Whether to append a trailing unicode bidi mark449* matching the context directionality, when needed, to prevent the possible450* garbling of whatever may follow {@code str}. Default: true.451* @return {string} Input text after applying the above processing.452* @private453*/454goog.i18n.BidiFormatter.prototype.unicodeWrapWithKnownDir_ = function(455dir, str, opt_isHtml, opt_dirReset) {456opt_dirReset = opt_dirReset || (opt_dirReset == undefined);457var result = [];458if (dir != goog.i18n.bidi.Dir.NEUTRAL && dir != this.contextDir_) {459result.push(460dir == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.Format.RLE :461goog.i18n.bidi.Format.LRE);462result.push(str);463result.push(goog.i18n.bidi.Format.PDF);464} else {465result.push(str);466}467468result.push(this.dirResetIfNeeded_(str, dir, opt_isHtml, opt_dirReset));469return result.join('');470};471472473/**474* Returns a Unicode BiDi mark matching the context directionality (LRM or RLM)475* if the directionality or the exit directionality of {@code str} are opposite476* to the context directionality. Otherwise returns the empty string.477*478* @param {string} str The input text.479* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.480* Default: false.481* @return {string} A Unicode bidi mark matching the global directionality or482* the empty string.483*/484goog.i18n.BidiFormatter.prototype.markAfter = function(str, opt_isHtml) {485return this.markAfterKnownDir(null, str, opt_isHtml);486};487488489/**490* Returns a Unicode BiDi mark matching the context directionality (LRM or RLM)491* if the given directionality or the exit directionality of {@code str} are492* opposite to the context directionality. Otherwise returns the empty string.493*494* @param {?goog.i18n.bidi.Dir} dir {@code str}'s overall directionality, or495* null if unknown and needs to be estimated.496* @param {string} str The input text.497* @param {boolean=} opt_isHtml Whether {@code str} is HTML / HTML-escaped.498* Default: false.499* @return {string} A Unicode bidi mark matching the global directionality or500* the empty string.501*/502goog.i18n.BidiFormatter.prototype.markAfterKnownDir = function(503dir, str, opt_isHtml) {504if (dir == null) {505dir = this.estimateDirection(str, opt_isHtml);506}507return this.dirResetIfNeeded_(str, dir, opt_isHtml, true);508};509510511/**512* Returns the Unicode BiDi mark matching the context directionality (LRM for513* LTR context directionality, RLM for RTL context directionality), or the514* empty string for neutral / unknown context directionality.515*516* @return {string} LRM for LTR context directionality and RLM for RTL context517* directionality.518*/519goog.i18n.BidiFormatter.prototype.mark = function() {520switch (this.contextDir_) {521case (goog.i18n.bidi.Dir.LTR):522return goog.i18n.bidi.Format.LRM;523case (goog.i18n.bidi.Dir.RTL):524return goog.i18n.bidi.Format.RLM;525default:526return '';527}528};529530531/**532* Returns 'right' for RTL context directionality. Otherwise (LTR or neutral /533* unknown context directionality) returns 'left'.534*535* @return {string} 'right' for RTL context directionality and 'left' for other536* context directionality.537*/538goog.i18n.BidiFormatter.prototype.startEdge = function() {539return this.contextDir_ == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.RIGHT :540goog.i18n.bidi.LEFT;541};542543544/**545* Returns 'left' for RTL context directionality. Otherwise (LTR or neutral /546* unknown context directionality) returns 'right'.547*548* @return {string} 'left' for RTL context directionality and 'right' for other549* context directionality.550*/551goog.i18n.BidiFormatter.prototype.endEdge = function() {552return this.contextDir_ == goog.i18n.bidi.Dir.RTL ? goog.i18n.bidi.LEFT :553goog.i18n.bidi.RIGHT;554};555556557