| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 #library('bidi_utils'); |
| 6 |
| 7 /** |
| 8 * Bidi stands for Bi-directional text. |
| 9 * According to http://en.wikipedia.org/wiki/Bi-directional_text: |
| 10 * Bi-directional text is text containing text in both text directionalities, |
| 11 * both right-to-left (RTL) and left-to-right (LTR). It generally involves text |
| 12 * containing different types of alphabets, but may also refer to boustrophedon, |
| 13 * which is changing text directionality in each row. |
| 14 * |
| 15 * This file provides some utility classes for determining directionality of |
| 16 * text, switching CSS layout from LTR to RTL, and other normalizing utilities |
| 17 * needed when switching between RTL and LTR formatting. |
| 18 * |
| 19 * It defines the TextDirection class which is used to represent directionality |
| 20 * of text, |
| 21 * In most cases, it is preferable to use bidi_formatter.dart, which provides |
| 22 * bidi functionality in the given directional context, instead of using |
| 23 * bidi_utils.dart directly. |
| 24 */ |
| 25 |
| 26 /** Class containing constants to represent the directionality of text. */ |
| 27 class TextDirection { |
| 28 static final LTR = const TextDirection._('LTR', 'ltr'); |
| 29 static final RTL = const TextDirection._('RTL', 'rtl'); |
| 30 // If the directionality of the text cannot be determined and we are not using |
| 31 // the context direction (or if the context direction is unknown), then the |
| 32 // text falls back on the more common ltr direction. |
| 33 static final UNKNOWN = const TextDirection._('UNKNOWN', 'ltr'); |
| 34 |
| 35 /** |
| 36 * Textual representation of the directionality constant. One of |
| 37 * 'LTR', 'RTL', or 'UNKNOWN'. |
| 38 */ |
| 39 final String value; |
| 40 |
| 41 /** Textual representation of the directionality when used in span tag. */ |
| 42 final String spanText; |
| 43 |
| 44 const TextDirection._(this.value, this.spanText); |
| 45 |
| 46 /** |
| 47 * Returns true if [otherDirection] is known to be different from this |
| 48 * direction. |
| 49 */ |
| 50 bool isDirectionChange(TextDirection otherDirection) { |
| 51 return otherDirection != TextDirection.UNKNOWN && this != otherDirection; |
| 52 } |
| 53 } |
| 54 |
| 55 class BidiUtils { |
| 56 /** Unicode "Left-To-Right Embedding" (LRE) character. */ |
| 57 static final LRE = '\u202A'; |
| 58 |
| 59 /** Unicode "Right-To-Left Embedding" (RLE) character. */ |
| 60 static final RLE = '\u202B'; |
| 61 |
| 62 /** Unicode "Pop Directional Formatting" (PDF) character. */ |
| 63 static final PDF = '\u202C'; |
| 64 |
| 65 /** Unicode "Left-To-Right Mark" (LRM) character. */ |
| 66 static final LRM = '\u200E'; |
| 67 |
| 68 /** Unicode "Right-To-Left Mark" (RLM) character. */ |
| 69 static final RLM = '\u200F'; |
| 70 |
| 71 /** Constant to define the threshold of RTL directionality. */ |
| 72 static num _RTL_DETECTION_THRESHOLD = 0.40; |
| 73 |
| 74 /** |
| 75 * Practical patterns to identify strong LTR and RTL characters, respectively. |
| 76 * These patterns are not completely correct according to the Unicode |
| 77 * standard. They are simplified for performance and small code size. |
| 78 */ |
| 79 static final String _LTR_CHARS = |
| 80 @'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590' |
| 81 @'\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; |
| 82 static final String _RTL_CHARS = @'\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; |
| 83 |
| 84 /** |
| 85 * Returns the input [text] with spaces instead of HTML tags or HTML escapes, |
| 86 * which is helpful for text directionality estimation. |
| 87 * Note: This function should not be used in other contexts. |
| 88 * It does not deal well with many things: comments, script, |
| 89 * elements, style elements, dir attribute,`>` in quoted attribute values, |
| 90 * etc. But it does handle well enough the most common use cases. |
| 91 * Since the worst that can happen as a result of these shortcomings is that |
| 92 * the wrong directionality will be estimated, we have not invested in |
| 93 * improving this. |
| 94 */ |
| 95 static String stripHtmlIfNeeded(String text) { |
| 96 // The regular expression is simplified for an HTML tag (opening or |
| 97 // closing) or an HTML escape. We might want to skip over such expressions |
| 98 // when estimating the text directionality. |
| 99 return text.replaceAll(const RegExp(@'<[^>]*>|&[^;]+;'), ' '); |
| 100 } |
| 101 |
| 102 /** |
| 103 * Determines if the first character in [text] with strong directionality is |
| 104 * LTR. If [isHtml] is true, the text is HTML or HTML-escaped. |
| 105 */ |
| 106 static bool startsWithLtr(String text, [isHtml=false]) { |
| 107 return const RegExp('^[^$_RTL_CHARS]*[$_LTR_CHARS]').hasMatch( |
| 108 isHtml? stripHtmlIfNeeded(text) : text); |
| 109 } |
| 110 |
| 111 /** |
| 112 * Determines if the first character in [text] with strong directionality is |
| 113 * RTL. If [isHtml] is true, the text is HTML or HTML-escaped. |
| 114 */ |
| 115 static bool startsWithRtl(String text, [isHtml=false]) { |
| 116 return const RegExp('^[^$_LTR_CHARS]*[$_RTL_CHARS]').hasMatch( |
| 117 isHtml? stripHtmlIfNeeded(text) : text); |
| 118 } |
| 119 |
| 120 /** |
| 121 * Determines if the exit directionality (ie, the last strongly-directional |
| 122 * character in [text] is LTR. If [isHtml] is true, the text is HTML or |
| 123 * HTML-escaped. |
| 124 */ |
| 125 static bool endsWithLtr(String text, [isHtml=false]) { |
| 126 return const RegExp('[$_LTR_CHARS][^$_RTL_CHARS]*\$').hasMatch( |
| 127 isHtml? stripHtmlIfNeeded(text) : text); |
| 128 } |
| 129 |
| 130 /** |
| 131 * Determines if the exit directionality (ie, the last strongly-directional |
| 132 * character in [text] is RTL. If [isHtml] is true, the text is HTML or |
| 133 * HTML-escaped. |
| 134 */ |
| 135 static bool endsWithRtl(String text, [isHtml=false]) { |
| 136 return const RegExp('[$_RTL_CHARS][^$_LTR_CHARS]*\$').hasMatch( |
| 137 isHtml? stripHtmlIfNeeded(text) : text); |
| 138 } |
| 139 |
| 140 /** |
| 141 * Determines if the given [text] has any LTR characters in it. |
| 142 * If [isHtml] is true, the text is HTML or HTML-escaped. |
| 143 */ |
| 144 static bool hasAnyLtr(String text, [isHtml=false]) { |
| 145 return const RegExp(@'[' '$_LTR_CHARS' @']').hasMatch( |
| 146 isHtml? stripHtmlIfNeeded(text) : text); |
| 147 } |
| 148 |
| 149 /** |
| 150 * Determines if the given [text] has any RTL characters in it. |
| 151 * If [isHtml] is true, the text is HTML or HTML-escaped. |
| 152 */ |
| 153 static bool hasAnyRtl(String text, [isHtml=false]) { |
| 154 return const RegExp(@'[' '$_RTL_CHARS' @']').hasMatch( |
| 155 isHtml? stripHtmlIfNeeded(text) : text); |
| 156 } |
| 157 |
| 158 /** |
| 159 * Check if a BCP 47 / III [languageString] indicates an RTL language. |
| 160 * |
| 161 * i.e. either: |
| 162 * - a language code explicitly specifying one of the right-to-left scripts, |
| 163 * e.g. "az-Arab", or |
| 164 * - a language code specifying one of the languages normally written in a |
| 165 * right-to-left script, e.g. "fa" (Farsi), except ones explicitly |
| 166 * specifying Latin or Cyrillic script (which are the usual LTR |
| 167 * alternatives). |
| 168 * |
| 169 * The list of right-to-left scripts appears in the 100-199 range in |
| 170 * http://www.unicode.org/iso15924/iso15924-num.html, of which Arabic and |
| 171 * Hebrew are by far the most widely used. We also recognize Thaana, N'Ko, and |
| 172 * Tifinagh, which also have significant modern usage. The rest (Syriac, |
| 173 * Samaritan, Mandaic, etc.) seem to have extremely limited or no modern usage |
| 174 * and are not recognized. |
| 175 * The languages usually written in a right-to-left script are taken as those |
| 176 * with Suppress-Script: Hebr|Arab|Thaa|Nkoo|Tfng in |
| 177 * http://www.iana.org/assignments/language-subtag-registry, |
| 178 * as well as Sindhi (sd) and Uyghur (ug). |
| 179 * The presence of other subtags of the language code, e.g. regions like EG |
| 180 * (Egypt), is ignored. |
| 181 */ |
| 182 static bool isRtlLanguage(String languageString) { |
| 183 return const RegExp(@'^(ar|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_]' |
| 184 @'(Arab|Hebr|Thaa|Nkoo|Tfng))(?!.*[-_](Latn|Cyrl)($|-|_))' |
| 185 @'($|-|_)', ignoreCase : true).hasMatch(languageString); |
| 186 } |
| 187 |
| 188 /** |
| 189 * Enforce the [html] snippet in RTL directionality regardless of overall |
| 190 * context. If the html piece was enclosed by a tag, the direction will be |
| 191 * applied to existing tag, otherwise a span tag will be added as wrapper. |
| 192 * For this reason, if html snippet start with with tag, this tag must enclose |
| 193 * the whole piece. If the tag already has a direction specified, this new one |
| 194 * will override existing one in behavior (should work on Chrome, FF, and IE |
| 195 * since this was ported directly from the Closure version). |
| 196 */ |
| 197 static String enforceRtlInHtml(String html) { |
| 198 return _enforceInHtmlHelper(html, 'rtl'); |
| 199 } |
| 200 |
| 201 /** |
| 202 * Enforce RTL on both end of the given [text] using unicode BiDi formatting |
| 203 * characters RLE and PDF. |
| 204 */ |
| 205 static String enforceRtlInText(String text) { |
| 206 return '$RLE$text$PDF'; |
| 207 } |
| 208 |
| 209 /** |
| 210 * Enforce the [html] snippet in LTR directionality regardless of overall |
| 211 * context. If the html piece was enclosed by a tag, the direction will be |
| 212 * applied to existing tag, otherwise a span tag will be added as wrapper. |
| 213 * For this reason, if html snippet start with with tag, this tag must enclose |
| 214 * the whole piece. If the tag already has a direction specified, this new one |
| 215 * will override existing one in behavior (tested on FF and IE). |
| 216 */ |
| 217 static String enforceLtrInHtml(String html) { |
| 218 return _enforceInHtmlHelper(html, 'ltr'); |
| 219 } |
| 220 |
| 221 /** |
| 222 * Enforce LTR on both end of the given [text] using unicode BiDi formatting |
| 223 * characters LRE and PDF. |
| 224 */ |
| 225 static String enforceLtrInText(String text) { |
| 226 return '$LRE$text$PDF'; |
| 227 } |
| 228 |
| 229 /** |
| 230 * Enforce the [html] snippet in the desired [direction] regardless of overall |
| 231 * context. If the html piece was enclosed by a tag, the direction will be |
| 232 * applied to existing tag, otherwise a span tag will be added as wrapper. |
| 233 * For this reason, if html snippet start with with tag, this tag must enclose |
| 234 * the whole piece. If the tag already has a direction specified, this new one |
| 235 * will override existing one in behavior (tested on FF and IE). |
| 236 */ |
| 237 static String _enforceInHtmlHelper(String html, String direction) { |
| 238 if (html.startsWith('<')) { |
| 239 StringBuffer buffer = new StringBuffer(); |
| 240 var startIndex = 0; |
| 241 Match match = const RegExp('<\\w+').firstMatch(html); |
| 242 if (match != null) { |
| 243 buffer.add(html.substring( |
| 244 startIndex, match.end())).add(' dir=$direction'); |
| 245 startIndex = match.end(); |
| 246 } |
| 247 return buffer.add(html.substring(startIndex)).toString(); |
| 248 } |
| 249 // '\n' is important for FF so that it won't incorrectly merge span groups. |
| 250 return '\n<span dir=$direction>$html</span>'; |
| 251 } |
| 252 |
| 253 /** |
| 254 * Apply bracket guard to [str] using html span tag. This is to address the |
| 255 * problem of messy bracket display that frequently happens in RTL layout. |
| 256 * If [isRtlContext] is true, then we explicitly want to wrap in a span of RTL |
| 257 * directionality, regardless of the estimated directionality. |
| 258 */ |
| 259 static String guardBracketInHtml(String str, [bool isRtlContext]) { |
| 260 var useRtl = isRtlContext == null ? hasAnyRtl(str) : isRtlContext; |
| 261 RegExp matchingBrackets = |
| 262 const RegExp(@'(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(<.*?(>)+)'); |
| 263 return _guardBracketHelper(str, matchingBrackets, |
| 264 '<span dir=${useRtl? "rtl" : "ltr"}>', '</span>'); |
| 265 } |
| 266 |
| 267 /** |
| 268 * Apply bracket guard to [str] using LRM and RLM. This is to address the |
| 269 * problem of messy bracket display that frequently happens in RTL layout. |
| 270 * This version works for both plain text and html, but in some cases is not |
| 271 * as good as guardBracketInHtml. |
| 272 * If [isRtlContext] is true, then we explicitly want to wrap in a span of RTL |
| 273 * directionality, regardless of the estimated directionality. |
| 274 */ |
| 275 static String guardBracketInText(String str, [bool isRtlContext]) { |
| 276 var useRtl = isRtlContext == null ? hasAnyRtl(str) : isRtlContext; |
| 277 var mark = useRtl ? RLM : LRM; |
| 278 return _guardBracketHelper(str, |
| 279 const RegExp(@'(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(<.*?>+)'), mark, mark); |
| 280 } |
| 281 |
| 282 /** |
| 283 * (Mostly) reimplements the $& functionality of "replace" in JavaScript. |
| 284 * Given a [str] and the [regexp] to match with, optionally supply a string to |
| 285 * be inserted [before] the match and/or [after]. For example, |
| 286 * `_guardBracketHelper('firetruck', const RegExp('truck'), 'hydrant', '!')` |
| 287 * would return 'firehydrant!'. |
| 288 */ |
| 289 // TODO(efortuna): Get rid of this once this is implemented in Dart. |
| 290 // See Issue 2979. |
| 291 static String _guardBracketHelper(String str, RegExp regexp, [String before, |
| 292 String after]) { |
| 293 StringBuffer buffer = new StringBuffer(); |
| 294 var startIndex = 0; |
| 295 Iterable matches = regexp.allMatches(str); |
| 296 for (Match match in matches) { |
| 297 buffer.add(str.substring(startIndex, match.start())).add(before); |
| 298 buffer.add(str.substring(match.start(), match.end())).add(after); |
| 299 startIndex = match.end(); |
| 300 } |
| 301 return buffer.add(str.substring(startIndex)).toString(); |
| 302 } |
| 303 |
| 304 /** |
| 305 * Estimates the directionality of [text] using the best known |
| 306 * general-purpose method (using relative word counts). A |
| 307 * TextDirection.UNKNOWN return value indicates completely neutral input. |
| 308 * [isHtml] is true if [text] HTML or HTML-escaped. |
| 309 * |
| 310 * If the number of RTL words is above a certain percentage of the total |
| 311 * number of strongly directional words, returns RTL. |
| 312 * Otherwise, if any words are strongly or weakly LTR, returns LTR. |
| 313 * Otherwise, returns UNKNOWN, which is used to mean `neutral`. |
| 314 * Numbers and URLs are counted as weakly LTR. |
| 315 */ |
| 316 static TextDirection estimateDirection(String text, [bool isHtml=false]) { |
| 317 text = isHtml? stripHtmlIfNeeded(text) : text; |
| 318 var rtlCount = 0; |
| 319 var total = 0; |
| 320 var hasWeaklyLtr = false; |
| 321 // Split a string into 'words' for directionality estimation based on |
| 322 // relative word counts. |
| 323 for (String token in text.split(const RegExp(@'\s+'))) { |
| 324 if (BidiUtils.startsWithRtl(token)) { |
| 325 rtlCount++; |
| 326 total++; |
| 327 } else if (const RegExp(@'^http://').hasMatch(token)) { |
| 328 // Checked if token looks like something that must always be LTR even in |
| 329 // RTL text, such as a URL. |
| 330 hasWeaklyLtr = true; |
| 331 } else if (BidiUtils.hasAnyLtr(token)) { |
| 332 total++; |
| 333 } else if (const RegExp(@'\d').hasMatch(token)) { |
| 334 // Checked if token contains any numerals. |
| 335 hasWeaklyLtr = true; |
| 336 } |
| 337 } |
| 338 |
| 339 if (total == 0) { |
| 340 return hasWeaklyLtr ? TextDirection.LTR : TextDirection.UNKNOWN; |
| 341 } else if (rtlCount > BidiUtils._RTL_DETECTION_THRESHOLD * total) { |
| 342 return TextDirection.RTL; |
| 343 } else { |
| 344 return TextDirection.LTR; |
| 345 } |
| 346 } |
| 347 |
| 348 /** |
| 349 * Find the first index in [str] of the first closing parenthesis that does |
| 350 * not match an opening parenthesis. |
| 351 */ |
| 352 static int _unmatchedParenIndex(String str) { |
| 353 int sum = 0; |
| 354 int index = 0; |
| 355 while (sum >= 0 || index > str.length) { |
| 356 int char = str.charCodeAt(index); |
| 357 if (char == '('.charCodeAt(0)) sum++; |
| 358 else if (char == ')'.charCodeAt(0)) sum--; |
| 359 index++; |
| 360 } |
| 361 return index; |
| 362 } |
| 363 |
| 364 /** |
| 365 * Replace the double and single quote directly after a Hebrew character in |
| 366 * [str] with GERESH and GERSHAYIM. This is most likely the user's intention. |
| 367 */ |
| 368 static String normalizeHebrewQuote(String str) { |
| 369 StringBuffer buf = new StringBuffer(); |
| 370 if (str.length > 0) { |
| 371 buf.add(str.substring(0, 1)); |
| 372 } |
| 373 // Start at 1 because we're looking for the patterns [\u0591-\u05f2])" or |
| 374 // [\u0591-\u05f2]'. |
| 375 for (int i = 1; i < str.length; i++) { |
| 376 if (str.substring(i, i+1) == '"' |
| 377 && const RegExp('[\u0591-\u05f2]').hasMatch(str.substring(i-1, i))) { |
| 378 buf.add('\u05f4'); |
| 379 } else if (str.substring(i, i+1) == "'" |
| 380 && const RegExp('[\u0591-\u05f2]').hasMatch(str.substring(i-1, i))) { |
| 381 buf.add('\u05f3'); |
| 382 } else { |
| 383 buf.add(str.substring(i, i+1)); |
| 384 } |
| 385 } |
| 386 return buf.toString(); |
| 387 } |
| 388 |
| 389 /** |
| 390 * Check the directionality of [str], return true if the piece of |
| 391 * text should be laid out in RTL direction. If [isHtml] is true, the string |
| 392 * is HTML or HTML-escaped. |
| 393 */ |
| 394 static bool detectRtlDirectionality(String str, [bool isHtml]) { |
| 395 return estimateDirection(str, isHtml) == TextDirection.RTL; |
| 396 } |
| 397 } |
| OLD | NEW |