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