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 /** | |
6 * **DEPRECATED** Use the JSON facilities in `dart:convert` instead. | |
7 * | |
8 * Utilities for encoding and decoding JSON (JavaScript Object Notation) data. | |
9 */ | |
10 @deprecated | |
11 library json; | |
12 | |
13 import 'dart:convert' show JSON, StringConversionSink; | |
14 export 'dart:convert' show JsonUnsupportedObjectError, JsonCyclicError; | |
15 | |
16 /** | |
17 * **DEPRECATED** Use [JSON.decode] from `dart:convert` instead. | |
18 * | |
19 * Parses [json] and build the corresponding parsed JSON value. | |
20 * | |
21 * Parsed JSON values are of the types [num], [String], [bool], [Null], | |
22 * [List]s of parsed JSON values or [Map]s from [String] to parsed | |
23 * JSON values. | |
24 * | |
25 * The optional [reviver] function, if provided, is called once for each | |
26 * object or list property parsed. The arguments are the property name | |
27 * ([String]) or list index ([int]), and the value is the parsed value. | |
28 * The return value of the reviver will be used as the value of that property | |
29 * instead the parsed value. | |
30 * | |
31 * Throws [FormatException] if the input is not valid JSON text. | |
32 */ | |
33 @deprecated | |
34 parse(String json, [reviver(var key, var value)]) { | |
35 return JSON.decode(json, reviver: reviver); | |
36 } | |
37 | |
38 /** | |
39 * **DEPRECATED**. Use [JsonCodec] from `dart:convert` instead. | |
40 * | |
41 * Serializes [object] into a JSON string. | |
42 * | |
43 * Directly serializable values are [num], [String], [bool], and [Null], as well | |
44 * as some [List] and [Map] values. | |
45 * For [List], the elements must all be serializable. | |
46 * For [Map], the keys must be [String] and the values must be serializable. | |
47 * | |
48 * If a value is any other type is attempted serialized, a "toJson()" method | |
49 * is invoked on the object and the result, which must be a directly | |
50 * serializable value, is serialized instead of the original value. | |
51 * | |
52 * If the object does not support this method, throws, or returns a | |
53 * value that is not directly serializable, a [JsonUnsupportedObjectError] | |
54 * exception is thrown. If the call throws (including the case where there | |
55 * is no nullary "toJson" method, the error is caught and stored in the | |
56 * [JsonUnsupportedObjectError]'s [:cause:] field. | |
57 * | |
58 * If a [List] or [Map] contains a reference to itself, directly or through | |
59 * other lists or maps, it cannot be serialized and a [JsonCyclicError] is | |
60 * thrown. | |
61 * | |
62 * Json Objects should not change during serialization. | |
63 * If an object is serialized more than once, [stringify] is allowed to cache | |
64 * the JSON text for it. I.e., if an object changes after it is first | |
65 * serialized, the new values may or may not be reflected in the result. | |
66 */ | |
67 @deprecated | |
68 String stringify(Object object) { | |
69 return JSON.encode(object); | |
70 } | |
71 | |
72 /** | |
73 * **DEPRECATED**. Use [JsonCodec] from `dart:convert` instead. | |
74 * | |
75 * Serializes [object] into [output] stream. | |
76 * | |
77 * Performs the same operations as [stringify] but outputs the resulting | |
78 * string to an existing [StringSink] instead of creating a new [String]. | |
79 * | |
80 * If serialization fails by throwing, some data might have been added to | |
81 * [output], but it won't contain valid JSON text. | |
82 */ | |
83 @deprecated | |
84 void printOn(Object object, StringSink output) { | |
85 var stringConversionSink = new StringConversionSink.fromStringSink(output); | |
86 JSON.encoder.startChunkedConversion(stringConversionSink).add(object); | |
87 } | |
88 | |
89 //// Implementation /////////////////////////////////////////////////////////// | |
90 | |
91 // Simple API for JSON parsing. | |
92 | |
93 abstract class JsonListener { | |
94 void handleString(String value) {} | |
95 void handleNumber(num value) {} | |
96 void handleBool(bool value) {} | |
97 void handleNull() {} | |
98 void beginObject() {} | |
99 void propertyName() {} | |
100 void propertyValue() {} | |
101 void endObject() {} | |
102 void beginArray() {} | |
103 void arrayElement() {} | |
104 void endArray() {} | |
105 /** Called on failure to parse [source]. */ | |
106 void fail(String source, int position, String message) {} | |
107 } | |
108 | |
109 /** | |
110 * A [JsonListener] that builds data objects from the parser events. | |
111 * | |
112 * This is a simple stack-based object builder. It keeps the most recently | |
113 * seen value in a variable, and uses it depending on the following event. | |
114 */ | |
115 class BuildJsonListener extends JsonListener { | |
116 /** | |
117 * Stack used to handle nested containers. | |
118 * | |
119 * The current container is pushed on the stack when a new one is | |
120 * started. If the container is a [Map], there is also a current [key] | |
121 * which is also stored on the stack. | |
122 */ | |
123 List stack = []; | |
124 /** The current [Map] or [List] being built. */ | |
125 var currentContainer; | |
126 /** The most recently read property key. */ | |
127 String key; | |
128 /** The most recently read value. */ | |
129 var value; | |
130 | |
131 /** Pushes the currently active container (and key, if a [Map]). */ | |
132 void pushContainer() { | |
133 if (currentContainer is Map) stack.add(key); | |
134 stack.add(currentContainer); | |
135 } | |
136 | |
137 /** Pops the top container from the [stack], including a key if applicable. */ | |
138 void popContainer() { | |
139 value = currentContainer; | |
140 currentContainer = stack.removeLast(); | |
141 if (currentContainer is Map) key = stack.removeLast(); | |
142 } | |
143 | |
144 void handleString(String value) { this.value = value; } | |
145 void handleNumber(num value) { this.value = value; } | |
146 void handleBool(bool value) { this.value = value; } | |
147 void handleNull() { this.value = null; } | |
148 | |
149 void beginObject() { | |
150 pushContainer(); | |
151 currentContainer = {}; | |
152 } | |
153 | |
154 void propertyName() { | |
155 key = value; | |
156 value = null; | |
157 } | |
158 | |
159 void propertyValue() { | |
160 Map map = currentContainer; | |
161 map[key] = value; | |
162 key = value = null; | |
163 } | |
164 | |
165 void endObject() { | |
166 popContainer(); | |
167 } | |
168 | |
169 void beginArray() { | |
170 pushContainer(); | |
171 currentContainer = []; | |
172 } | |
173 | |
174 void arrayElement() { | |
175 List list = currentContainer; | |
176 currentContainer.add(value); | |
177 value = null; | |
178 } | |
179 | |
180 void endArray() { | |
181 popContainer(); | |
182 } | |
183 | |
184 /** Read out the final result of parsing a JSON string. */ | |
185 get result { | |
186 assert(currentContainer == null); | |
187 return value; | |
188 } | |
189 } | |
190 | |
191 typedef _Reviver(var key, var value); | |
192 | |
193 class ReviverJsonListener extends BuildJsonListener { | |
194 final _Reviver reviver; | |
195 ReviverJsonListener(reviver(key, value)) : this.reviver = reviver; | |
196 | |
197 void arrayElement() { | |
198 List list = currentContainer; | |
199 value = reviver(list.length, value); | |
200 super.arrayElement(); | |
201 } | |
202 | |
203 void propertyValue() { | |
204 value = reviver(key, value); | |
205 super.propertyValue(); | |
206 } | |
207 | |
208 get result { | |
209 return reviver("", value); | |
210 } | |
211 } | |
212 | |
213 class JsonParser { | |
214 // A simple non-recursive state-based parser for JSON. | |
215 // | |
216 // Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON | |
217 // and strings also in OBJECT_EMPTY, OBJECT_COMMA. | |
218 // VALUE STRING : , } ] Transitions to | |
219 // EMPTY X X -> END | |
220 // ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop | |
221 // ARRAY_VALUE @ @ -> ARRAY_COMMA / pop | |
222 // ARRAY_COMMA X X -> ARRAY_VALUE | |
223 // OBJECT_EMPTY X @ -> OBJECT_KEY / pop | |
224 // OBJECT_KEY @ -> OBJECT_COLON | |
225 // OBJECT_COLON X X -> OBJECT_VALUE | |
226 // OBJECT_VALUE @ @ -> OBJECT_COMMA / pop | |
227 // OBJECT_COMMA X -> OBJECT_KEY | |
228 // END | |
229 // Starting a new array or object will push the current state. The "pop" | |
230 // above means restoring this state and then marking it as an ended value. | |
231 // X means generic handling, @ means special handling for just that | |
232 // state - that is, values are handled generically, only punctuation | |
233 // cares about the current state. | |
234 // Values for states are chosen so bits 0 and 1 tell whether | |
235 // a string/value is allowed, and setting bits 0 through 2 after a value | |
236 // gets to the next state (not empty, doesn't allow a value). | |
237 | |
238 // State building-block constants. | |
239 static const int INSIDE_ARRAY = 1; | |
240 static const int INSIDE_OBJECT = 2; | |
241 static const int AFTER_COLON = 3; // Always inside object. | |
242 | |
243 static const int ALLOW_STRING_MASK = 8; // Allowed if zero. | |
244 static const int ALLOW_VALUE_MASK = 4; // Allowed if zero. | |
245 static const int ALLOW_VALUE = 0; | |
246 static const int STRING_ONLY = 4; | |
247 static const int NO_VALUES = 12; | |
248 | |
249 // Objects and arrays are "empty" until their first property/element. | |
250 static const int EMPTY = 0; | |
251 static const int NON_EMPTY = 16; | |
252 static const int EMPTY_MASK = 16; // Empty if zero. | |
253 | |
254 | |
255 static const int VALUE_READ_BITS = NO_VALUES | NON_EMPTY; | |
256 | |
257 // Actual states. | |
258 static const int STATE_INITIAL = EMPTY | ALLOW_VALUE; | |
259 static const int STATE_END = NON_EMPTY | NO_VALUES; | |
260 | |
261 static const int STATE_ARRAY_EMPTY = INSIDE_ARRAY | EMPTY | ALLOW_VALUE; | |
262 static const int STATE_ARRAY_VALUE = INSIDE_ARRAY | NON_EMPTY | NO_VALUES; | |
263 static const int STATE_ARRAY_COMMA = INSIDE_ARRAY | NON_EMPTY | ALLOW_VALUE; | |
264 | |
265 static const int STATE_OBJECT_EMPTY = INSIDE_OBJECT | EMPTY | STRING_ONLY; | |
266 static const int STATE_OBJECT_KEY = INSIDE_OBJECT | NON_EMPTY | NO_VALUES; | |
267 static const int STATE_OBJECT_COLON = AFTER_COLON | NON_EMPTY | ALLOW_VALUE; | |
268 static const int STATE_OBJECT_VALUE = AFTER_COLON | NON_EMPTY | NO_VALUES; | |
269 static const int STATE_OBJECT_COMMA = INSIDE_OBJECT | NON_EMPTY | STRING_ONLY; | |
270 | |
271 // Character code constants. | |
272 static const int BACKSPACE = 0x08; | |
273 static const int TAB = 0x09; | |
274 static const int NEWLINE = 0x0a; | |
275 static const int CARRIAGE_RETURN = 0x0d; | |
276 static const int FORM_FEED = 0x0c; | |
277 static const int SPACE = 0x20; | |
278 static const int QUOTE = 0x22; | |
279 static const int PLUS = 0x2b; | |
280 static const int COMMA = 0x2c; | |
281 static const int MINUS = 0x2d; | |
282 static const int DECIMALPOINT = 0x2e; | |
283 static const int SLASH = 0x2f; | |
284 static const int CHAR_0 = 0x30; | |
285 static const int CHAR_9 = 0x39; | |
286 static const int COLON = 0x3a; | |
287 static const int CHAR_E = 0x45; | |
288 static const int LBRACKET = 0x5b; | |
289 static const int BACKSLASH = 0x5c; | |
290 static const int RBRACKET = 0x5d; | |
291 static const int CHAR_a = 0x61; | |
292 static const int CHAR_b = 0x62; | |
293 static const int CHAR_e = 0x65; | |
294 static const int CHAR_f = 0x66; | |
295 static const int CHAR_l = 0x6c; | |
296 static const int CHAR_n = 0x6e; | |
297 static const int CHAR_r = 0x72; | |
298 static const int CHAR_s = 0x73; | |
299 static const int CHAR_t = 0x74; | |
300 static const int CHAR_u = 0x75; | |
301 static const int LBRACE = 0x7b; | |
302 static const int RBRACE = 0x7d; | |
303 | |
304 final String source; | |
305 final JsonListener listener; | |
306 JsonParser(this.source, this.listener); | |
307 | |
308 /** Parses [source], or throws if it fails. */ | |
309 void parse() { | |
310 final List<int> states = <int>[]; | |
311 int state = STATE_INITIAL; | |
312 int position = 0; | |
313 int length = source.length; | |
314 while (position < length) { | |
315 int char = source.codeUnitAt(position); | |
316 switch (char) { | |
317 case SPACE: | |
318 case CARRIAGE_RETURN: | |
319 case NEWLINE: | |
320 case TAB: | |
321 position++; | |
322 break; | |
323 case QUOTE: | |
324 if ((state & ALLOW_STRING_MASK) != 0) fail(position); | |
325 position = parseString(position + 1); | |
326 state |= VALUE_READ_BITS; | |
327 break; | |
328 case LBRACKET: | |
329 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
330 listener.beginArray(); | |
331 states.add(state); | |
332 state = STATE_ARRAY_EMPTY; | |
333 position++; | |
334 break; | |
335 case LBRACE: | |
336 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
337 listener.beginObject(); | |
338 states.add(state); | |
339 state = STATE_OBJECT_EMPTY; | |
340 position++; | |
341 break; | |
342 case CHAR_n: | |
343 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
344 position = parseNull(position); | |
345 state |= VALUE_READ_BITS; | |
346 break; | |
347 case CHAR_f: | |
348 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
349 position = parseFalse(position); | |
350 state |= VALUE_READ_BITS; | |
351 break; | |
352 case CHAR_t: | |
353 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
354 position = parseTrue(position); | |
355 state |= VALUE_READ_BITS; | |
356 break; | |
357 case COLON: | |
358 if (state != STATE_OBJECT_KEY) fail(position); | |
359 listener.propertyName(); | |
360 state = STATE_OBJECT_COLON; | |
361 position++; | |
362 break; | |
363 case COMMA: | |
364 if (state == STATE_OBJECT_VALUE) { | |
365 listener.propertyValue(); | |
366 state = STATE_OBJECT_COMMA; | |
367 position++; | |
368 } else if (state == STATE_ARRAY_VALUE) { | |
369 listener.arrayElement(); | |
370 state = STATE_ARRAY_COMMA; | |
371 position++; | |
372 } else { | |
373 fail(position); | |
374 } | |
375 break; | |
376 case RBRACKET: | |
377 if (state == STATE_ARRAY_EMPTY) { | |
378 listener.endArray(); | |
379 } else if (state == STATE_ARRAY_VALUE) { | |
380 listener.arrayElement(); | |
381 listener.endArray(); | |
382 } else { | |
383 fail(position); | |
384 } | |
385 state = states.removeLast() | VALUE_READ_BITS; | |
386 position++; | |
387 break; | |
388 case RBRACE: | |
389 if (state == STATE_OBJECT_EMPTY) { | |
390 listener.endObject(); | |
391 } else if (state == STATE_OBJECT_VALUE) { | |
392 listener.propertyValue(); | |
393 listener.endObject(); | |
394 } else { | |
395 fail(position); | |
396 } | |
397 state = states.removeLast() | VALUE_READ_BITS; | |
398 position++; | |
399 break; | |
400 default: | |
401 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); | |
402 position = parseNumber(char, position); | |
403 state |= VALUE_READ_BITS; | |
404 break; | |
405 } | |
406 } | |
407 if (state != STATE_END) fail(position); | |
408 } | |
409 | |
410 /** | |
411 * Parses a "true" literal starting at [position]. | |
412 * | |
413 * [:source[position]:] must be "t". | |
414 */ | |
415 int parseTrue(int position) { | |
416 assert(source.codeUnitAt(position) == CHAR_t); | |
417 if (source.length < position + 4) fail(position, "Unexpected identifier"); | |
418 if (source.codeUnitAt(position + 1) != CHAR_r || | |
419 source.codeUnitAt(position + 2) != CHAR_u || | |
420 source.codeUnitAt(position + 3) != CHAR_e) { | |
421 fail(position); | |
422 } | |
423 listener.handleBool(true); | |
424 return position + 4; | |
425 } | |
426 | |
427 /** | |
428 * Parses a "false" literal starting at [position]. | |
429 * | |
430 * [:source[position]:] must be "f". | |
431 */ | |
432 int parseFalse(int position) { | |
433 assert(source.codeUnitAt(position) == CHAR_f); | |
434 if (source.length < position + 5) fail(position, "Unexpected identifier"); | |
435 if (source.codeUnitAt(position + 1) != CHAR_a || | |
436 source.codeUnitAt(position + 2) != CHAR_l || | |
437 source.codeUnitAt(position + 3) != CHAR_s || | |
438 source.codeUnitAt(position + 4) != CHAR_e) { | |
439 fail(position); | |
440 } | |
441 listener.handleBool(false); | |
442 return position + 5; | |
443 } | |
444 | |
445 /** Parses a "null" literal starting at [position]. | |
446 * | |
447 * [:source[position]:] must be "n". | |
448 */ | |
449 int parseNull(int position) { | |
450 assert(source.codeUnitAt(position) == CHAR_n); | |
451 if (source.length < position + 4) fail(position, "Unexpected identifier"); | |
452 if (source.codeUnitAt(position + 1) != CHAR_u || | |
453 source.codeUnitAt(position + 2) != CHAR_l || | |
454 source.codeUnitAt(position + 3) != CHAR_l) { | |
455 fail(position); | |
456 } | |
457 listener.handleNull(); | |
458 return position + 4; | |
459 } | |
460 | |
461 int parseString(int position) { | |
462 // Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"' | |
463 // Initial position is right after first '"'. | |
464 int start = position; | |
465 int char; | |
466 do { | |
467 if (position == source.length) { | |
468 fail(start - 1, "Unterminated string"); | |
469 } | |
470 char = source.codeUnitAt(position); | |
471 if (char == QUOTE) { | |
472 listener.handleString(source.substring(start, position)); | |
473 return position + 1; | |
474 } | |
475 if (char < SPACE) { | |
476 fail(position, "Control character in string"); | |
477 } | |
478 position++; | |
479 } while (char != BACKSLASH); | |
480 // Backslash escape detected. Collect character codes for rest of string. | |
481 int firstEscape = position - 1; | |
482 List<int> chars = <int>[]; | |
483 while (true) { | |
484 if (position == source.length) { | |
485 fail(start - 1, "Unterminated string"); | |
486 } | |
487 char = source.codeUnitAt(position); | |
488 switch (char) { | |
489 case CHAR_b: char = BACKSPACE; break; | |
490 case CHAR_f: char = FORM_FEED; break; | |
491 case CHAR_n: char = NEWLINE; break; | |
492 case CHAR_r: char = CARRIAGE_RETURN; break; | |
493 case CHAR_t: char = TAB; break; | |
494 case SLASH: | |
495 case BACKSLASH: | |
496 case QUOTE: | |
497 break; | |
498 case CHAR_u: | |
499 int hexStart = position - 1; | |
500 int value = 0; | |
501 for (int i = 0; i < 4; i++) { | |
502 position++; | |
503 if (position == source.length) { | |
504 fail(start - 1, "Unterminated string"); | |
505 } | |
506 char = source.codeUnitAt(position); | |
507 char -= 0x30; | |
508 if (char < 0) fail(hexStart, "Invalid unicode escape"); | |
509 if (char < 10) { | |
510 value = value * 16 + char; | |
511 } else { | |
512 char = (char | 0x20) - 0x31; | |
513 if (char < 0 || char > 5) { | |
514 fail(hexStart, "Invalid unicode escape"); | |
515 } | |
516 value = value * 16 + char + 10; | |
517 } | |
518 } | |
519 char = value; | |
520 break; | |
521 default: | |
522 if (char < SPACE) fail(position, "Control character in string"); | |
523 fail(position, "Unrecognized string escape"); | |
524 } | |
525 do { | |
526 chars.add(char); | |
527 position++; | |
528 if (position == source.length) fail(start - 1, "Unterminated string"); | |
529 char = source.codeUnitAt(position); | |
530 if (char == QUOTE) { | |
531 String result = new String.fromCharCodes(chars); | |
532 if (start < firstEscape) { | |
533 result = "${source.substring(start, firstEscape)}$result"; | |
534 } | |
535 listener.handleString(result); | |
536 return position + 1; | |
537 } | |
538 if (char < SPACE) { | |
539 fail(position, "Control character in string"); | |
540 } | |
541 } while (char != BACKSLASH); | |
542 position++; | |
543 } | |
544 } | |
545 | |
546 int _handleLiteral(start, position, isDouble) { | |
547 String literal = source.substring(start, position); | |
548 // This correctly creates -0 for doubles. | |
549 num value = (isDouble ? double.parse(literal) : int.parse(literal)); | |
550 listener.handleNumber(value); | |
551 return position; | |
552 } | |
553 | |
554 int parseNumber(int char, int position) { | |
555 // Format: | |
556 // '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)? | |
557 int start = position; | |
558 int length = source.length; | |
559 bool isDouble = false; | |
560 if (char == MINUS) { | |
561 position++; | |
562 if (position == length) fail(position, "Missing expected digit"); | |
563 char = source.codeUnitAt(position); | |
564 } | |
565 if (char < CHAR_0 || char > CHAR_9) { | |
566 fail(position, "Missing expected digit"); | |
567 } | |
568 if (char == CHAR_0) { | |
569 position++; | |
570 if (position == length) return _handleLiteral(start, position, false); | |
571 char = source.codeUnitAt(position); | |
572 if (CHAR_0 <= char && char <= CHAR_9) { | |
573 fail(position); | |
574 } | |
575 } else { | |
576 do { | |
577 position++; | |
578 if (position == length) return _handleLiteral(start, position, false); | |
579 char = source.codeUnitAt(position); | |
580 } while (CHAR_0 <= char && char <= CHAR_9); | |
581 } | |
582 if (char == DECIMALPOINT) { | |
583 isDouble = true; | |
584 position++; | |
585 if (position == length) fail(position, "Missing expected digit"); | |
586 char = source.codeUnitAt(position); | |
587 if (char < CHAR_0 || char > CHAR_9) fail(position); | |
588 do { | |
589 position++; | |
590 if (position == length) return _handleLiteral(start, position, true); | |
591 char = source.codeUnitAt(position); | |
592 } while (CHAR_0 <= char && char <= CHAR_9); | |
593 } | |
594 if (char == CHAR_e || char == CHAR_E) { | |
595 isDouble = true; | |
596 position++; | |
597 if (position == length) fail(position, "Missing expected digit"); | |
598 char = source.codeUnitAt(position); | |
599 if (char == PLUS || char == MINUS) { | |
600 position++; | |
601 if (position == length) fail(position, "Missing expected digit"); | |
602 char = source.codeUnitAt(position); | |
603 } | |
604 if (char < CHAR_0 || char > CHAR_9) { | |
605 fail(position, "Missing expected digit"); | |
606 } | |
607 do { | |
608 position++; | |
609 if (position == length) return _handleLiteral(start, position, true); | |
610 char = source.codeUnitAt(position); | |
611 } while (CHAR_0 <= char && char <= CHAR_9); | |
612 } | |
613 return _handleLiteral(start, position, isDouble); | |
614 } | |
615 | |
616 void fail(int position, [String message]) { | |
617 if (message == null) message = "Unexpected character"; | |
618 listener.fail(source, position, message); | |
619 // If the listener didn't throw, do it here. | |
620 String slice; | |
621 int sliceEnd = position + 20; | |
622 if (sliceEnd > source.length) { | |
623 slice = "'${source.substring(position)}'"; | |
624 } else { | |
625 slice = "'${source.substring(position, sliceEnd)}...'"; | |
626 } | |
627 throw new FormatException("Unexpected character at $position: $slice"); | |
628 } | |
629 } | |
OLD | NEW |