node-properties.js (19363B)
1 /** 2 * The MIT License (MIT) 3 * 4 * Copyright (c) 2014 Gabriel Llamas 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining a copy 7 * of this software and associated documentation files (the "Software"), to deal 8 * in the Software without restriction, including without limitation the rights 9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 * copies of the Software, and to permit persons to whom the Software is 11 * furnished to do so, subject to the following conditions: 12 * 13 * The above copyright notice and this permission notice shall be included in 14 * all copies or substantial portions of the Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 * THE SOFTWARE. 23 * 24 */ 25 26 "use strict"; 27 28 var hex = function (c){ 29 switch (c){ 30 case "0": return 0; 31 case "1": return 1; 32 case "2": return 2; 33 case "3": return 3; 34 case "4": return 4; 35 case "5": return 5; 36 case "6": return 6; 37 case "7": return 7; 38 case "8": return 8; 39 case "9": return 9; 40 case "a": case "A": return 10; 41 case "b": case "B": return 11; 42 case "c": case "C": return 12; 43 case "d": case "D": return 13; 44 case "e": case "E": return 14; 45 case "f": case "F": return 15; 46 } 47 }; 48 49 var parse = function (data, options, handlers, control){ 50 var c; 51 var code; 52 var escape; 53 var skipSpace = true; 54 var isCommentLine; 55 var isSectionLine; 56 var newLine = true; 57 var multiLine; 58 var isKey = true; 59 var key = ""; 60 var value = ""; 61 var section; 62 var unicode; 63 var unicodeRemaining; 64 var escapingUnicode; 65 var keySpace; 66 var sep; 67 var ignoreLine; 68 69 var line = function (){ 70 if (key || value || sep){ 71 handlers.line (key, value); 72 key = ""; 73 value = ""; 74 sep = false; 75 } 76 }; 77 78 var escapeString = function (key, c, code){ 79 if (escapingUnicode && unicodeRemaining){ 80 unicode = (unicode << 4) + hex (c); 81 if (--unicodeRemaining) return key; 82 escape = false; 83 escapingUnicode = false; 84 return key + String.fromCharCode (unicode); 85 } 86 87 //code 117: u 88 if (code === 117){ 89 unicode = 0; 90 escapingUnicode = true; 91 unicodeRemaining = 4; 92 return key; 93 } 94 95 escape = false; 96 97 //code 116: t 98 //code 114: r 99 //code 110: n 100 //code 102: f 101 if (code === 116) return key + "\t"; 102 else if (code === 114) return key + "\r"; 103 else if (code === 110) return key + "\n"; 104 else if (code === 102) return key + "\f"; 105 106 return key + c; 107 }; 108 109 var isComment; 110 var isSeparator; 111 112 if (options._strict){ 113 isComment = function (c, code, options){ 114 return options._comments[c]; 115 }; 116 117 isSeparator = function (c, code, options){ 118 return options._separators[c]; 119 }; 120 }else{ 121 isComment = function (c, code, options){ 122 //code 35: # 123 //code 33: ! 124 return code === 35 || code === 33 || options._comments[c]; 125 }; 126 127 isSeparator = function (c, code, options){ 128 //code 61: = 129 //code 58: : 130 return code === 61 || code === 58 || options._separators[c]; 131 }; 132 } 133 134 for (var i=~~control.resume; i<data.length; i++){ 135 if (control.abort) return; 136 if (control.pause){ 137 //The next index is always the start of a new line, it's a like a fresh 138 //start, there's no need to save the current state 139 control.resume = i; 140 return; 141 } 142 143 c = data[i]; 144 code = data.charCodeAt (i); 145 146 //code 13: \r 147 if (code === 13) continue; 148 149 if (isCommentLine){ 150 //code 10: \n 151 if (code === 10){ 152 isCommentLine = false; 153 newLine = true; 154 skipSpace = true; 155 } 156 continue; 157 } 158 159 //code 93: ] 160 if (isSectionLine && code === 93){ 161 handlers.section (section); 162 //Ignore chars after the section in the same line 163 ignoreLine = true; 164 continue; 165 } 166 167 if (skipSpace){ 168 //code 32: " " (space) 169 //code 9: \t 170 //code 12: \f 171 if (code === 32 || code === 9 || code === 12){ 172 continue; 173 } 174 //code 10: \n 175 if (!multiLine && code === 10){ 176 //Empty line or key w/ separator and w/o value 177 isKey = true; 178 keySpace = false; 179 newLine = true; 180 line (); 181 continue; 182 } 183 skipSpace = false; 184 multiLine = false; 185 } 186 187 if (newLine){ 188 newLine = false; 189 if (isComment (c, code, options)){ 190 isCommentLine = true; 191 continue; 192 } 193 //code 91: [ 194 if (options.sections && code === 91){ 195 section = ""; 196 isSectionLine = true; 197 control.skipSection = false; 198 continue; 199 } 200 } 201 202 //code 10: \n 203 if (code !== 10){ 204 if (control.skipSection || ignoreLine) continue; 205 206 if (!isSectionLine){ 207 if (!escape && isKey && isSeparator (c, code, options)){ 208 //sep is needed to detect empty key and empty value with a 209 //non-whitespace separator 210 sep = true; 211 isKey = false; 212 keySpace = false; 213 //Skip whitespace between separator and value 214 skipSpace = true; 215 continue; 216 } 217 } 218 219 //code 92: "\" (backslash) 220 if (code === 92){ 221 if (escape){ 222 if (escapingUnicode) continue; 223 224 if (keySpace){ 225 //Line with whitespace separator 226 keySpace = false; 227 isKey = false; 228 } 229 230 if (isSectionLine) section += "\\"; 231 else if (isKey) key += "\\"; 232 else value += "\\"; 233 } 234 escape = !escape; 235 }else{ 236 if (keySpace){ 237 //Line with whitespace separator 238 keySpace = false; 239 isKey = false; 240 } 241 242 if (isSectionLine){ 243 if (escape) section = escapeString (section, c, code); 244 else section += c; 245 }else if (isKey){ 246 if (escape){ 247 key = escapeString (key, c, code); 248 }else{ 249 //code 32: " " (space) 250 //code 9: \t 251 //code 12: \f 252 if (code === 32 || code === 9 || code === 12){ 253 keySpace = true; 254 //Skip whitespace between key and separator 255 skipSpace = true; 256 continue; 257 } 258 key += c; 259 } 260 }else{ 261 if (escape) value = escapeString (value, c, code); 262 else value += c; 263 } 264 } 265 }else{ 266 if (escape){ 267 if (!escapingUnicode){ 268 escape = false; 269 } 270 skipSpace = true; 271 multiLine = true; 272 }else{ 273 if (isSectionLine){ 274 isSectionLine = false; 275 if (!ignoreLine){ 276 //The section doesn't end with ], it's a key 277 control.error = new Error ("The section line \"" + section + 278 "\" must end with \"]\""); 279 return; 280 } 281 ignoreLine = false; 282 } 283 newLine = true; 284 skipSpace = true; 285 isKey = true; 286 287 line (); 288 } 289 } 290 } 291 292 control.parsed = true; 293 294 if (isSectionLine && !ignoreLine){ 295 //The section doesn't end with ], it's a key 296 control.error = new Error ("The section line \"" + section + "\" must end" + 297 "with \"]\""); 298 return; 299 } 300 line (); 301 }; 302 303 var INCLUDE_KEY = "include"; 304 var INDEX_FILE = "index.properties"; 305 306 var cast = function (value){ 307 if (value === null || value === "null") return null; 308 if (value === "undefined") return undefined; 309 if (value === "true") return true; 310 if (value === "false") return false; 311 var v = Number (value); 312 return isNaN (v) ? value : v; 313 }; 314 315 var expand = function (o, str, options, cb){ 316 if (!options.variables || !str) return cb (null, str); 317 318 var stack = []; 319 var c; 320 var cp; 321 var key = ""; 322 var section = null; 323 var v; 324 var holder; 325 var t; 326 var n; 327 328 for (var i=0; i<str.length; i++){ 329 c = str[i]; 330 331 if (cp === "$" && c === "{"){ 332 key = key.substring (0, key.length - 1); 333 stack.push ({ 334 key: key, 335 section: section 336 }); 337 key = ""; 338 section = null; 339 continue; 340 }else if (stack.length){ 341 if (options.sections && c === "|"){ 342 section = key; 343 key = ""; 344 continue; 345 }else if (c === "}"){ 346 holder = section !== null ? searchValue (o, section, true) : o; 347 if (!holder){ 348 return cb (new Error ("The section \"" + section + "\" does not " + 349 "exist")); 350 } 351 352 v = options.namespaces ? searchValue (holder, key) : holder[key]; 353 if (v === undefined){ 354 //Read the external vars 355 v = options.namespaces 356 ? searchValue (options._vars, key) 357 : options._vars[key] 358 359 if (v === undefined){ 360 return cb (new Error ("The property \"" + key + "\" does not " + 361 "exist")); 362 } 363 } 364 365 t = stack.pop (); 366 section = t.section; 367 key = t.key + (v === null ? "" : v); 368 continue; 369 } 370 } 371 372 cp = c; 373 key += c; 374 } 375 376 if (stack.length !== 0){ 377 return cb (new Error ("Malformed variable: " + str)); 378 } 379 380 cb (null, key); 381 }; 382 383 var searchValue = function (o, chain, section){ 384 var n = chain.split ("."); 385 var str; 386 387 for (var i=0; i<n.length-1; i++){ 388 str = n[i]; 389 if (o[str] === undefined) return; 390 o = o[str]; 391 } 392 393 var v = o[n[n.length - 1]]; 394 if (section){ 395 if (typeof v !== "object") return; 396 return v; 397 }else{ 398 if (typeof v === "object") return; 399 return v; 400 } 401 }; 402 403 var namespaceKey = function (o, key, value){ 404 var n = key.split ("."); 405 var str; 406 407 for (var i=0; i<n.length-1; i++){ 408 str = n[i]; 409 if (o[str] === undefined){ 410 o[str] = {}; 411 }else if (typeof o[str] !== "object"){ 412 throw new Error ("Invalid namespace chain in the property name '" + 413 key + "' ('" + str + "' has already a value)"); 414 } 415 o = o[str]; 416 } 417 418 o[n[n.length - 1]] = value; 419 }; 420 421 var namespaceSection = function (o, section){ 422 var n = section.split ("."); 423 var str; 424 425 for (var i=0; i<n.length; i++){ 426 str = n[i]; 427 if (o[str] === undefined){ 428 o[str] = {}; 429 }else if (typeof o[str] !== "object"){ 430 throw new Error ("Invalid namespace chain in the section name '" + 431 section + "' ('" + str + "' has already a value)"); 432 } 433 o = o[str]; 434 } 435 436 return o; 437 }; 438 439 var merge = function (o1, o2){ 440 for (var p in o2){ 441 try{ 442 if (o1[p].constructor === Object){ 443 o1[p] = merge (o1[p], o2[p]); 444 }else{ 445 o1[p] = o2[p]; 446 } 447 }catch (e){ 448 o1[p] = o2[p]; 449 } 450 } 451 return o1; 452 } 453 454 var build = function (data, options, dirname, cb){ 455 var o = {}; 456 457 if (options.namespaces){ 458 var n = {}; 459 } 460 461 var control = { 462 abort: false, 463 skipSection: false 464 }; 465 466 if (options.include){ 467 var remainingIncluded = 0; 468 469 var include = function (value){ 470 if (currentSection !== null){ 471 return abort (new Error ("Cannot include files from inside a " + 472 "section: " + currentSection)); 473 } 474 475 var p = path.resolve (dirname, value); 476 if (options._included[p]) return; 477 478 options._included[p] = true; 479 remainingIncluded++; 480 control.pause = true; 481 482 read (p, options, function (error, included){ 483 if (error) return abort (error); 484 485 remainingIncluded--; 486 merge (options.namespaces ? n : o, included); 487 control.pause = false; 488 489 if (!control.parsed){ 490 parse (data, options, handlers, control); 491 if (control.error) return abort (control.error); 492 } 493 494 if (!remainingIncluded) cb (null, options.namespaces ? n : o); 495 }); 496 }; 497 } 498 499 if (!data){ 500 if (cb) return cb (null, o); 501 return o; 502 } 503 504 var currentSection = null; 505 var currentSectionStr = null; 506 507 var abort = function (error){ 508 control.abort = true; 509 if (cb) return cb (error); 510 throw error; 511 }; 512 513 var handlers = {}; 514 var reviver = { 515 assert: function (){ 516 return this.isProperty ? reviverLine.value : true; 517 } 518 }; 519 var reviverLine = {}; 520 521 //Line handler 522 //For speed reasons, if "namespaces" is enabled, the old object is still 523 //populated, e.g.: ${a.b} reads the "a.b" property from { "a.b": 1 }, instead 524 //of having a unique object { a: { b: 1 } } which is slower to search for 525 //the "a.b" value 526 //If "a.b" is not found, then the external vars are read. If "namespaces" is 527 //enabled, the var "a.b" is split and it searches the a.b value. If it is not 528 //enabled, then the var "a.b" searches the "a.b" value 529 530 var line; 531 var error; 532 533 if (options.reviver){ 534 if (options.sections){ 535 line = function (key, value){ 536 if (options.include && key === INCLUDE_KEY) return include (value); 537 538 reviverLine.value = value; 539 reviver.isProperty = true; 540 reviver.isSection = false; 541 542 value = options.reviver.call (reviver, key, value, currentSectionStr); 543 if (value !== undefined){ 544 if (options.namespaces){ 545 try{ 546 namespaceKey (currentSection === null ? n : currentSection, 547 key, value); 548 }catch (error){ 549 abort (error); 550 } 551 }else{ 552 if (currentSection === null) o[key] = value; 553 else currentSection[key] = value; 554 } 555 } 556 }; 557 }else{ 558 line = function (key, value){ 559 if (options.include && key === INCLUDE_KEY) return include (value); 560 561 reviverLine.value = value; 562 reviver.isProperty = true; 563 reviver.isSection = false; 564 565 value = options.reviver.call (reviver, key, value); 566 if (value !== undefined){ 567 if (options.namespaces){ 568 try{ 569 namespaceKey (n, key, value); 570 }catch (error){ 571 abort (error); 572 } 573 }else{ 574 o[key] = value; 575 } 576 } 577 }; 578 } 579 }else{ 580 if (options.sections){ 581 line = function (key, value){ 582 if (options.include && key === INCLUDE_KEY) return include (value); 583 584 if (options.namespaces){ 585 try{ 586 namespaceKey (currentSection === null ? n : currentSection, key, 587 value); 588 }catch (error){ 589 abort (error); 590 } 591 }else{ 592 if (currentSection === null) o[key] = value; 593 else currentSection[key] = value; 594 } 595 }; 596 }else{ 597 line = function (key, value){ 598 if (options.include && key === INCLUDE_KEY) return include (value); 599 600 if (options.namespaces){ 601 try{ 602 namespaceKey (n, key, value); 603 }catch (error){ 604 abort (error); 605 } 606 }else{ 607 o[key] = value; 608 } 609 }; 610 } 611 } 612 613 //Section handler 614 var section; 615 if (options.sections){ 616 if (options.reviver){ 617 section = function (section){ 618 currentSectionStr = section; 619 reviverLine.section = section; 620 reviver.isProperty = false; 621 reviver.isSection = true; 622 623 var add = options.reviver.call (reviver, null, null, section); 624 if (add){ 625 if (options.namespaces){ 626 try{ 627 currentSection = namespaceSection (n, section); 628 }catch (error){ 629 abort (error); 630 } 631 }else{ 632 currentSection = o[section] = {}; 633 } 634 }else{ 635 control.skipSection = true; 636 } 637 }; 638 }else{ 639 section = function (section){ 640 currentSectionStr = section; 641 if (options.namespaces){ 642 try{ 643 currentSection = namespaceSection (n, section); 644 }catch (error){ 645 abort (error); 646 } 647 }else{ 648 currentSection = o[section] = {}; 649 } 650 }; 651 } 652 } 653 654 //Variables 655 if (options.variables){ 656 handlers.line = function (key, value){ 657 expand (options.namespaces ? n : o, key, options, function (error, key){ 658 if (error) return abort (error); 659 660 expand (options.namespaces ? n : o, value, options, 661 function (error, value){ 662 if (error) return abort (error); 663 664 line (key, cast (value || null)); 665 }); 666 }); 667 }; 668 669 if (options.sections){ 670 handlers.section = function (s){ 671 expand (options.namespaces ? n : o, s, options, function (error, s){ 672 if (error) return abort (error); 673 674 section (s); 675 }); 676 }; 677 } 678 }else{ 679 handlers.line = function (key, value){ 680 line (key, cast (value || null)); 681 }; 682 683 if (options.sections){ 684 handlers.section = section; 685 } 686 } 687 688 parse (data, options, handlers, control); 689 if (control.error) return abort (control.error); 690 691 if (control.abort || control.pause) return; 692 693 if (cb) return cb (null, options.namespaces ? n : o); 694 return options.namespaces ? n : o; 695 }; 696 697 var read = function (f, options, cb){ 698 fs.stat (f, function (error, stats){ 699 if (error) return cb (error); 700 701 var dirname; 702 703 if (stats.isDirectory ()){ 704 dirname = f; 705 f = path.join (f, INDEX_FILE); 706 }else{ 707 dirname = path.dirname (f); 708 } 709 710 fs.readFile (f, { encoding: "utf8" }, function (error, data){ 711 if (error) return cb (error); 712 build (data, options, dirname, cb); 713 }); 714 }); 715 }; 716 717 module.exports = function (data, options, cb){ 718 if (typeof options === "function"){ 719 cb = options; 720 options = {}; 721 } 722 723 options = options || {}; 724 var code; 725 726 if (options.include){ 727 if (!cb) throw new Error ("A callback must be passed if the 'include' " + 728 "option is enabled"); 729 options._included = {}; 730 } 731 732 options = options || {}; 733 options._strict = options.strict && (options.comments || options.separators); 734 options._vars = options.vars || {}; 735 736 var comments = options.comments || []; 737 if (!Array.isArray (comments)) comments = [comments]; 738 var c = {}; 739 comments.forEach (function (comment){ 740 code = comment.charCodeAt (0); 741 if (comment.length > 1 || code < 33 || code > 126){ 742 throw new Error ("The comment token must be a single printable ASCII " + 743 "character"); 744 } 745 c[comment] = true; 746 }); 747 options._comments = c; 748 749 var separators = options.separators || []; 750 if (!Array.isArray (separators)) separators = [separators]; 751 var s = {}; 752 separators.forEach (function (separator){ 753 code = separator.charCodeAt (0); 754 if (separator.length > 1 || code < 33 || code > 126){ 755 throw new Error ("The separator token must be a single printable ASCII " + 756 "character"); 757 } 758 s[separator] = true; 759 }); 760 options._separators = s; 761 762 if (options.path){ 763 if (!cb) throw new Error ("A callback must be passed if the 'path' " + 764 "option is enabled"); 765 if (options.include){ 766 read (data, options, cb); 767 }else{ 768 fs.readFile (data, { encoding: "utf8" }, function (error, data){ 769 if (error) return cb (error); 770 build (data, options, ".", cb); 771 }); 772 } 773 }else{ 774 return build (data, options, ".", cb); 775 } 776 };