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 part of _interceptors; | |
6 | |
7 /** | |
8 * The interceptor class for [String]. The compiler recognizes this | |
9 * class as an interceptor, and changes references to [:this:] to | |
10 * actually use the receiver of the method, which is generated as an extra | |
11 * argument added to each member. | |
12 */ | |
13 class JSString extends Interceptor implements String, JSIndexable { | |
14 const JSString(); | |
15 | |
16 int codeUnitAt(int index) { | |
17 if (index is !int) throw diagnoseIndexError(this, index); | |
18 if (index < 0) throw diagnoseIndexError(this, index); | |
19 if (index >= length) throw diagnoseIndexError(this, index); | |
20 return JS('JSUInt31', r'#.charCodeAt(#)', this, index); | |
21 } | |
22 | |
23 Iterable<Match> allMatches(String string, [int start = 0]) { | |
24 checkString(string); | |
25 checkInt(start); | |
26 if (0 > start || start > string.length) { | |
27 throw new RangeError.range(start, 0, string.length); | |
28 } | |
29 return allMatchesInStringUnchecked(this, string, start); | |
30 } | |
31 | |
32 Match matchAsPrefix(String string, [int start = 0]) { | |
33 if (start < 0 || start > string.length) { | |
34 throw new RangeError.range(start, 0, string.length); | |
35 } | |
36 if (start + this.length > string.length) return null; | |
37 // TODO(lrn): See if this can be optimized. | |
38 for (int i = 0; i < this.length; i++) { | |
39 if (string.codeUnitAt(start + i) != this.codeUnitAt(i)) { | |
40 return null; | |
41 } | |
42 } | |
43 return new StringMatch(start, string, this); | |
44 } | |
45 | |
46 String operator +(String other) { | |
47 if (other is !String) throw new ArgumentError.value(other); | |
48 return JS('String', r'# + #', this, other); | |
49 } | |
50 | |
51 bool endsWith(String other) { | |
52 checkString(other); | |
53 int otherLength = other.length; | |
54 if (otherLength > length) return false; | |
55 return other == substring(length - otherLength); | |
56 } | |
57 | |
58 String replaceAll(Pattern from, String to) { | |
59 checkString(to); | |
60 return stringReplaceAllUnchecked(this, from, to); | |
61 } | |
62 | |
63 String replaceAllMapped(Pattern from, String convert(Match match)) { | |
64 return this.splitMapJoin(from, onMatch: convert); | |
65 } | |
66 | |
67 String splitMapJoin(Pattern from, | |
68 {String onMatch(Match match), | |
69 String onNonMatch(String nonMatch)}) { | |
70 return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch); | |
71 } | |
72 | |
73 String replaceFirst(Pattern from, String to, [int startIndex = 0]) { | |
74 checkString(to); | |
75 checkInt(startIndex); | |
76 RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); | |
77 return stringReplaceFirstUnchecked(this, from, to, startIndex); | |
78 } | |
79 | |
80 String replaceFirstMapped(Pattern from, String replace(Match match), | |
81 [int startIndex = 0]) { | |
82 checkNull(replace); | |
83 checkInt(startIndex); | |
84 RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex"); | |
85 return stringReplaceFirstMappedUnchecked(this, from, replace, startIndex); | |
86 } | |
87 | |
88 List<String> split(Pattern pattern) { | |
89 checkNull(pattern); | |
90 if (pattern is String) { | |
91 return JS('JSExtendableArray', r'#.split(#)', this, pattern); | |
92 } else if (pattern is JSSyntaxRegExp && regExpCaptureCount(pattern) == 0) { | |
93 var re = regExpGetNative(pattern); | |
94 return JS('JSExtendableArray', r'#.split(#)', this, re); | |
95 } else { | |
96 return _defaultSplit(pattern); | |
97 } | |
98 } | |
99 | |
100 String replaceRange(int start, int end, String replacement) { | |
101 checkString(replacement); | |
102 checkInt(start); | |
103 end = RangeError.checkValidRange(start, end, this.length); | |
104 checkInt(end); | |
105 return stringReplaceRangeUnchecked(this, start, end, replacement); | |
106 } | |
107 | |
108 List<String> _defaultSplit(Pattern pattern) { | |
109 List<String> result = <String>[]; | |
110 // End of most recent match. That is, start of next part to add to result. | |
111 int start = 0; | |
112 // Length of most recent match. | |
113 // Set >0, so no match on the empty string causes the result to be [""]. | |
114 int length = 1; | |
115 for (var match in pattern.allMatches(this)) { | |
116 int matchStart = match.start; | |
117 int matchEnd = match.end; | |
118 length = matchEnd - matchStart; | |
119 if (length == 0 && start == matchStart) { | |
120 // An empty match right after another match is ignored. | |
121 // This includes an empty match at the start of the string. | |
122 continue; | |
123 } | |
124 int end = matchStart; | |
125 result.add(this.substring(start, end)); | |
126 start = matchEnd; | |
127 } | |
128 if (start < this.length || length > 0) { | |
129 // An empty match at the end of the string does not cause a "" at the end. | |
130 // A non-empty match ending at the end of the string does add a "". | |
131 result.add(this.substring(start)); | |
132 } | |
133 return result; | |
134 } | |
135 | |
136 bool startsWith(Pattern pattern, [int index = 0]) { | |
137 checkInt(index); | |
138 if (index < 0 || index > this.length) { | |
139 throw new RangeError.range(index, 0, this.length); | |
140 } | |
141 if (pattern is String) { | |
142 String other = pattern; | |
143 int otherLength = other.length; | |
144 int endIndex = index + otherLength; | |
145 if (endIndex > length) return false; | |
146 return other == JS('String', r'#.substring(#, #)', this, index, endIndex); | |
147 } | |
148 return pattern.matchAsPrefix(this, index) != null; | |
149 } | |
150 | |
151 String substring(int startIndex, [int endIndex]) { | |
152 checkInt(startIndex); | |
153 if (endIndex == null) endIndex = length; | |
154 checkInt(endIndex); | |
155 if (startIndex < 0 ) throw new RangeError.value(startIndex); | |
156 if (startIndex > endIndex) throw new RangeError.value(startIndex); | |
157 if (endIndex > length) throw new RangeError.value(endIndex); | |
158 return JS('String', r'#.substring(#, #)', this, startIndex, endIndex); | |
159 } | |
160 | |
161 String toLowerCase() { | |
162 return JS( | |
163 'returns:String;effects:none;depends:none;throws:null(1)', | |
164 r'#.toLowerCase()', this); | |
165 } | |
166 | |
167 String toUpperCase() { | |
168 return JS( | |
169 'returns:String;effects:none;depends:none;throws:null(1)', | |
170 r'#.toUpperCase()', this); | |
171 } | |
172 | |
173 // Characters with Whitespace property (Unicode 6.2). | |
174 // 0009..000D ; White_Space # Cc <control-0009>..<control-000D> | |
175 // 0020 ; White_Space # Zs SPACE | |
176 // 0085 ; White_Space # Cc <control-0085> | |
177 // 00A0 ; White_Space # Zs NO-BREAK SPACE | |
178 // 1680 ; White_Space # Zs OGHAM SPACE MARK | |
179 // 180E ; White_Space # Zs MONGOLIAN VOWEL SEPARATOR | |
180 // 2000..200A ; White_Space # Zs EN QUAD..HAIR SPACE | |
181 // 2028 ; White_Space # Zl LINE SEPARATOR | |
182 // 2029 ; White_Space # Zp PARAGRAPH SEPARATOR | |
183 // 202F ; White_Space # Zs NARROW NO-BREAK SPACE | |
184 // 205F ; White_Space # Zs MEDIUM MATHEMATICAL SPACE | |
185 // 3000 ; White_Space # Zs IDEOGRAPHIC SPACE | |
186 // | |
187 // BOM: 0xFEFF | |
188 static bool _isWhitespace(int codeUnit) { | |
189 // Most codeUnits should be less than 256. Special case with a smaller | |
190 // switch. | |
191 if (codeUnit < 256) { | |
192 switch (codeUnit) { | |
193 case 0x09: | |
194 case 0x0A: | |
195 case 0x0B: | |
196 case 0x0C: | |
197 case 0x0D: | |
198 case 0x20: | |
199 case 0x85: | |
200 case 0xA0: | |
201 return true; | |
202 default: | |
203 return false; | |
204 } | |
205 } | |
206 switch (codeUnit) { | |
207 case 0x1680: | |
208 case 0x180E: | |
209 case 0x2000: | |
210 case 0x2001: | |
211 case 0x2002: | |
212 case 0x2003: | |
213 case 0x2004: | |
214 case 0x2005: | |
215 case 0x2006: | |
216 case 0x2007: | |
217 case 0x2008: | |
218 case 0x2009: | |
219 case 0x200A: | |
220 case 0x2028: | |
221 case 0x2029: | |
222 case 0x202F: | |
223 case 0x205F: | |
224 case 0x3000: | |
225 case 0xFEFF: | |
226 return true; | |
227 default: | |
228 return false; | |
229 } | |
230 } | |
231 | |
232 /// Finds the index of the first non-whitespace character, or the | |
233 /// end of the string. Start looking at position [index]. | |
234 static int _skipLeadingWhitespace(String string, int index) { | |
235 const int SPACE = 0x20; | |
236 const int CARRIAGE_RETURN = 0x0D; | |
237 while (index < string.length) { | |
238 int codeUnit = string.codeUnitAt(index); | |
239 if (codeUnit != SPACE && | |
240 codeUnit != CARRIAGE_RETURN && | |
241 !_isWhitespace(codeUnit)) { | |
242 break; | |
243 } | |
244 index++; | |
245 } | |
246 return index; | |
247 } | |
248 | |
249 /// Finds the index after the the last non-whitespace character, or 0. | |
250 /// Start looking at position [index - 1]. | |
251 static int _skipTrailingWhitespace(String string, int index) { | |
252 const int SPACE = 0x20; | |
253 const int CARRIAGE_RETURN = 0x0D; | |
254 while (index > 0) { | |
255 int codeUnit = string.codeUnitAt(index - 1); | |
256 if (codeUnit != SPACE && | |
257 codeUnit != CARRIAGE_RETURN && | |
258 !_isWhitespace(codeUnit)) { | |
259 break; | |
260 } | |
261 index--; | |
262 } | |
263 return index; | |
264 } | |
265 | |
266 // Dart2js can't use JavaScript trim directly, | |
267 // because JavaScript does not trim | |
268 // the NEXT LINE (NEL) character (0x85). | |
269 String trim() { | |
270 const int NEL = 0x85; | |
271 | |
272 // Start by doing JS trim. Then check if it leaves a NEL at | |
273 // either end of the string. | |
274 String result = JS('String', '#.trim()', this); | |
275 if (result.length == 0) return result; | |
276 int firstCode = result.codeUnitAt(0); | |
277 int startIndex = 0; | |
278 if (firstCode == NEL) { | |
279 startIndex = _skipLeadingWhitespace(result, 1); | |
280 if (startIndex == result.length) return ""; | |
281 } | |
282 | |
283 int endIndex = result.length; | |
284 // We know that there is at least one character that is non-whitespace. | |
285 // Therefore we don't need to verify that endIndex > startIndex. | |
286 int lastCode = result.codeUnitAt(endIndex - 1); | |
287 if (lastCode == NEL) { | |
288 endIndex = _skipTrailingWhitespace(result, endIndex - 1); | |
289 } | |
290 if (startIndex == 0 && endIndex == result.length) return result; | |
291 return JS('String', r'#.substring(#, #)', result, startIndex, endIndex); | |
292 } | |
293 | |
294 // Dart2js can't use JavaScript trimLeft directly, | |
295 // because it is not in ES5, so not every browser implements it, | |
296 // and because those that do will not trim the NEXT LINE character (0x85). | |
297 String trimLeft() { | |
298 const int NEL = 0x85; | |
299 | |
300 // Start by doing JS trim. Then check if it leaves a NEL at | |
301 // the beginning of the string. | |
302 String result; | |
303 int startIndex = 0; | |
304 if (JS('bool', 'typeof #.trimLeft != "undefined"', this)) { | |
305 result = JS('String', '#.trimLeft()', this); | |
306 if (result.length == 0) return result; | |
307 int firstCode = result.codeUnitAt(0); | |
308 if (firstCode == NEL) { | |
309 startIndex = _skipLeadingWhitespace(result, 1); | |
310 } | |
311 } else { | |
312 result = this; | |
313 startIndex = _skipLeadingWhitespace(this, 0); | |
314 } | |
315 if (startIndex == 0) return result; | |
316 if (startIndex == result.length) return ""; | |
317 return JS('String', r'#.substring(#)', result, startIndex); | |
318 } | |
319 | |
320 // Dart2js can't use JavaScript trimRight directly, | |
321 // because it is not in ES5 and because JavaScript does not trim | |
322 // the NEXT LINE character (0x85). | |
323 String trimRight() { | |
324 const int NEL = 0x85; | |
325 | |
326 // Start by doing JS trim. Then check if it leaves a NEL or BOM at | |
327 // the end of the string. | |
328 String result; | |
329 int endIndex; | |
330 // trimRight is implemented by Firefox and Chrome/Blink, | |
331 // so use it if it is there. | |
332 if (JS('bool', 'typeof #.trimRight != "undefined"', this)) { | |
333 result = JS('String', '#.trimRight()', this); | |
334 endIndex = result.length; | |
335 if (endIndex == 0) return result; | |
336 int lastCode = result.codeUnitAt(endIndex - 1); | |
337 if (lastCode == NEL) { | |
338 endIndex = _skipTrailingWhitespace(result, endIndex - 1); | |
339 } | |
340 } else { | |
341 result = this; | |
342 endIndex = _skipTrailingWhitespace(this, this.length); | |
343 } | |
344 | |
345 if (endIndex == result.length) return result; | |
346 if (endIndex == 0) return ""; | |
347 return JS('String', r'#.substring(#, #)', result, 0, endIndex); | |
348 } | |
349 | |
350 String operator*(int times) { | |
351 if (0 >= times) return ''; // Unnecessary but hoists argument type check. | |
352 if (times == 1 || this.length == 0) return this; | |
353 if (times != JS('JSUInt32', '# >>> 0', times)) { | |
354 // times >= 2^32. We can't create a string that big. | |
355 throw const OutOfMemoryError(); | |
356 } | |
357 var result = ''; | |
358 var s = this; | |
359 while (true) { | |
360 if (times & 1 == 1) result = s + result; | |
361 times = JS('JSUInt31', '# >>> 1', times); | |
362 if (times == 0) break; | |
363 s += s; | |
364 } | |
365 return result; | |
366 } | |
367 | |
368 String padLeft(int width, [String padding = ' ']) { | |
369 int delta = width - this.length; | |
370 if (delta <= 0) return this; | |
371 return padding * delta + this; | |
372 } | |
373 | |
374 String padRight(int width, [String padding = ' ']) { | |
375 int delta = width - this.length; | |
376 if (delta <= 0) return this; | |
377 return this + padding * delta; | |
378 } | |
379 | |
380 List<int> get codeUnits => new CodeUnits(this); | |
381 | |
382 Runes get runes => new Runes(this); | |
383 | |
384 int indexOf(Pattern pattern, [int start = 0]) { | |
385 checkNull(pattern); | |
386 if (start is! int) throw argumentErrorValue(start); | |
387 if (start < 0 || start > this.length) { | |
388 throw new RangeError.range(start, 0, this.length); | |
389 } | |
390 if (pattern is String) { | |
391 return stringIndexOfStringUnchecked(this, pattern, start); | |
392 } | |
393 if (pattern is JSSyntaxRegExp) { | |
394 JSSyntaxRegExp re = pattern; | |
395 Match match = firstMatchAfter(re, this, start); | |
396 return (match == null) ? -1 : match.start; | |
397 } | |
398 for (int i = start; i <= this.length; i++) { | |
399 if (pattern.matchAsPrefix(this, i) != null) return i; | |
400 } | |
401 return -1; | |
402 } | |
403 | |
404 int lastIndexOf(Pattern pattern, [int start]) { | |
405 checkNull(pattern); | |
406 if (start == null) { | |
407 start = length; | |
408 } else if (start is! int) { | |
409 throw argumentErrorValue(start); | |
410 } else if (start < 0 || start > this.length) { | |
411 throw new RangeError.range(start, 0, this.length); | |
412 } | |
413 if (pattern is String) { | |
414 String other = pattern; | |
415 if (start + other.length > this.length) { | |
416 start = this.length - other.length; | |
417 } | |
418 return stringLastIndexOfUnchecked(this, other, start); | |
419 } | |
420 for (int i = start; i >= 0; i--) { | |
421 if (pattern.matchAsPrefix(this, i) != null) return i; | |
422 } | |
423 return -1; | |
424 } | |
425 | |
426 bool contains(Pattern other, [int startIndex = 0]) { | |
427 checkNull(other); | |
428 if (startIndex < 0 || startIndex > this.length) { | |
429 throw new RangeError.range(startIndex, 0, this.length); | |
430 } | |
431 return stringContainsUnchecked(this, other, startIndex); | |
432 } | |
433 | |
434 bool get isEmpty => length == 0; | |
435 | |
436 bool get isNotEmpty => !isEmpty; | |
437 | |
438 int compareTo(String other) { | |
439 if (other is !String) throw argumentErrorValue(other); | |
440 return this == other ? 0 | |
441 : JS('bool', r'# < #', this, other) ? -1 : 1; | |
442 } | |
443 | |
444 // Note: if you change this, also change the function [S]. | |
445 String toString() => this; | |
446 | |
447 /** | |
448 * This is the [Jenkins hash function][1] but using masking to keep | |
449 * values in SMI range. | |
450 * | |
451 * [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function | |
452 */ | |
453 int get hashCode { | |
454 // TODO(ahe): This method shouldn't have to use JS. Update when our | |
455 // optimizations are smarter. | |
456 int hash = 0; | |
457 for (int i = 0; i < length; i++) { | |
458 hash = 0x1fffffff & (hash + JS('int', r'#.charCodeAt(#)', this, i)); | |
459 hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | |
460 hash = JS('int', '# ^ (# >> 6)', hash, hash); | |
461 } | |
462 hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | |
463 hash = JS('int', '# ^ (# >> 11)', hash, hash); | |
464 return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | |
465 } | |
466 | |
467 Type get runtimeType => String; | |
468 | |
469 int get length => JS('int', r'#.length', this); | |
470 | |
471 String operator [](int index) { | |
472 if (index is !int) throw diagnoseIndexError(this, index); | |
473 if (index >= length || index < 0) throw diagnoseIndexError(this, index); | |
474 return JS('String', '#[#]', this, index); | |
475 } | |
476 } | |
OLD | NEW |