├── .github └── workflows │ ├── dart.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── main.dart └── repl.dart ├── lib ├── json5.dart └── src │ ├── parse.dart │ ├── stringify.dart │ ├── syntax_exception.dart │ ├── token.dart │ ├── unicode.dart │ └── util.dart ├── pubspec.yaml └── test ├── bugs_test.dart ├── indentation_test.dart ├── json5_test.dart ├── parse_error_test.dart ├── stringify_test.dart ├── to_encodable_default_test.dart ├── to_encodable_test.dart └── unicode_escape_test.dart /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | container: 15 | image: dart:latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Install dependencies 20 | run: dart pub get 21 | - name: Run tests 22 | run: dart pub run test 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish JSON5 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Publish 16 | uses: sakebook/actions-flutter-pub-publisher@v1.3.1 17 | with: 18 | credential: ${{ secrets.CREDENTIAL_JSON }} 19 | flutter_package: false 20 | skip_test: false 21 | dry_run: false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Omit commiting pubspec.lock for library packages: 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock 7 | pubspec.lock 8 | 9 | # Conventional directory for build outputs 10 | build/ 11 | 12 | # Directory created by dartdoc 13 | doc/api/ 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.8.2 2 | 3 | - Replaced pedantic package with lints package, fixed some analysis issues (#16) 4 | 5 | ## 0.8.1 6 | 7 | - Updated package description, fixed analysis issues (#15) 8 | 9 | ## 0.8.0 10 | 11 | - feat: values without decimal point or exponent should be `int` type 12 | 13 | ## 0.7.0 14 | 15 | - feat: toEncodable parameter 16 | - feat: toDecodable is default to `toMap()` then `toJson()` then `toString()` 17 | 18 | ```dart 19 | json5Encode(obj, toEncodable: (v) => v.toMap()) 20 | ``` 21 | 22 | ## 0.6.1 23 | 24 | - feat: serialize iterable objects 25 | - fix: JSON.stringify return non-null String 26 | - fix: indentication can be number and string 27 | 28 | ## 0.6.0 29 | 30 | - Migrating to null safety 31 | 32 | ## 0.5.5 33 | 34 | - hotfix: dart not support '\0' escape 35 | 36 | ## 0.5.4 37 | 38 | - Fix: RangeError when stringify a string with ending '\0' 39 | 40 | ## 0.5.3 41 | 42 | - fix: json5Decode() should return Map instead of Map 43 | 44 | ## 0.5.2 45 | 46 | - support runtime-js, flutter web 47 | 48 | ## 0.5.1 49 | 50 | - fix: cannot stringify object if their keys are not string . 51 | 52 | ## 0.5.0 53 | 54 | - supports Map, List, null, num, bool, String 55 | - `JSON5.parse( aString );` 56 | - `JSON5.stringify( anObj )` 57 | - `JSON5.stringify( abObj, space:2 )` 58 | - `json5Decode( aString );` 59 | - `json5Encode( anObj )` 60 | - `json5Encode( abObj, space:2 )` 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dam Ngoc Phat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## JSON5 for Dart and Flutter 2 | ![Dart CI](https://github.com/damphat/json5-dart/workflows/Dart%20CI/badge.svg) 3 | 4 | This Dart package is a port of [JSON5](https://github.com/json5/json5), originally written in pure JavaScript. 5 | 6 | It follows the same algorithms and specifications as the original JSON5, ensuring identical behavior. 7 | 8 | However, Dart has different naming conventions—see the example below. 9 | 10 | ## Usage 11 | 12 | ```dart 13 | import 'package:json5/json5.dart'; 14 | 15 | void main() { 16 | var obj = JSON5.parse(''' 17 | { 18 | /* Comment block */ 19 | name: { first: "Phat" }, 20 | lang: ["C++", "Dart", "Kotlin"], 21 | nums: [NaN, Infinity, -Infinity] 22 | } // End object 23 | '''); 24 | 25 | var compact = JSON5.stringify(obj); 26 | print(compact); 27 | 28 | var pretty = JSON5.stringify(obj, space: 2); 29 | print(pretty); 30 | } 31 | ``` 32 | 33 | ## References 34 | - [JSON5 Official Site](https://json5.org/) 35 | - [JSON5 JavaScript Implementation](https://github.com/json5/json5) 36 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:lints/core.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | exclude: 14 | - lib/src/unicode.dart 15 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | 3 | void main() { 4 | var obj = JSON5.parse('{ ' 5 | ' /* comment block */ ' 6 | ' name: {first: "phat"}, ' 7 | ' lang: ["C++", "dart", "kotlin"], ' 8 | ' nums: [NaN, Infinity, -Infinity] ' 9 | '} // end object '); 10 | 11 | var compact = JSON5.stringify(obj); 12 | 13 | print(compact); 14 | 15 | var pretty = JSON5.stringify(obj, space: 2); 16 | 17 | print(pretty); 18 | } 19 | -------------------------------------------------------------------------------- /example/repl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:json5/json5.dart'; 4 | 5 | void main() { 6 | dynamic state; 7 | while (true) { 8 | stdout.write('json5>'); 9 | var src = stdin.readLineSync(); 10 | if (src == null) { 11 | print('break'); 12 | return; 13 | } 14 | switch (src) { 15 | case '': 16 | continue; 17 | case '?': 18 | case 'h': 19 | case 'help': 20 | case '.help': 21 | print('JSON5 demonstration:'); 22 | print(' type in your json string'); 23 | print(' it will print out pretty '); 24 | print(' '); 25 | print('Commands'); 26 | print('> help : document'); 27 | print('> exit : quit the app'); 28 | print('> clear : clear the console'); 29 | continue; 30 | case 'exit': 31 | case 'quit': 32 | return; 33 | case 'cls': 34 | case 'clear': 35 | for (var i = 0; i < stdout.terminalLines; i++) { 36 | stdout.writeln(); 37 | } 38 | continue; 39 | } 40 | try { 41 | state = JSON5.parse(src); 42 | print(JSON5.stringify(state, space: 2)); 43 | } catch (e) { 44 | print(e); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/json5.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'src/parse.dart' as parser; 4 | import 'src/stringify.dart' as render; 5 | 6 | Object? _toEncodable(dynamic nonEncodable) { 7 | if (nonEncodable == null) return null; 8 | try { 9 | if (nonEncodable.toMap != null) return nonEncodable.toMap(); 10 | // ignore: empty_catches 11 | } on NoSuchMethodError {} 12 | 13 | try { 14 | if (nonEncodable.toJson != null) return nonEncodable.toJson(); 15 | // ignore: empty_catches 16 | } on NoSuchMethodError {} 17 | 18 | return nonEncodable.toString(); 19 | } 20 | 21 | /// [JSON5] contains 2 static methods [parse()] and [stringify()] 22 | /// 23 | /// The name [JSON5] is uppercase so that it compatible with document. 24 | /// 25 | /// If you love dart convention. You can also use [json5Encode()] and 26 | /// [json5Decode()] instead. 27 | abstract class JSON5 { 28 | JSON5._(); 29 | 30 | /// Convert the object to json5 string. 31 | /// 32 | /// Currently, only objects that inherit from [Map] or [List] 33 | /// are supported. 34 | /// 35 | /// The altenative is [json5Encode(string)] 36 | static String stringify( 37 | dynamic object, { 38 | space = 0, 39 | Object? Function(Object? nonEncodable)? toEncodable, 40 | }) { 41 | return render.stringify(object, null, space, toEncodable ?? _toEncodable); 42 | } 43 | 44 | /// Parses the string and return the json object. 45 | /// 46 | /// If the input string is not [json] or [json5], an 47 | /// [SyntaxException] will be thrown. 48 | /// 49 | /// The altenative is [json5Decode(string)] 50 | static dynamic parse(String string) { 51 | return parser.parse(string, null); 52 | } 53 | } 54 | 55 | /// Convert the object to json5 string. 56 | /// 57 | /// Currently, only objects that inherit from [Map] or [List] 58 | /// are supported. 59 | /// 60 | /// The altenative is [JSON5.stringify(string)] 61 | String json5Encode( 62 | dynamic object, { 63 | space = 0, 64 | Object? Function(Object? nonEncodable)? toEncodable, 65 | }) { 66 | return render.stringify(object, null, space, toEncodable ?? _toEncodable); 67 | } 68 | 69 | /// Parses the string and return the json object. 70 | /// 71 | /// If the input string is not [json] or [json5], an 72 | /// [SyntaxException] will be thrown. 73 | /// 74 | /// You can use the altenative method [JSON5.parse(string)] 75 | dynamic json5Decode(String string) { 76 | return parser.parse(string, null); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/parse.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/src/syntax_exception.dart'; 2 | 3 | import 'token.dart'; 4 | import 'util.dart' as util; 5 | 6 | late String source; 7 | String? parseState; 8 | late List stack; 9 | late int pos; 10 | late int line; 11 | late int column; 12 | late Token token; 13 | String? key; 14 | Object? root; 15 | 16 | Object? parse(String text, reviver) { 17 | source = text; 18 | parseState = 'start'; 19 | stack = []; 20 | pos = 0; 21 | line = 1; 22 | column = 0; 23 | // token = null; 24 | key = null; 25 | root = null; 26 | 27 | do { 28 | token = lex(); 29 | 30 | // This code is unreachable. 31 | // if (!parseStates[parseState]) { 32 | // throw invalidParseState() 33 | // } 34 | 35 | parseStates[parseState!]!(); 36 | } while (token.type != 'eof'); 37 | 38 | if (reviver is Function) { 39 | return internalize({'': root}, '', reviver); 40 | } 41 | 42 | return root; 43 | } 44 | 45 | List split(String text) { 46 | source = text; 47 | parseState = 'start'; 48 | stack = []; 49 | pos = 0; 50 | line = 1; 51 | column = 0; 52 | // token = null; 53 | key = null; 54 | root = null; 55 | 56 | var ret = []; 57 | for (;;) { 58 | token = lex(); 59 | if (token.type == 'eof') { 60 | return ret; 61 | } else { 62 | ret.add(token); 63 | } 64 | } 65 | } 66 | 67 | dynamic internalize(holder, name, reviver) { 68 | final value = holder[name]; 69 | if (value != null && value is Map) { 70 | for (final key in value.keys) { 71 | final replacement = internalize(value, key, reviver); 72 | if (replacement == null) { 73 | value.remove(key); 74 | } else { 75 | value[key] = replacement; 76 | } 77 | } 78 | } 79 | 80 | return reviver.call(holder, name, value); 81 | } 82 | 83 | String? lexState; 84 | String? buffer; 85 | late bool doubleQuote; 86 | late int sign; 87 | String? c; 88 | 89 | // not null; 90 | Token lex() { 91 | lexState = 'default'; 92 | buffer = ''; 93 | doubleQuote = false; 94 | sign = 1; 95 | 96 | for (;;) { 97 | c = peek(); 98 | 99 | // This code is unreachable. 100 | // if (!lexStates[lexState]) { 101 | // throw invalidLexState(lexState) 102 | // } 103 | 104 | final token = lexStates[lexState!]!(); 105 | if (token != null) { 106 | return token; 107 | } 108 | } 109 | } 110 | 111 | String? peek() { 112 | if (pos >= 0 && pos < source.length) return source[pos]; 113 | return null; 114 | } 115 | 116 | String? read() { 117 | final c = peek(); 118 | 119 | if (c == '\n') { 120 | line++; 121 | column = 0; 122 | } else if (c != null) { 123 | column += c.length; 124 | } else { 125 | column++; 126 | } 127 | 128 | if (c != null) { 129 | pos += c.length; 130 | } 131 | 132 | return c; 133 | } 134 | 135 | Map lexStates = { 136 | 'default': () { 137 | switch (c) { 138 | case '\t': 139 | case '\v': 140 | case '\f': 141 | case ' ': 142 | case '\u00A0': 143 | case '\uFEFF': 144 | case '\n': 145 | case '\r': 146 | case '\u2028': 147 | case '\u2029': 148 | read(); 149 | return null; //$ 150 | 151 | case '/': 152 | read(); 153 | lexState = 'comment'; 154 | return null; //$ 155 | 156 | default: 157 | if (c == null) { 158 | read(); 159 | return newToken('eof', null); 160 | } 161 | } 162 | 163 | if (util.isSpaceSeparator(c)) { 164 | read(); 165 | return null; //$ 166 | } 167 | 168 | // This code is unreachable. 169 | // if (!lexStates[parseState]) { 170 | // throw invalidLexState(parseState) 171 | // } 172 | 173 | return lexStates[parseState!]!(); 174 | }, 175 | 'comment': () { 176 | switch (c) { 177 | case '*': 178 | read(); 179 | lexState = 'multiLineComment'; 180 | return null; //$ 181 | 182 | case '/': 183 | read(); 184 | lexState = 'singleLineComment'; 185 | return null; //$ 186 | } 187 | 188 | throw invalidChar(read()); 189 | }, 190 | 'multiLineComment': () { 191 | switch (c) { 192 | case '*': 193 | read(); 194 | lexState = 'multiLineCommentAsterisk'; 195 | return null; //$ 196 | 197 | default: 198 | if (c == null) { 199 | throw invalidChar(read()); 200 | } 201 | } 202 | 203 | read(); 204 | return null; 205 | }, 206 | 'multiLineCommentAsterisk': () { 207 | switch (c) { 208 | case '*': 209 | read(); 210 | return; 211 | 212 | case '/': 213 | read(); 214 | lexState = 'default'; 215 | return null; //$ 216 | 217 | default: 218 | if (c == null) { 219 | throw invalidChar(read()); 220 | } 221 | } 222 | 223 | read(); 224 | lexState = 'multiLineComment'; 225 | return null; 226 | }, 227 | 'singleLineComment': () { 228 | switch (c) { 229 | case '\n': 230 | case '\r': 231 | case '\u2028': 232 | case '\u2029': 233 | read(); 234 | lexState = 'default'; 235 | return null; //$ 236 | 237 | default: 238 | if (c == null) { 239 | read(); 240 | return newToken('eof', null); 241 | } 242 | } 243 | 244 | read(); 245 | return null; 246 | }, 247 | 'value': () { 248 | switch (c) { 249 | case '{': 250 | case '[': 251 | return newToken('punctuator', read()); 252 | 253 | case 'n': 254 | read(); 255 | literal('ull'); 256 | return newToken('null', null); 257 | 258 | case 't': 259 | read(); 260 | literal('rue'); 261 | return newToken('boolean', true); 262 | 263 | case 'f': 264 | read(); 265 | literal('alse'); 266 | return newToken('boolean', false); 267 | 268 | case '-': 269 | case '+': 270 | if (read() == '-') { 271 | sign = -1; 272 | } 273 | 274 | lexState = 'sign'; 275 | return null; //$ 276 | 277 | case '.': 278 | buffer = read(); 279 | lexState = 'decimalPointLeading'; 280 | return null; //$ 281 | 282 | case '0': 283 | buffer = read(); 284 | lexState = 'zero'; 285 | return null; //$ 286 | 287 | case '1': 288 | case '2': 289 | case '3': 290 | case '4': 291 | case '5': 292 | case '6': 293 | case '7': 294 | case '8': 295 | case '9': 296 | buffer = read(); 297 | lexState = 'decimalInteger'; 298 | return null; //$ 299 | 300 | case 'I': 301 | read(); 302 | literal('nfinity'); 303 | return newToken('numeric', double.infinity); 304 | 305 | case 'N': 306 | read(); 307 | literal('aN'); 308 | return newToken('numeric', double.nan); 309 | 310 | case '"': 311 | case "'": 312 | doubleQuote = (read() == '"'); 313 | buffer = ''; 314 | lexState = 'string'; 315 | return null; //$ 316 | } 317 | 318 | throw invalidChar(read()); 319 | }, 320 | 'identifierNameStartEscape': () { 321 | if (c != 'u') { 322 | throw invalidChar(read()); 323 | } 324 | 325 | read(); 326 | final u = unicodeEscape(); 327 | switch (u) { 328 | case '\$': 329 | case '_': 330 | break; 331 | 332 | default: 333 | if (!util.isIdStartChar(u)) { 334 | throw invalidIdentifier(); 335 | } 336 | 337 | break; 338 | } 339 | 340 | buffer = buffer! + u; 341 | lexState = 'identifierName'; 342 | return null; 343 | }, 344 | 'identifierName': () { 345 | switch (c) { 346 | case '\$': 347 | case '_': 348 | case '\u200C': 349 | case '\u200D': 350 | buffer = buffer! + read()!; 351 | return null; //$ 352 | 353 | case '\\': 354 | read(); 355 | lexState = 'identifierNameEscape'; 356 | return null; //$ 357 | } 358 | 359 | if (util.isIdContinueChar(c)) { 360 | buffer = buffer! + read()!; 361 | return null; //$ 362 | } 363 | 364 | return newToken('identifier', buffer); 365 | }, 366 | 'identifierNameEscape': () { 367 | if (c != 'u') { 368 | throw invalidChar(read()); 369 | } 370 | 371 | read(); 372 | final u = unicodeEscape(); 373 | switch (u) { 374 | case '\$': 375 | case '_': 376 | case '\u200C': 377 | case '\u200D': 378 | break; 379 | 380 | default: 381 | if (!util.isIdContinueChar(u)) { 382 | throw invalidIdentifier(); 383 | } 384 | 385 | break; 386 | } 387 | 388 | buffer = buffer! + u; 389 | lexState = 'identifierName'; 390 | return null; 391 | }, 392 | 'sign': () { 393 | switch (c) { 394 | case '.': 395 | buffer = read(); 396 | lexState = 'decimalPointLeading'; 397 | return null; //$ 398 | 399 | case '0': 400 | buffer = read(); 401 | lexState = 'zero'; 402 | return null; //$ 403 | 404 | case '1': 405 | case '2': 406 | case '3': 407 | case '4': 408 | case '5': 409 | case '6': 410 | case '7': 411 | case '8': 412 | case '9': 413 | buffer = read(); 414 | lexState = 'decimalInteger'; 415 | return null; //$ 416 | 417 | case 'I': 418 | read(); 419 | literal('nfinity'); 420 | return newToken('numeric', sign * double.infinity); 421 | 422 | case 'N': 423 | read(); 424 | literal('aN'); 425 | return newToken('numeric', double.nan); 426 | } 427 | 428 | throw invalidChar(read()); 429 | }, 430 | 'zero': () { 431 | switch (c) { 432 | case '.': 433 | buffer = buffer! + read()!; 434 | lexState = 'decimalPoint'; 435 | return null; //$ 436 | 437 | case 'e': 438 | case 'E': 439 | buffer = buffer! + read()!; 440 | lexState = 'decimalExponent'; 441 | return null; //$ 442 | 443 | case 'x': 444 | case 'X': 445 | buffer = buffer! + read()!; 446 | lexState = 'hexadecimal'; 447 | return null; //$ 448 | } 449 | 450 | return newToken('numeric', sign * 0); 451 | }, 452 | 'decimalInteger': () { 453 | switch (c) { 454 | case '.': 455 | buffer = buffer! + read()!; 456 | lexState = 'decimalPoint'; 457 | return null; //$ 458 | 459 | case 'e': 460 | case 'E': 461 | buffer = buffer! + read()!; 462 | lexState = 'decimalExponent'; 463 | return null; //$ 464 | } 465 | 466 | if (util.isDigit(c)) { 467 | buffer = buffer! + read()!; 468 | return null; //$ 469 | } 470 | 471 | return newToken('numeric', sign * int.parse(buffer!)); 472 | }, 473 | 'decimalPointLeading': () { 474 | if (util.isDigit(c)) { 475 | buffer = buffer! + read()!; 476 | lexState = 'decimalFraction'; 477 | return null; //$ 478 | } 479 | 480 | throw invalidChar(read()); 481 | }, 482 | 'decimalPoint': () { 483 | switch (c) { 484 | case 'e': 485 | case 'E': 486 | buffer = buffer! + read()!; 487 | lexState = 'decimalExponent'; 488 | return null; //$ 489 | } 490 | 491 | if (util.isDigit(c)) { 492 | buffer = buffer! + read()!; 493 | lexState = 'decimalFraction'; 494 | return null; //$ 495 | } 496 | 497 | return newToken('numeric', sign * double.parse(buffer!)); 498 | }, 499 | 'decimalFraction': () { 500 | switch (c) { 501 | case 'e': 502 | case 'E': 503 | buffer = buffer! + read()!; 504 | lexState = 'decimalExponent'; 505 | return null; //$ 506 | } 507 | 508 | if (util.isDigit(c)) { 509 | buffer = buffer! + read()!; 510 | return null; //$ 511 | } 512 | 513 | return newToken('numeric', sign * double.parse(buffer!)); 514 | }, 515 | 'decimalExponent': () { 516 | switch (c) { 517 | case '+': 518 | case '-': 519 | buffer = buffer! + read()!; 520 | lexState = 'decimalExponentSign'; 521 | return null; //$ 522 | } 523 | 524 | if (util.isDigit(c)) { 525 | buffer = buffer! + read()!; 526 | lexState = 'decimalExponentInteger'; 527 | return null; //$ 528 | } 529 | 530 | throw invalidChar(read()); 531 | }, 532 | 'decimalExponentSign': () { 533 | if (util.isDigit(c)) { 534 | buffer = buffer! + read()!; 535 | lexState = 'decimalExponentInteger'; 536 | return null; //$ 537 | } 538 | 539 | throw invalidChar(read()); 540 | }, 541 | 'decimalExponentInteger': () { 542 | if (util.isDigit(c)) { 543 | buffer = buffer! + read()!; 544 | return null; //$ 545 | } 546 | 547 | return newToken('numeric', sign * double.parse(buffer!)); 548 | }, 549 | 'hexadecimal': () { 550 | if (util.isHexDigit(c)) { 551 | buffer = buffer! + read()!; 552 | lexState = 'hexadecimalInteger'; 553 | return null; //$ 554 | } 555 | 556 | throw invalidChar(read()); 557 | }, 558 | 'hexadecimalInteger': () { 559 | if (util.isHexDigit(c)) { 560 | buffer = buffer! + read()!; 561 | return null; //$ 562 | } 563 | 564 | return newToken('numeric', sign * double.parse(buffer!)); 565 | }, 566 | 'string': () { 567 | switch (c) { 568 | case '\\': 569 | read(); 570 | buffer = buffer! + escape()!; 571 | return null; //$ 572 | 573 | case '"': 574 | if (doubleQuote) { 575 | read(); 576 | return newToken('string', buffer); 577 | } 578 | 579 | buffer = buffer! + read()!; 580 | return null; //$ 581 | 582 | case "'": 583 | if (!doubleQuote) { 584 | read(); 585 | return newToken('string', buffer); 586 | } 587 | 588 | buffer = buffer! + read()!; 589 | return null; //$ 590 | 591 | case '\n': 592 | case '\r': 593 | throw invalidChar(read()); 594 | 595 | case '\u2028': 596 | case '\u2029': 597 | separatorChar(c); 598 | break; 599 | 600 | default: 601 | if (c == null) { 602 | throw invalidChar(read()); 603 | } 604 | } 605 | 606 | buffer = buffer! + read()!; 607 | return null; 608 | }, 609 | 'start': () { 610 | switch (c) { 611 | case '{': 612 | case '[': 613 | return newToken('punctuator', read()); 614 | 615 | // This code is unreachable since the default lexState handles eof. 616 | // case null: 617 | // return newToken('eof') 618 | } 619 | 620 | lexState = 'value'; 621 | return null; 622 | }, 623 | 'beforePropertyName': () { 624 | switch (c) { 625 | case '\$': 626 | case '_': 627 | buffer = read(); 628 | lexState = 'identifierName'; 629 | return null; //$ 630 | 631 | case '\\': 632 | read(); 633 | lexState = 'identifierNameStartEscape'; 634 | return null; //$ 635 | 636 | case '}': 637 | return newToken('punctuator', read()); 638 | 639 | case '"': 640 | case "'": 641 | doubleQuote = (read() == '"'); 642 | lexState = 'string'; 643 | return null; //$ 644 | } 645 | 646 | if (util.isIdStartChar(c)) { 647 | buffer = buffer! + read()!; 648 | lexState = 'identifierName'; 649 | return null; //$ 650 | } 651 | 652 | throw invalidChar(read()); 653 | }, 654 | 'afterPropertyName': () { 655 | if (c == ':') { 656 | return newToken('punctuator', read()); 657 | } 658 | 659 | throw invalidChar(read()); 660 | }, 661 | 'beforePropertyValue': () { 662 | lexState = 'value'; 663 | return null; 664 | }, 665 | 'afterPropertyValue': () { 666 | switch (c) { 667 | case ',': 668 | case '}': 669 | return newToken('punctuator', read()); 670 | } 671 | 672 | throw invalidChar(read()); 673 | }, 674 | 'beforeArrayValue': () { 675 | if (c == ']') { 676 | return newToken('punctuator', read()); 677 | } 678 | 679 | lexState = 'value'; 680 | return null; 681 | }, 682 | 'afterArrayValue': () { 683 | switch (c) { 684 | case ',': 685 | case ']': 686 | return newToken('punctuator', read()); 687 | } 688 | 689 | throw invalidChar(read()); 690 | }, 691 | 'end': () { 692 | // This code is unreachable since it's handled by the default lexState. 693 | // if (c == null) { 694 | // read() 695 | // return newToken('eof') 696 | // } 697 | 698 | throw invalidChar(read()); 699 | }, 700 | }; 701 | 702 | Token newToken(String type, Object? value) { 703 | return Token(type, value, line, column); 704 | } 705 | 706 | void literal(String s) { 707 | var len = s.length; 708 | for (var i = 0; i < len; i++) { 709 | final p = peek(); 710 | 711 | if (p != s[i]) { 712 | throw invalidChar(read()); 713 | } 714 | 715 | read(); 716 | } 717 | } 718 | 719 | String? escape() { 720 | final c = peek(); 721 | switch (c) { 722 | case 'b': 723 | read(); 724 | return '\b'; 725 | 726 | case 'f': 727 | read(); 728 | return '\f'; 729 | 730 | case 'n': 731 | read(); 732 | return '\n'; 733 | 734 | case 'r': 735 | read(); 736 | return '\r'; 737 | 738 | case 't': 739 | read(); 740 | return '\t'; 741 | 742 | case 'v': 743 | read(); 744 | return '\v'; 745 | 746 | case '0': 747 | read(); 748 | if (util.isDigit(peek())) { 749 | throw invalidChar(read()); 750 | } 751 | 752 | return '\u0000'; 753 | 754 | case 'x': 755 | read(); 756 | return hexEscape(); 757 | 758 | case 'u': 759 | read(); 760 | return unicodeEscape(); 761 | 762 | case '\n': 763 | case '\u2028': 764 | case '\u2029': 765 | read(); 766 | return ''; 767 | 768 | case '\r': 769 | read(); 770 | if (peek() == '\n') { 771 | read(); 772 | } 773 | 774 | return ''; 775 | 776 | case '1': 777 | case '2': 778 | case '3': 779 | case '4': 780 | case '5': 781 | case '6': 782 | case '7': 783 | case '8': 784 | case '9': 785 | throw invalidChar(read()); 786 | default: 787 | if (c == null) { 788 | throw invalidChar(read()); 789 | } 790 | } 791 | 792 | return read(); 793 | } 794 | 795 | String hexEscape() { 796 | var buffer = ''; 797 | var c = peek(); 798 | 799 | if (!util.isHexDigit(c)) { 800 | throw invalidChar(read()); 801 | } 802 | 803 | buffer += read()!; 804 | 805 | c = peek(); 806 | if (!util.isHexDigit(c)) { 807 | throw invalidChar(read()); 808 | } 809 | 810 | buffer += read()!; 811 | 812 | return String.fromCharCode(int.parse(buffer, radix: 16)); 813 | } 814 | 815 | String unicodeEscape() { 816 | var buffer = ''; 817 | var count = 4; 818 | 819 | while (count-- > 0) { 820 | final c = peek(); 821 | if (!util.isHexDigit(c)) { 822 | throw invalidChar(read()); 823 | } 824 | 825 | buffer += read()!; 826 | } 827 | 828 | return String.fromCharCode(int.parse(buffer, radix: 16)); 829 | } 830 | 831 | final Map parseStates = { 832 | 'start': () { 833 | if (token.type == 'eof') { 834 | throw invalidEOF(); 835 | } 836 | 837 | push(); 838 | }, 839 | 'beforePropertyName': () { 840 | switch (token.type) { 841 | case 'identifier': 842 | case 'string': 843 | key = token.value as String?; 844 | parseState = 'afterPropertyName'; 845 | return null; //$ 846 | 847 | case 'punctuator': 848 | // This code is unreachable since it's handled by the lexState. 849 | // if (token.value != '}') { 850 | // throw invalidToken() 851 | // } 852 | 853 | pop(); 854 | return null; //$ 855 | 856 | case 'eof': 857 | throw invalidEOF(); 858 | } 859 | 860 | // This code is unreachable since it's handled by the lexState. 861 | // throw invalidToken() 862 | }, 863 | 'afterPropertyName': () { 864 | // This code is unreachable since it's handled by the lexState. 865 | // if (token.type != 'punctuator' || token.value != ':') { 866 | // throw invalidToken() 867 | // } 868 | 869 | if (token.type == 'eof') { 870 | throw invalidEOF(); 871 | } 872 | 873 | parseState = 'beforePropertyValue'; 874 | }, 875 | 'beforePropertyValue': () { 876 | if (token.type == 'eof') { 877 | throw invalidEOF(); 878 | } 879 | 880 | push(); 881 | }, 882 | 'beforeArrayValue': () { 883 | if (token.type == 'eof') { 884 | throw invalidEOF(); 885 | } 886 | 887 | if (token.type == 'punctuator' && token.value == ']') { 888 | pop(); 889 | return null; //$ 890 | } 891 | 892 | push(); 893 | }, 894 | 'afterPropertyValue': () { 895 | // This code is unreachable since it's handled by the lexState. 896 | // if (token.type != 'punctuator') { 897 | // throw invalidToken() 898 | // } 899 | 900 | if (token.type == 'eof') { 901 | throw invalidEOF(); 902 | } 903 | 904 | switch (token.value) { 905 | case ',': 906 | parseState = 'beforePropertyName'; 907 | return null; //$ 908 | 909 | case '}': 910 | pop(); 911 | } 912 | 913 | // This code is unreachable since it's handled by the lexState. 914 | // throw invalidToken() 915 | }, 916 | 'afterArrayValue': () { 917 | // This code is unreachable since it's handled by the lexState. 918 | // if (token.type != 'punctuator') { 919 | // throw invalidToken() 920 | // } 921 | 922 | if (token.type == 'eof') { 923 | throw invalidEOF(); 924 | } 925 | 926 | switch (token.value) { 927 | case ',': 928 | parseState = 'beforeArrayValue'; 929 | return null; //$ 930 | 931 | case ']': 932 | pop(); 933 | } 934 | 935 | // This code is unreachable since it's handled by the lexState. 936 | // throw invalidToken() 937 | }, 938 | 'end': () { 939 | // This code is unreachable since it's handled by the lexState. 940 | // if (token.type != 'eof') { 941 | // throw invalidToken() 942 | // } 943 | }, 944 | }; 945 | 946 | bool isObject(dynamic value) => value is Map || value is List; 947 | void push() { 948 | dynamic value; 949 | 950 | switch (token.type) { 951 | case 'punctuator': 952 | switch (token.value) { 953 | case '{': 954 | value = {}; 955 | break; 956 | 957 | case '[': 958 | value = []; 959 | break; 960 | } 961 | 962 | break; 963 | 964 | case 'null': 965 | case 'boolean': 966 | case 'numeric': 967 | case 'string': 968 | value = token.value; 969 | break; 970 | 971 | // This code is unreachable. 972 | // default: 973 | // throw invalidToken() 974 | } 975 | 976 | if (root == null) { 977 | root = value; 978 | } else { 979 | final parent = stack[stack.length - 1]; 980 | if (parent is List) { 981 | parent.add(value); 982 | } else { 983 | parent[key] = value; 984 | } 985 | } 986 | 987 | if (isObject(value)) { 988 | stack.add(value); 989 | 990 | if (value is List) { 991 | parseState = 'beforeArrayValue'; 992 | } else { 993 | parseState = 'beforePropertyName'; 994 | } 995 | } else { 996 | final current = stack.isEmpty ? null : stack[stack.length - 1]; 997 | if (current == null) { 998 | parseState = 'end'; 999 | } else if (current is List) { 1000 | parseState = 'afterArrayValue'; 1001 | } else { 1002 | parseState = 'afterPropertyValue'; 1003 | } 1004 | } 1005 | } 1006 | 1007 | void pop() { 1008 | stack.removeLast(); 1009 | 1010 | final current = stack.isEmpty ? null : stack[stack.length - 1]; 1011 | if (current == null) { 1012 | parseState = 'end'; 1013 | } else if (current is List) { 1014 | parseState = 'afterArrayValue'; 1015 | } else { 1016 | parseState = 'afterPropertyValue'; 1017 | } 1018 | } 1019 | 1020 | // This code is unreachable. 1021 | // dynamic invalidParseState () { 1022 | // return new Error(`JSON5: invalid parse state '${parseState}'`) 1023 | // } 1024 | 1025 | // This code is unreachable. 1026 | // dynamic invalidLexState (state) { 1027 | // return new Error(`JSON5: invalid lex state '${state}'`) 1028 | // } 1029 | 1030 | SyntaxException invalidChar(c) { 1031 | if (c == null) { 1032 | return syntaxError('JSON5: invalid end of input at $line:$column'); 1033 | } 1034 | 1035 | return syntaxError( 1036 | "JSON5: invalid character '${formatChar(c)}' at $line:$column"); 1037 | } 1038 | 1039 | SyntaxException invalidEOF() { 1040 | return syntaxError('JSON5: invalid end of input at $line:$column'); 1041 | } 1042 | 1043 | // This code is unreachable. 1044 | // dynamic invalidToken () { 1045 | // if (token.type == 'eof') { 1046 | // return syntaxError("JSON5: invalid end of input at ${line}:${column}") 1047 | // } 1048 | 1049 | // final c = String.fromCodePoint(token.value.codePointAt(0)) 1050 | // return syntaxError("JSON5: invalid character '${formatChar(c)}' at ${line}:${column}") 1051 | // } 1052 | 1053 | SyntaxException invalidIdentifier() { 1054 | column -= 5; 1055 | return syntaxError('JSON5: invalid identifier character at $line:$column'); 1056 | } 1057 | 1058 | void separatorChar(c) { 1059 | // FIXME: how to print error 1060 | print( 1061 | "JSON5: '${formatChar(c)}' in strings is not valid ECMAScript; consider escaping"); 1062 | } 1063 | 1064 | String? formatChar(String? c) { 1065 | const replacements = { 1066 | "'": "\\'", 1067 | '"': '\\"', 1068 | '\\': '\\\\', 1069 | '\b': '\\b', 1070 | '\f': '\\f', 1071 | '\n': '\\n', 1072 | '\r': '\\r', 1073 | '\t': '\\t', 1074 | '\v': '\\v', 1075 | '\u0000': '\\0', 1076 | '\u2028': '\\u2028', 1077 | '\u2029': '\\u2029', 1078 | }; 1079 | 1080 | if (replacements[c!] != null) { 1081 | return replacements[c]; 1082 | } 1083 | 1084 | if (c.codeUnitAt(0) < ' '.codeUnitAt(0)) { 1085 | final hexString = c.codeUnitAt(0).toRadixString(16); 1086 | return '\\x' + ('00' + hexString).substring(hexString.length); 1087 | } 1088 | 1089 | return c; 1090 | } 1091 | 1092 | SyntaxException syntaxError(String message) { 1093 | final err = SyntaxException(message, line, column); 1094 | return err; 1095 | } 1096 | -------------------------------------------------------------------------------- /lib/src/stringify.dart: -------------------------------------------------------------------------------- 1 | import 'util.dart' as util; 2 | import 'dart:math' show min; 3 | 4 | class _Stringify { 5 | final stack = []; 6 | var indent = ''; 7 | var propertyList; 8 | var replacerFunc; 9 | var gap = ''; 10 | var quote; 11 | Object? Function(Object? nonEncodable)? toEncodable; 12 | 13 | String eval( 14 | dynamic value, 15 | replacer, 16 | space, 17 | Object? Function(Object? nonEncodable)? toEncodable, 18 | ) { 19 | assert(replacer == null); 20 | assert(space is num || space is String); 21 | this.toEncodable = toEncodable; 22 | 23 | if (space is num) { 24 | if (space > 0) { 25 | space = min(10, space.floor()); 26 | gap = ' '.substring(0, space as int?); 27 | } 28 | } else if (space is String) { 29 | gap = space.length > 10 ? space.substring(0, 10) : space; 30 | } 31 | 32 | return serializeProperty('', {'': value}); 33 | } 34 | 35 | String quoteString(String value) { 36 | final quotes = { 37 | "'": 0.1, 38 | '"': 0.2, 39 | }; 40 | 41 | const replacements = { 42 | "'": "\\'", 43 | '"': '\\"', 44 | '\\': '\\\\', 45 | '\b': '\\b', 46 | '\f': '\\f', 47 | '\n': '\\n', 48 | '\r': '\\r', 49 | '\t': '\\t', 50 | '\v': '\\v', 51 | '\u0000': '\\0', 52 | '\u2028': '\\u2028', 53 | '\u2029': '\\u2029', 54 | }; 55 | 56 | var product = ''; 57 | 58 | for (var i = 0; i < value.length; i++) { 59 | final c = value[i]; 60 | switch (c) { 61 | case "'": 62 | case '"': 63 | quotes[c]++; 64 | product += c; 65 | continue; 66 | 67 | case '\u0000': 68 | if ((i + 1 < value.length) && util.isDigit(value[i + 1])) { 69 | product += '\\x00'; 70 | continue; 71 | } 72 | } 73 | 74 | if (replacements.containsKey(c)) { 75 | product += replacements[c]!; 76 | continue; 77 | } 78 | 79 | if (c.codeUnitAt(0) < ' '.codeUnitAt(0)) { 80 | var hexString = c.codeUnitAt(0).toRadixString(16); 81 | product += '\\x' + ('00' + hexString).substring(hexString.length); 82 | continue; 83 | } 84 | 85 | product += c; 86 | } 87 | 88 | final quoteChar = quote ?? 89 | quotes.keys.reduce((a, b) => (quotes[a]! < quotes[b]!) ? a : b); 90 | 91 | // FIXME replaceall + doall? 92 | product = product.replaceAll( 93 | RegExp(quoteChar, dotAll: true), replacements[quoteChar]!); 94 | 95 | return quoteChar + product + quoteChar; 96 | } 97 | 98 | String serializeProperty(dynamic key, dynamic holder) { 99 | Object? value = holder[key]; 100 | 101 | String? serializedValue(value) { 102 | if (value == null) return 'null'; 103 | switch (value) { 104 | case true: 105 | return 'true'; 106 | case false: 107 | return 'false'; 108 | } 109 | 110 | if (value is String) { 111 | return quoteString(value); // , false? 112 | } 113 | 114 | if (value is num) { 115 | return value.toString(); 116 | } 117 | 118 | if (value is List) return serializeArray(value); 119 | if (value is Map) return serializeObject(value); 120 | 121 | if (value is Iterable) return serializeArray(value.toList()); 122 | 123 | return null; 124 | } 125 | 126 | var result = serializedValue(value); 127 | if (result != null) { 128 | return result; 129 | } 130 | 131 | if (toEncodable != null) { 132 | value = toEncodable!(value); 133 | } 134 | 135 | result = serializedValue(value); 136 | if (result != null) { 137 | return result; 138 | } 139 | 140 | throw Exception('Cannot stringify $value'); // undefined 141 | } 142 | 143 | String? serializeKey(String key) { 144 | if (key.isEmpty) { 145 | return quoteString(key); 146 | } 147 | 148 | final firstChar = key[0]; 149 | if (!util.isIdStartChar(firstChar)) { 150 | return quoteString(key); 151 | } 152 | 153 | for (var i = firstChar.length; i < key.length; i++) { 154 | if (!util.isIdContinueChar(key[i])) { 155 | return quoteString(key); 156 | } 157 | } 158 | 159 | return key; 160 | } 161 | 162 | String serializeObject(value) { 163 | if (stack.contains(value)) { 164 | throw Exception('Converting circular structure to JSON5'); 165 | } 166 | 167 | stack.add(value); 168 | 169 | var stepback = indent; 170 | indent = indent + gap; 171 | 172 | var keys = propertyList ?? value.keys; 173 | var partial = []; 174 | for (final key in keys) { 175 | final propertyString = serializeProperty(key, value); 176 | var member = serializeKey(key.toString())! + ':'; 177 | if (gap != '') { 178 | member += ' '; 179 | } 180 | member += propertyString; 181 | partial.add(member); 182 | } 183 | 184 | String final_; 185 | if (partial.isEmpty) { 186 | final_ = '{}'; 187 | } else { 188 | String properties; 189 | if (gap == '') { 190 | properties = partial.join(','); 191 | final_ = '{' + properties + '}'; 192 | } else { 193 | var separator = ',\n' + indent; 194 | properties = partial.join(separator); 195 | final_ = '{\n' + indent + properties + ',\n' + stepback + '}'; 196 | } 197 | } 198 | 199 | stack.removeLast(); 200 | 201 | indent = stepback; 202 | return final_; 203 | } 204 | 205 | String serializeArray(List value) { 206 | if (stack.contains(value)) { 207 | throw Exception('Converting circular structure to JSON5'); 208 | } 209 | 210 | stack.add(value); 211 | 212 | var stepback = indent; 213 | indent = indent + gap; 214 | 215 | var partial = []; 216 | for (var i = 0; i < value.length; i++) { 217 | final propertyString = serializeProperty(i, value); 218 | partial.add(propertyString); 219 | } 220 | 221 | String final_; 222 | if (partial.isEmpty) { 223 | final_ = '[]'; 224 | } else { 225 | if (gap == '') { 226 | var properties = partial.join(','); 227 | final_ = '[' + properties + ']'; 228 | } else { 229 | var separator = ',\n' + indent; 230 | var properties = partial.join(separator); 231 | final_ = '[\n' + indent + properties + ',\n' + stepback + ']'; 232 | } 233 | } 234 | 235 | stack.removeLast(); 236 | indent = stepback; 237 | return final_; 238 | } 239 | } 240 | 241 | String stringify( 242 | dynamic value, 243 | replacer, 244 | space, 245 | Object? Function(Object? nonEncodable)? toEncodable, 246 | ) { 247 | return _Stringify().eval(value, replacer, space, toEncodable); 248 | } 249 | -------------------------------------------------------------------------------- /lib/src/syntax_exception.dart: -------------------------------------------------------------------------------- 1 | class SyntaxException implements Exception { 2 | final String message; 3 | final int? lineNumber; 4 | final int? columnNumber; 5 | SyntaxException(this.message, this.lineNumber, this.columnNumber); 6 | @override 7 | String toString() { 8 | return 'SyntaxException: $message'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/token.dart: -------------------------------------------------------------------------------- 1 | class Token { 2 | final String type; 3 | final Object? value; 4 | final int? line; 5 | final int? column; 6 | Token(this.type, this.value, this.line, this.column); 7 | 8 | @override 9 | String toString() => '[$type: $value]'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/unicode.dart: -------------------------------------------------------------------------------- 1 | // This is a generated file. Do not edit. 2 | final Space_Separator = RegExp(r'[\u1680\u2000-\u200A\u202F\u205F\u3000]'); 3 | final ID_Start = RegExp( 4 | r'[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]'); 5 | final ID_Continue = RegExp( 6 | r'[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]'); 7 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | import 'unicode.dart' as unicode; 2 | 3 | bool isSpaceSeparator(c) { 4 | return c is String && unicode.Space_Separator.hasMatch(c); 5 | } 6 | 7 | bool isIdStartChar(c) { 8 | return c is String && 9 | (RegExp(r'[a-zA-Z_$]').hasMatch(c) || unicode.ID_Start.hasMatch(c)); 10 | } 11 | 12 | bool isIdContinueChar(c) { 13 | return c is String && 14 | // FIXME: check unicode 15 | (RegExp(r'[a-z0-9A-Z_\u200C\u200D$]').hasMatch(c) || 16 | unicode.ID_Continue.hasMatch(c)); 17 | } 18 | 19 | bool isDigit(c) { 20 | return c is String && RegExp(r'[0-9]').hasMatch(c); 21 | } 22 | 23 | bool isHexDigit(c) { 24 | return c is String && RegExp(r'[0-9A-Fa-f]').hasMatch(c); 25 | } 26 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: json5 2 | description: A lightweight library for encoding and decoding JSON5 with comments and trailing commas support. 3 | version: 0.8.2 4 | homepage: https://github.com/damphat/json5-dart 5 | 6 | environment: 7 | sdk: ">=2.17.1 <4.0.0" 8 | 9 | dev_dependencies: 10 | lints: ^3.0.0 11 | test: ^1.14.4 12 | -------------------------------------------------------------------------------- /test/bugs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('should return type Map', () { 6 | // BUG: 7 | // '_InternalLinkedHashMap' is not 8 | // a subtype of 'Map' 9 | Map? obj = JSON5.parse('{x: 1}'); 10 | expect(obj, {'x': 1}); 11 | }); 12 | 13 | test('non_string_keys', () { 14 | expect(JSON5.stringify({1: 1}), "{'1':1}"); 15 | expect(JSON5.stringify({-1: 1}), "{'-1':1}"); 16 | expect(JSON5.stringify({null: 1}), '{null:1}'); 17 | expect(JSON5.stringify({true: 1}), '{true:1}'); 18 | expect(JSON5.stringify({false: 1}), '{false:1}'); 19 | expect(JSON5.stringify({double.infinity: 1}), '{Infinity:1}'); 20 | }); 21 | 22 | test('null char at the end', () { 23 | // BUG: 24 | // RangeError: not in inclusive range 0..3 25 | expect(JSON5.stringify('abc\u0000'), "'abc\\0'"); 26 | }); 27 | 28 | test('dart does not support backslash-zero escape (\\0)', () { 29 | // BUG: 30 | // javascript has '\0' but dart has not 31 | expect(JSON5.stringify('0'), "'0'"); 32 | expect(JSON5.stringify('\0'), "'0'"); 33 | expect(JSON5.stringify('\u0000'), "'\\0'"); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/indentation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('indentation', () { 6 | test('space: null', () { 7 | expect(JSON5.stringify({}), '{}'); 8 | expect(JSON5.stringify({'x': 1}), '{x:1}'); 9 | expect(JSON5.stringify({'x': 1, 'y': 2}), '{x:1,y:2}'); 10 | 11 | expect(JSON5.stringify([]), '[]'); 12 | expect(JSON5.stringify([1]), '[1]'); 13 | expect(JSON5.stringify([1, 2]), '[1,2]'); 14 | }); 15 | 16 | test('space: 0', () { 17 | expect(JSON5.stringify({'x': 1}, space: 0), '{x:1}'); 18 | }); 19 | 20 | test('space: 2', () { 21 | expect(JSON5.stringify({'x': 1}, space: 2), '{\n x: 1,\n}'); 22 | expect(JSON5.stringify([1], space: 2), '[\n 1,\n]'); 23 | }); 24 | 25 | test('space: 2.5 (same as 2)', () { 26 | expect(JSON5.stringify({'x': 1}, space: 2.5), '{\n x: 1,\n}'); 27 | expect(JSON5.stringify([1], space: 2.5), '[\n 1,\n]'); 28 | }); 29 | 30 | test('space: "===="', () { 31 | expect(JSON5.stringify({'x': 1}, space: '===='), '{\n====x: 1,\n}'); 32 | expect(JSON5.stringify([1], space: '===='), '[\n====1,\n]'); 33 | }); 34 | test('space: "1234567890123" (trim with limit 10 chars)', () { 35 | expect(JSON5.stringify({'x': 1}, space: '1234567890123'), 36 | '{\n1234567890x: 1,\n}'); 37 | 38 | expect( 39 | JSON5.stringify([1], space: '123456789_123'), '[\n123456789_1,\n]'); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/json5_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('JSON.parse', () { 6 | test('primitives', () { 7 | expect(JSON5.parse('null'), isNull); 8 | expect(JSON5.parse('true'), isTrue); 9 | expect(JSON5.parse('false'), isFalse); 10 | expect(JSON5.parse('0'), TypeMatcher()); 11 | expect(JSON5.parse('1'), TypeMatcher()); 12 | expect(JSON5.parse('0.0'), TypeMatcher()); 13 | expect(JSON5.parse('1.0'), TypeMatcher()); 14 | expect(JSON5.parse('-90e+09'), -90e+09); 15 | expect(JSON5.parse('NaN'), isNaN); 16 | expect(JSON5.parse('Infinity'), double.infinity); 17 | expect(JSON5.parse('-Infinity'), -double.infinity); 18 | }); 19 | 20 | test('string-unicode', () { 21 | expect(JSON5.parse(r'"👌😁👍"'), '👌😁👍'); 22 | }); 23 | test('string-escape', () { 24 | expect(JSON5.parse(r'"\b\f\v\r\n\t\\"'), '\b\f\v\r\n\t\\'); 25 | expect(JSON5.parse(r'"\u0123 \u4567"'), '\u0123 \u4567'); 26 | expect(JSON5.parse(r'"\u8901 \uabcd"'), '\u8901 \uabcd'); 27 | expect(JSON5.parse(r'"\uefAB \uCDEF"'), '\uefAB \uCDEF'); 28 | expect(JSON5.parse(r'"\uefAB \uCDEF"'), '\uefAB \uCDEF'); 29 | expect(JSON5.parse(r'"\x20"'), ' '); 30 | 31 | '\b\f\v\r\n\t\u0000'.runes.forEach((e) { 32 | expect(e, lessThan(32)); 33 | }); 34 | }); 35 | 36 | test('JSON5.stringify-compact', () { 37 | expect( 38 | JSON5.stringify({ 39 | 'x': [1, 2] 40 | }), 41 | '{x:[1,2]}'); 42 | }); 43 | test('JSON5.stringify - pretty', () { 44 | var actual = JSON5.stringify({ 45 | 'x': [1, 2] 46 | }, space: 2); 47 | expect(actual, '{\n x: [\n 1,\n 2,\n ],\n}'); 48 | }); 49 | }); 50 | 51 | test('json5Decode', () { 52 | expect(json5Decode("{'x':[1,2]}"), { 53 | 'x': [1, 2] 54 | }); 55 | }); 56 | 57 | test('json5Encode', () { 58 | expect( 59 | json5Encode({ 60 | 'x': [1, 2] 61 | }), 62 | '{x:[1,2]}'); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/parse_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:json5/src/syntax_exception.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | var error = throwsA(TypeMatcher()); 6 | void main() { 7 | group('syntax error', () { 8 | var badJson = [ 9 | ['', error], 10 | [' ', error], 11 | ['x', error], 12 | ['1x', error], 13 | ['+', error], 14 | ['"', error], 15 | ['"a', error], 16 | ['//comment 1', error], 17 | ['//comment\n', error], 18 | ['/*1', error], 19 | ['/**1', error], 20 | ['[', error], 21 | ['[1', error], 22 | ['[1,', error], 23 | ['nul', error], 24 | ['[[]', error], 25 | ['{1:1}', error], 26 | ['{"x":1', error], 27 | ['{"x":}', error], 28 | ['{"x"1}', error], 29 | ['{"x",1}', error], 30 | ]; 31 | for (var item in badJson) { 32 | test(item[0], () { 33 | if (item[1] == error) { 34 | expect(() => json5Decode(item[0] as String), item[1]); 35 | } else { 36 | expect(json5Decode(item[0] as String), item[1]); 37 | } 38 | }); 39 | } 40 | }); 41 | 42 | group('separators', () { 43 | var seps = [ 44 | ['[]', []], 45 | [ 46 | '[1]', 47 | [1] 48 | ], 49 | [ 50 | '[1,]', 51 | [ 52 | 1, 53 | ] 54 | ], 55 | ['[,]', error], 56 | ['[,1]', error], 57 | ['[1,,]', error], 58 | ]; 59 | for (var item in seps) { 60 | if (item[1] == error) { 61 | test('${item[0]} throws', () { 62 | expect(() => json5Decode(item[0] as String), item[1]); 63 | }); 64 | } else { 65 | test(item[0], () { 66 | expect(json5Decode(item[0] as String), item[1]); 67 | }); 68 | } 69 | } 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/stringify_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('object', () { 6 | var x1 = [ 7 | {'x': 1}, 8 | const {'x': 1}, 9 | {'x': 1}, 10 | {'x': 1}, 11 | {'x': 1}, 12 | {'x': 1}, 13 | ]; 14 | 15 | for (var item in x1) { 16 | test('${item.runtimeType} $item', () { 17 | expect(json5Encode(item), '{x:1}'); 18 | }); 19 | } 20 | }); 21 | 22 | group('array', () { 23 | var items = [ 24 | [1, 2], 25 | const [1, 2], 26 | {1, 2}, 27 | [1, 2].map((e) => e), 28 | ]; 29 | 30 | for (var item in items) { 31 | test('${item.runtimeType} $item', () { 32 | expect(json5Encode(item), '[1,2]'); 33 | }); 34 | } 35 | }); 36 | 37 | group('primitives', () { 38 | var items = [ 39 | [null, 'null'], 40 | [true, 'true'], 41 | [false, 'false'], 42 | [0.0, '0.0'], 43 | [-0.0, '-0.0'], 44 | [double.infinity, 'Infinity'], 45 | [double.negativeInfinity, '-Infinity'], 46 | [double.nan, 'NaN'], 47 | [-1.525e-10, '-1.525e-10'], 48 | ['😁', "'😁'"], 49 | ['\b\f\r\n\t', "'\\b\\f\\r\\n\\t'"], 50 | ['\\', "'\\\\'"], 51 | ['"', "'\"'"], 52 | ["'", '"\'"'], 53 | ["'\"", "'\\'\"'"], 54 | ['\u0019', "'\\x19'"], 55 | ['\u0000', "'\\0'"], 56 | ]; 57 | 58 | for (var item in items) { 59 | test('${item[0].runtimeType} $item', () { 60 | expect(json5Encode(item[0]), item[1]); 61 | }); 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/to_encodable_default_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class ToMap { 5 | Map toMap() { 6 | return { 7 | 'x': 1, 8 | }; 9 | } 10 | } 11 | 12 | class ToJson { 13 | String toJson() => 'toJson'; 14 | } 15 | 16 | class ToString { 17 | @override 18 | String toString() { 19 | return 'toString'; 20 | } 21 | } 22 | 23 | void main() { 24 | test('to_map', () { 25 | expect(json5Encode(ToMap()), '{x:1}'); 26 | }); 27 | 28 | test('to_json', () { 29 | expect(json5Encode(ToJson()), "'toJson'"); 30 | }); 31 | 32 | test('to_string', () { 33 | expect(json5Encode(ToString()), "'toString'"); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/to_encodable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class Foo { 5 | @override 6 | String toString() { 7 | return 'foo'; 8 | } 9 | } 10 | 11 | void main() { 12 | test('toEncodable', () { 13 | expect( 14 | json5Encode(Foo(), toEncodable: (value) => value.toString()), 15 | "'foo'", 16 | ); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/unicode_escape_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json5/json5.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('unicode escape', () { 6 | var escape = [ 7 | ['\\0', '\u0000'], 8 | ['\\b', '\u0008'], 9 | ['\\t', '\u0009'], 10 | ['\\n', '\u000A'], 11 | ['\\v', '\u000B'], 12 | ['\\f', '\u000C'], 13 | ['\\r', '\u000D'], 14 | ['\\\\', '\\'], 15 | ['\\"', '"'], 16 | ["\\'", "'"], 17 | ['\\u0000', '\u0000'], 18 | ['\\u1234', '\u1234'], 19 | ['\\u5678', '\u5678'], 20 | ['\\u9abc', '\u9abc'], 21 | ['\\udefA', '\udefA'], 22 | ['\\uBCDE', '\uBCDE'], 23 | ['\\uF012', '\uF012'], 24 | ['\\x09', '\u0009'], 25 | ['\\x90', '\u0090'], 26 | ]; 27 | 28 | String quote(String src) { 29 | return '"$src"'; 30 | } 31 | 32 | for (var item in escape) { 33 | test('$item', () { 34 | expect(json5Decode(quote(item[0])), item[1]); 35 | }); 36 | } 37 | }); 38 | } 39 | --------------------------------------------------------------------------------