Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(52)

Side by Side Diff: lib/i18n/bidi_utils.dart

Issue 10592011: Add BiDirectional Text formatting utilites to the i18n library. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « lib/i18n/bidi_formatter.dart ('k') | tests/lib/i18n/bidi_format_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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(@'(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(&lt;.*?(&gt;)+)');
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 }
OLDNEW
« no previous file with comments | « lib/i18n/bidi_formatter.dart ('k') | tests/lib/i18n/bidi_format_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698