1 /* 2 * This file is part of gir-to-d. 3 * 4 * gir-to-d is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License 6 * as published by the Free Software Foundation, either version 3 7 * of the License, or (at your option) any later version. 8 * 9 * gir-to-d is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with gir-to-d. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 module gtd.GirWrapper; 19 20 import std.algorithm; 21 import std.array; 22 import std.file; 23 import std.uni; 24 import std.path; 25 import std.stdio; 26 import std.string; 27 28 import gtd.DefReader; 29 import gtd.GirField; 30 import gtd.GirFunction; 31 import gtd.GirPackage; 32 import gtd.GirStruct; 33 import gtd.GirType; 34 import gtd.GirVersion; 35 import gtd.GlibTypes; 36 import gtd.IndentedStringBuilder; 37 import gtd.Log; 38 39 enum PrintFileMethod 40 { 41 Absolute, 42 Relative, 43 Default 44 } 45 46 class GirWrapper 47 { 48 bool includeComments = true; 49 bool useRuntimeLinker; 50 bool useBindDir; 51 52 bool printFiles; 53 PrintFileMethod printFileMethod = PrintFileMethod.Default; 54 string cwdOrBaseDirectory; 55 56 string inputDir; 57 string outputDir; 58 string srcDir = "./"; 59 string commandlineGirPath; 60 61 static string licence; 62 static string[string] aliasses; 63 64 static GirPackage[string] packages; 65 66 public this(string inputDir, string outputDir) 67 { 68 this.inputDir = inputDir; 69 this.outputDir = outputDir; 70 } 71 72 void proccess(string lookupFileName) 73 { 74 if ( !exists(buildPath(inputDir, lookupFileName)) ) 75 error(lookupFileName, " not found, check '--help' for more information."); 76 77 DefReader defReader = new DefReader( buildPath(inputDir, lookupFileName) ); 78 79 proccess(defReader); 80 } 81 82 void proccess(DefReader defReader, GirPackage currentPackage = null, bool isDependency = false, GirStruct currentStruct = null) 83 { 84 while ( !defReader.empty ) 85 { 86 if ( !currentPackage && defReader.key.among( 87 "addAliases", "addConstants", "addEnums", "addFuncts", "addStructs", "file", "move", 88 "struct", "class", "interface", "namespace", "noAlias", "noConstant", "noEnum", "noCallback") ) 89 error("Found: '", defReader.key, "' before wrap.", defReader); 90 91 if ( !currentStruct && defReader.key.among( 92 "code", "cType", "extend", "implements", "import", "interfaceCode", "merge", 93 "noCode", "noExternal", "noProperty", "noSignal", "noStruct", "override", "structWrap", 94 "array", "in", "out", "inout", "ref") ) 95 error("Found: '", defReader.key, "' without an active struct.", defReader); 96 97 switch ( defReader.key ) 98 { 99 //Toplevel keys. 100 case "bindDir": 101 warning("Don't use bindDir, it is no longer used since the c definitions have moved.", defReader); 102 break; 103 case "includeComments": 104 includeComments = defReader.valueBool; 105 break; 106 case "inputRoot": 107 warning("Don't use inputRoot, it has been removed as it was never implemented.", defReader); 108 break; 109 case "license": 110 licence = defReader.readBlock().join(); 111 break; 112 case "outputRoot": 113 if ( outputDir == "./out" ) 114 outputDir = defReader.value; 115 break; 116 117 //Global keys. 118 case "alias": 119 if ( currentStruct ) 120 loadAA(currentStruct.aliases, defReader); 121 else 122 loadAA(aliasses, defReader); 123 break; 124 case "copy": 125 try 126 copyFiles(inputDir, buildPath(outputDir, srcDir), defReader.value); 127 catch(FileException ex) 128 error(ex.msg, defReader); 129 break; 130 case "dependency": 131 loadDependency(defReader); 132 break; 133 case "lookup": 134 DefReader reader = new DefReader( buildPath(inputDir, defReader.value) ); 135 136 proccess(reader, currentPackage, isDependency, currentStruct); 137 break; 138 case "srcDir": 139 srcDir = defReader.value; 140 break; 141 case "version": 142 if ( defReader.value == "end" ) 143 break; 144 145 if ( defReader.subKey.empty ) 146 error("No version specified.", defReader); 147 148 bool parseVersion = checkOsVersion(defReader.subKey); 149 150 if ( !parseVersion && defReader.subKey[0].isNumber() ) 151 { 152 if ( !currentPackage ) 153 error("Only use OS versions before wrap.", defReader); 154 parseVersion = defReader.subKey <= currentPackage._version; 155 } 156 157 if ( defReader.value == "start" ) 158 { 159 if ( parseVersion ) 160 break; 161 162 defReader.skipBlock(); 163 } 164 165 if ( !parseVersion ) 166 break; 167 168 size_t index = defReader.value.indexOf(':'); 169 defReader.key = defReader.value[0 .. max(index, 0)].strip(); 170 defReader.value = defReader.value[index +1 .. $].strip(); 171 172 if ( !defReader.key.empty ) 173 continue; 174 175 break; 176 case "wrap": 177 if ( isDependency ) 178 { 179 currentPackage.name = defReader.value; 180 break; 181 } 182 183 if ( outputDir.empty ) 184 error("Found wrap while outputRoot isn't set", defReader); 185 if (defReader.value in packages) 186 error("Package '", defReader.value, "' is already defined.", defReader); 187 188 currentStruct = null; 189 currentPackage = new GirPackage(defReader.value, this, srcDir); 190 packages[defReader.value] = currentPackage; 191 break; 192 193 //Package keys 194 case "addAliases": 195 currentPackage.lookupAliases ~= defReader.readBlock(); 196 break; 197 case "addConstants": 198 currentPackage.lookupConstants ~= defReader.readBlock(); 199 break; 200 case "addEnums": 201 currentPackage.lookupEnums ~= defReader.readBlock(); 202 break; 203 case "addFuncts": 204 currentPackage.lookupFuncts ~= defReader.readBlock(); 205 break; 206 case "addStructs": 207 currentPackage.lookupStructs ~= defReader.readBlock(); 208 break; 209 case "file": 210 if ( !isAbsolute(defReader.value) ) 211 { 212 currentPackage.parseGIR(getAbsoluteGirPath(defReader.value)); 213 } 214 else 215 { 216 warning("Don't use absolute paths for specifying gir files.", defReader); 217 218 currentPackage.parseGIR(defReader.value); 219 } 220 break; 221 case "move": 222 string[] vals = defReader.value.split(); 223 if ( vals.length <= 1 ) 224 error("No destination for move: ", defReader.value, defReader); 225 string newFuncName = ( vals.length == 3 ) ? vals[2] : vals[0]; 226 GirStruct dest = currentPackage.getStruct(vals[1]); 227 if ( dest is null ) 228 dest = createClass(currentPackage, vals[1]); 229 230 if ( currentStruct && vals[0] in currentStruct.functions ) 231 { 232 currentStruct.functions[vals[0]].strct = dest; 233 dest.functions[newFuncName] = currentStruct.functions[vals[0]]; 234 dest.functions[newFuncName].name = newFuncName; 235 if ( newFuncName.startsWith("new") ) 236 dest.functions[newFuncName].type = GirFunctionType.Constructor; 237 if ( currentStruct.virtualFunctions.canFind(vals[0]) ) 238 dest.virtualFunctions ~= newFuncName; 239 currentStruct.functions.remove(vals[0]); 240 } 241 else if ( vals[0] in currentPackage.collectedFunctions ) 242 { 243 currentPackage.collectedFunctions[vals[0]].strct = dest; 244 dest.functions[newFuncName] = currentPackage.collectedFunctions[vals[0]]; 245 dest.functions[newFuncName].name = newFuncName; 246 currentPackage.collectedFunctions.remove(vals[0]); 247 } 248 else 249 error("Unknown function ", vals[0], defReader); 250 break; 251 case "noAlias": 252 currentPackage.collectedAliases.remove(defReader.value); 253 break; 254 case "noConstant": 255 currentPackage.collectedConstants.remove(defReader.value); 256 break; 257 case "noEnum": 258 currentPackage.collectedEnums.remove(defReader.value); 259 break; 260 case "noCallback": 261 currentPackage.collectedCallbacks.remove(defReader.value); 262 break; 263 case "struct": 264 if ( defReader.value.empty ) 265 { 266 currentStruct = null; 267 } 268 else 269 { 270 currentStruct = currentPackage.getStruct(defReader.value); 271 if ( currentStruct is null ) 272 currentStruct = createClass(currentPackage, defReader.value); 273 } 274 break; 275 276 //Struct keys. 277 case "array": 278 string[] vals = defReader.value.split(); 279 280 if ( vals[0] in currentStruct.functions ) 281 { 282 GirFunction func = currentStruct.functions[vals[0]]; 283 284 if ( vals[1] == "Return" ) 285 { 286 if ( vals.length < 3 ) 287 { 288 func.returnType.zeroTerminated = true; 289 break; 290 } 291 292 GirType elementType = new GirType(this); 293 294 elementType.name = func.returnType.name; 295 elementType.cType = func.returnType.cType[0..$-1]; 296 func.returnType.elementType = elementType; 297 func.returnType.girArray = true; 298 299 foreach( i, p; func.params ) 300 { 301 if ( p.name == vals[2] ) 302 func.returnType.length = cast(int)i; 303 } 304 } 305 else 306 { 307 GirParam param = findParam(currentStruct, vals[0], vals[1]); 308 GirType elementType = new GirType(this); 309 310 elementType.name = param.type.name; 311 elementType.cType = param.type.cType[0..$-1]; 312 param.type.elementType = elementType; 313 param.type.girArray = true; 314 315 if ( vals.length < 3 ) 316 { 317 param.type.zeroTerminated = true; 318 break; 319 } 320 321 if ( vals[2] == "Return" ) 322 { 323 param.type.length = -2; 324 break; 325 } 326 327 foreach( i, p; func.params ) 328 { 329 if ( p.name == vals[2] ) 330 param.type.length = cast(int)i; 331 } 332 } 333 } 334 else if ( currentStruct.fields.map!(a => a.name).canFind(vals[0]) ) 335 { 336 GirField arrayField; 337 int lengthID = -1; 338 339 foreach ( size_t i, field; currentStruct.fields ) 340 { 341 if ( field.name == vals[0] ) 342 arrayField = field; 343 else if ( field.name == vals[1] ) 344 lengthID = cast(int)i; 345 346 if ( arrayField && lengthID > -1 ) 347 break; 348 } 349 350 arrayField.type.length = lengthID; 351 currentStruct.fields[lengthID].isLength = true; 352 353 GirType elementType = new GirType(this); 354 elementType.name = arrayField.type.name; 355 elementType.cType = arrayField.type.cType[0..$-1]; 356 arrayField.type.elementType = elementType; 357 arrayField.type.girArray = true; 358 } 359 else 360 { 361 error("Field or function: `", vals[0], "' is unknown.", defReader); 362 } 363 break; 364 case "class": 365 if ( currentStruct is null ) 366 currentStruct = createClass(currentPackage, defReader.value); 367 368 currentStruct.lookupClass = true; 369 currentStruct.name = defReader.value; 370 break; 371 case "code": 372 currentStruct.lookupCode ~= defReader.readBlock; 373 break; 374 case "cType": 375 currentStruct.cType = defReader.value; 376 break; 377 case "extend": 378 currentStruct.lookupParent = true; 379 currentStruct.parent = defReader.value; 380 break; 381 case "implements": 382 if ( defReader.value.empty ) 383 currentStruct.implements = null; 384 else 385 currentStruct.implements ~= defReader.value; 386 break; 387 case "import": 388 currentStruct.imports ~= defReader.value; 389 break; 390 case "interface": 391 if ( currentStruct is null ) 392 currentStruct = createClass(currentPackage, defReader.value); 393 394 currentStruct.lookupInterface = true; 395 currentStruct.name = defReader.value; 396 break; 397 case "interfaceCode": 398 currentStruct.lookupInterfaceCode ~= defReader.readBlock; 399 break; 400 case "merge": 401 GirStruct mergeStruct = currentPackage.getStruct(defReader.value); 402 currentStruct.merge(mergeStruct); 403 GirStruct copy = currentStruct.dup(); 404 copy.noCode = true; 405 copy.noExternal = true; 406 mergeStruct.pack.collectedStructs[defReader.value] = copy; 407 break; 408 case "namespace": 409 currentStruct.type = GirStructType.Record; 410 currentStruct.lookupClass = false; 411 currentStruct.lookupInterface = false; 412 413 if ( defReader.value.empty ) 414 { 415 currentStruct.noNamespace = true; 416 } 417 else 418 { 419 currentStruct.noNamespace = false; 420 currentStruct.name = defReader.value; 421 } 422 break; 423 case "noCode": 424 if ( defReader.valueBool ) 425 { 426 currentStruct.noCode = true; 427 break; 428 } 429 if ( defReader.value !in currentStruct.functions ) 430 error("Unknown function ", defReader.value, ". Possible values: ", currentStruct.functions.keys, defReader); 431 432 currentStruct.functions[defReader.value].noCode = true; 433 break; 434 case "noExternal": 435 currentStruct.noExternal = true; 436 break; 437 case "noProperty": 438 foreach ( field; currentStruct.fields ) 439 { 440 if ( field.name == defReader.value ) 441 { 442 field.noProperty = true; 443 break; 444 } 445 else if ( field == currentStruct.fields.back ) 446 error("Unknown field ", defReader.value, defReader); 447 } 448 break; 449 case "noSignal": 450 currentStruct.functions[defReader.value~"-signal"].noCode = true; 451 break; 452 case "noStruct": 453 currentStruct.noDeclaration = true; 454 break; 455 case "structWrap": 456 loadAA(currentStruct.structWrap, defReader); 457 break; 458 459 //Function keys 460 case "in": 461 string[] vals = defReader.value.split(); 462 if ( vals[0] !in currentStruct.functions ) 463 error("Unknown function ", vals[0], ". Possible values: ", currentStruct.functions, defReader); 464 findParam(currentStruct, vals[0], vals[1]).direction = GirParamDirection.Default; 465 break; 466 case "out": 467 string[] vals = defReader.value.split(); 468 if ( vals[0] !in currentStruct.functions ) 469 error("Unknown function ", vals[0], ". Possible values: ", currentStruct.functions, defReader); 470 findParam(currentStruct, vals[0], vals[1]).direction = GirParamDirection.Out; 471 break; 472 case "override": 473 currentStruct.functions[defReader.value].lookupOverride = true; 474 break; 475 case "inout": 476 case "ref": 477 string[] vals = defReader.value.split(); 478 if ( vals[0] !in currentStruct.functions ) 479 error("Unknown function ", vals[0], ". Possible values: ", currentStruct.functions, defReader); 480 findParam(currentStruct, vals[0], vals[1]).direction = GirParamDirection.InOut; 481 break; 482 483 default: 484 error("Unknown key: ", defReader.key, defReader); 485 } 486 487 defReader.popFront(); 488 } 489 } 490 491 void proccessGIR(string girFile) 492 { 493 GirPackage pack = new GirPackage("", this, srcDir); 494 495 if ( !isAbsolute(girFile) ) 496 { 497 girFile = getAbsoluteGirPath(girFile); 498 } 499 500 pack.parseGIR(girFile); 501 packages[pack.name] = pack; 502 } 503 504 void printFreeFunctions() 505 { 506 foreach ( pack; packages ) 507 { 508 foreach ( func; pack.collectedFunctions ) 509 { 510 if ( func.movedTo.empty ) 511 writefln("%s: %s", pack.name, func.name); 512 } 513 } 514 } 515 516 void writeFile(string fileName, string contents, bool createDirectory = false) 517 { 518 if ( createDirectory ) 519 { 520 try 521 { 522 if ( !exists(fileName.dirName()) ) 523 mkdirRecurse(fileName.dirName()); 524 } 525 catch (FileException ex) 526 { 527 error("Failed to create directory: ", ex.msg); 528 } 529 } 530 531 std.file.write(fileName, contents); 532 533 if ( printFiles ) 534 printFilePath(fileName); 535 } 536 537 string getAbsoluteGirPath(string girFile) 538 { 539 if ( commandlineGirPath ) 540 { 541 string cmdGirFile = buildNormalizedPath(commandlineGirPath, girFile); 542 543 if ( exists(cmdGirFile) ) 544 return cmdGirFile; 545 } 546 547 return buildNormalizedPath(getGirDirectory(), girFile); 548 } 549 550 private void printFilePath(string fileName) 551 { 552 with (PrintFileMethod) switch(printFileMethod) 553 { 554 case Absolute: 555 writeln(asAbsolutePath(fileName)); 556 break; 557 case Relative: 558 writeln(asRelativePath(asAbsolutePath(fileName), cwdOrBaseDirectory)); 559 break; 560 default: 561 writeln(fileName); 562 break; 563 } 564 } 565 566 private string getGirDirectory() 567 { 568 version(Windows) 569 { 570 import std.process : environment; 571 572 static string path; 573 574 if (path !is null) 575 return path; 576 577 foreach (p; splitter(environment.get("PATH"), ';')) 578 { 579 string dllPath = buildNormalizedPath(p, "libgtk-3-0.dll"); 580 581 if ( exists(dllPath) ) 582 path = p.buildNormalizedPath("../share/gir-1.0"); 583 } 584 585 return path; 586 } 587 else version(OSX) 588 { 589 import std.process : environment; 590 591 static string path; 592 593 if (path !is null) 594 return path; 595 596 path = environment.get("GTK_BASEPATH"); 597 if(path) 598 { 599 path = path.buildNormalizedPath("../share/gir-1.0"); 600 } 601 else 602 { 603 path = environment.get("HOMEBREW_ROOT"); 604 if(path) 605 { 606 path = path.buildNormalizedPath("share/gir-1.0"); 607 } 608 } 609 610 return path; 611 } 612 else 613 { 614 return "/usr/share/gir-1.0"; 615 } 616 } 617 618 private GirParam findParam(GirStruct strct, string func, string name) 619 { 620 foreach( param; strct.functions[func].params ) 621 { 622 if ( param.name == name ) 623 return param; 624 } 625 626 return null; 627 } 628 629 private void loadAA (ref string[string] aa, const DefReader defReader) 630 { 631 string[] vals = defReader.value.split(); 632 633 if ( vals.length == 1 ) 634 vals ~= ""; 635 636 if ( vals.length == 2 ) 637 aa[vals[0]] = vals[1]; 638 else 639 error("Worng amount of arguments for key: ", defReader.key, defReader); 640 } 641 642 private void loadDependency(DefReader defReader) 643 { 644 if ( defReader.value == "end" ) 645 return; 646 647 if ( defReader.subKey.empty ) 648 error("No dependency specified.", defReader); 649 650 GirInclude inc = GirPackage.includes.get(defReader.subKey, GirInclude.init); 651 652 if ( defReader.value == "skip" ) 653 inc.skip = true; 654 else if ( defReader.value == "start" ) 655 { 656 inc.lookupFile = defReader.fileName; 657 inc.lookupLine = defReader.lineNumber; 658 659 inc.lookupText = defReader.readBlock(); 660 } 661 else 662 error("Missing 'skip' or 'start' for dependency: ", defReader.subKey, defReader); 663 664 GirPackage.includes[defReader.subKey] = inc; 665 } 666 667 private void copyFiles(string srcDir, string destDir, string file) 668 { 669 string from = buildNormalizedPath(srcDir, file); 670 string to = buildNormalizedPath(destDir, file); 671 672 if ( !printFiles ) 673 writefln("copying file [%s] to [%s]", from, to); 674 675 if ( isFile(from) ) 676 { 677 if ( printFiles ) 678 writeln(to); 679 680 copy(from, to); 681 return; 682 } 683 684 void copyDir(string from, string to) 685 { 686 if ( !exists(to) ) 687 mkdirRecurse(to); 688 689 foreach ( entry; dirEntries(from, SpanMode.shallow) ) 690 { 691 string dst = buildPath(to, entry.name.baseName); 692 693 if ( isDir(entry.name) ) 694 { 695 copyDir(entry.name, dst); 696 } 697 else 698 { 699 if ( printFiles && !dst.endsWith("functions-runtime.d") && !dst.endsWith("functions-compiletime.d") ) 700 printFilePath(dst); 701 702 copy(entry.name, dst); 703 } 704 } 705 } 706 707 copyDir(from, to); 708 709 if ( file == "cairo" ) 710 { 711 if ( printFiles ) 712 printFilePath(buildNormalizedPath(to, "c", "functions.d")); 713 714 if ( useRuntimeLinker ) 715 copy(buildNormalizedPath(to, "c", "functions-runtime.d"), buildNormalizedPath(to, "c", "functions.d")); 716 else 717 copy(buildNormalizedPath(to, "c", "functions-compiletime.d"), buildNormalizedPath(to, "c", "functions.d")); 718 719 remove(buildNormalizedPath(to, "c", "functions-runtime.d")); 720 remove(buildNormalizedPath(to, "c", "functions-compiletime.d")); 721 } 722 } 723 724 private GirStruct createClass(GirPackage pack, string name) 725 { 726 GirStruct strct = new GirStruct(this, pack); 727 strct.name = name; 728 strct.cType = pack.cTypePrefix ~ name; 729 strct.type = GirStructType.Record; 730 strct.noDeclaration = true; 731 pack.collectedStructs["lookup"~name] = strct; 732 733 return strct; 734 } 735 736 private bool checkOsVersion(string _version) 737 { 738 if ( _version.empty || !(_version[0].isAlpha() || _version[0] == '!') ) 739 return false; 740 741 version(Windows) 742 { 743 return _version.among("Windows", "!OSX", "!linux", "!Linux", "!Posix") != 0; 744 } 745 else version(OSX) 746 { 747 return _version.among("!Windows", "OSX", "!linux", "!Linux", "Posix") != 0; 748 } 749 else version(linux) 750 { 751 return _version.among("!Windows", "!OSX", "linux", "Linux", "Posix") != 0; 752 } 753 else version(Posix) 754 { 755 return _version.among("!Windows", "!OSX", "!linux", "!Linux", "Posix") != 0; 756 } 757 else 758 { 759 return false; 760 } 761 } 762 763 } 764 765 /** 766 * Apply aliasses to the tokens in the string, and 767 * camelCase underscore separated tokens. 768 */ 769 string stringToGtkD(string str, string[string] aliases, bool caseConvert = true) 770 { 771 return stringToGtkD(str, aliases, null, caseConvert); 772 } 773 774 string stringToGtkD(string str, string[string] aliases, string[string] localAliases, bool caseConvert = true) 775 { 776 size_t pos, start; 777 string seps = " \n\r\t\f\v()[]*,;"; 778 auto converted = appender!string(); 779 780 while ( pos < str.length ) 781 { 782 if ( !seps.canFind(str[pos]) ) 783 { 784 start = pos; 785 786 while ( pos < str.length && !seps.canFind(str[pos]) ) 787 pos++; 788 789 //Workaround for the tm struct, type and variable have the same name. 790 if ( pos < str.length && str[pos] == '*' && str[start..pos] == "tm" ) 791 converted.put("void"); 792 else 793 converted.put(tokenToGtkD(str[start..pos], aliases, localAliases, caseConvert)); 794 795 if ( pos == str.length ) 796 break; 797 } 798 799 converted.put(str[pos]); 800 pos++; 801 } 802 803 return converted.data; 804 } 805 806 unittest 807 { 808 assert(stringToGtkD("token", ["token":"tok"]) == "tok"); 809 assert(stringToGtkD("string token_to_gtkD(string token, string[string] aliases)", ["token":"tok"]) 810 == "string tokenToGtkD(string tok, string[string] aliases)"); 811 } 812 813 string tokenToGtkD(string token, string[string] aliases, bool caseConvert=true) 814 { 815 return tokenToGtkD(token, aliases, null, caseConvert); 816 } 817 818 string tokenToGtkD(string token, string[string] aliases, string[string] localAliases, bool caseConvert=true) 819 { 820 if ( token in glibTypes ) 821 return glibTypes[token]; 822 else if ( token in localAliases ) 823 return localAliases[token]; 824 else if ( token in aliases ) 825 return aliases[token]; 826 else if ( token.endsWith("_t", "_t*", "_t**") ) 827 return token; 828 else if ( token == "pid_t" || token == "size_t" ) 829 return token; 830 else if ( caseConvert ) 831 return tokenToGtkD(removeUnderscore(token), aliases, localAliases, false); 832 else 833 return token; 834 } 835 836 string removeUnderscore(string token) 837 { 838 char pc; 839 auto converted = appender!string(); 840 841 while ( !token.empty ) 842 { 843 if ( token[0] == '_' ) 844 { 845 pc = token[0]; 846 token = token[1..$]; 847 848 continue; 849 } 850 851 if ( pc == '_' ) 852 converted.put(token[0].toUpper()); 853 else 854 converted.put(token[0]); 855 856 pc = token[0]; 857 token = token[1..$]; 858 } 859 860 return converted.data; 861 } 862 863 unittest 864 { 865 assert(removeUnderscore("this_is_a_test") == "thisIsATest"); 866 }