1 /** 2 $(P An SDL ($(LINK2 http://www.ikayzo.org/display/SDL/Home,Simple Declarative Language)) parser. 3 Supports StAX/SAX style API. See $(D more.std.dom) for DOM style API.) 4 5 Examples: 6 -------------------------------- 7 void printTags(char[] sdl) { 8 Tag tag; 9 while(parseSdlTag(&tag, &sdl)) { 10 writefln("(depth %s) tag '%s' values '%s' attributes '%s'", 11 tag.depth, tag.name, tag.values.data, tag.attributes.data); 12 } 13 } 14 15 struct Person { 16 string name; 17 ushort age; 18 string[] nicknames; 19 auto children = appender!(Person[])(); 20 void reset() { 21 name = null; 22 age = 0; 23 nicknames = null; 24 children.clear(); 25 } 26 void parseFromSdl(ref SdlWalker walker) { 27 tag.enforceNoValues(); 28 tag.enforceNoAttributes(); 29 reset(); 30 foreach(auto personWalker = walker.children(); 31 !personWalker.empty; personWalker.popFront) { 32 33 if(tag.name == "name") { 34 35 tag.enforceNoAttributes(); 36 tag.enforceNoChildren(); 37 tag.getOneValue(name); 38 39 } else if(tag.name == "age") { 40 41 tag.enforceNoAttributes(); 42 tag.enforceNoChildren(); 43 tag.getOneValue(age); 44 45 } else if(tag.name == "nicknames") { 46 47 tag.enforceNoAttributes(); 48 tag.enforceNoChildren(); 49 tag.getValues(nicknames); 50 51 } else if(tag.name == "child") { 52 53 54 55 // todo implement 56 57 } else tag.throwIsUnknown(); 58 } 59 } 60 void validate() { 61 if(name == null) throw new Exception("person is missing the 'name' tag"); 62 if(age == 0) throw new Exception("person is missing the 'age' tag"); 63 } 64 } 65 66 void parseTags(char[] sdl) { 67 struct Person { 68 string name; 69 ushort age; 70 string[] nicknames; 71 Person[] children; 72 void reset() { 73 name = null; 74 age = 0; 75 nicknames = null; 76 children = null; 77 } 78 void validate() { 79 if(name == null) throw new Exception("person is missing the 'name' tag"); 80 if(age == 0) throw new Exception("person is missing the 'age' tag"); 81 } 82 } 83 auto people = appender(Person[])(); 84 Person person; 85 86 Tag tag; 87 for(auto walker = SdlWalker(&tag, sdl); !walker.empty; walker.popFront) { 88 if(tag.name == "person") { 89 90 tag.enforceNoValues(); 91 tag.enforceNoAttributes(); 92 person.reset(); 93 person.validate(); 94 people.put(person); 95 96 } else tag.throwIsUnknown(); 97 } 98 } 99 100 -------------------------------- 101 TODO: implement escaped strings 102 TODO: finish unit tests 103 TODO: write a input-range sdl parser 104 TODO: implement datetime/timespans 105 106 Authors: Jonathan Marler, johnnymarler@gmail.com 107 License: use freely for any purpose 108 */ 109 110 module more.sdl; 111 112 import std.array; 113 import std.string; 114 import std.range; 115 import std.conv; 116 import std.bitmanip; 117 import std.traits; 118 119 import std.c.string: memmove; 120 121 import more.common; 122 import more.utf8; 123 124 version(unittest_sdl) 125 { 126 import std.stdio; 127 } 128 129 130 /// Used in SdlParseException to distinguish specific sdl parse errors. 131 enum SdlErrorType { 132 unknown, 133 braceAfterNewline, 134 mixedValuesAndAttributes, 135 } 136 /// Thrown by sdl parse functions when invalid SDL is encountered. 137 class SdlParseException : Exception 138 { 139 SdlErrorType type; 140 uint lineInSdl; 141 this(uint lineInSdl, string msg, string file = __FILE__, size_t codeLine = __LINE__) { 142 this(SdlErrorType.unknown, lineInSdl, msg, file, codeLine); 143 } 144 this(SdlErrorType errorType, uint lineInSdl, string msg, string file = __FILE__, size_t codeLine = __LINE__) { 145 super((lineInSdl == 0) ? msg : "line "~to!string(lineInSdl)~": "~msg, file, codeLine); 146 this.type = errorType; 147 this.lineInSdl = lineInSdl; 148 } 149 } 150 151 /// Holds three character arrays for an SDL attribute, namespace/id/value. 152 struct Attribute { 153 //size_t line, column; 154 const(char)[] namespace; 155 const(char)[] id; 156 const(char)[] value; 157 } 158 159 /// Contains a tag's name, values and attributes. 160 /// It does not contain any information about its child tags because that part of the sdl would not have been parsed yet, however, 161 /// it does indicate if the tag was followed by an open brace. 162 /// This struct is used directly for the StAX/SAX APIs and indirectly for the DOM or Reflection APIs. 163 struct Tag { 164 165 // A bifield of flags used to pass extra options to parseSdlTag. 166 // Used to accept/reject different types of SDL or cause parseSdlTag to 167 // behave differently like preventing it from modifying the sdl text. 168 private ubyte flags; 169 170 /// Normally SDL only allows a tag's attributes to appear after all it's values. 171 /// This flag causes parseSdlTag to allow values/attributes to appear in any order, i.e. 172 /// $(D tag attr="my-value" "another-value" # would be valid) 173 @property @safe bool allowMixedValuesAndAttributes() pure nothrow const { return (flags & 1U) != 0;} 174 @property @safe void allowMixedValuesAndAttributes(bool v) pure nothrow { if (v) flags |= 1U;else flags &= ~1U;} 175 176 /// Causes parseSdlTag to allow a tag's open brace to appear after any number of newlines 177 @property @safe bool allowBraceAfterNewline() pure nothrow const { return (flags & 2U) != 0;} 178 @property @safe void allowBraceAfterNewline(bool v) pure nothrow { if (v) flags |= 2U;else flags &= ~2U;} 179 180 /// Causes parseSdlTag to allow a child tags to appear on the same line as the parents 181 /// open and close braces 182 @property @safe bool allowChildTagsOnSameLineAsBrace() pure nothrow const { return (flags & 4U) != 0;} 183 @property @safe void allowChildTagsOnSameLineAsBrace(bool v) pure nothrow { if (v) flags |= 4U;else flags &= ~4U;} 184 185 // Causes parseSdlTag to throw an exception if it finds any number literals 186 // with postfix letters indicating the type 187 // @property @safe bool verifyTypedNumbers() pure nothrow const { return (flags & 4U) != 0;} 188 // @property @safe void verifyTypedNumbers(bool v) pure nothrow { if (v) flags |= 4U;else flags &= ~4U;} 189 190 /// Causes parseSdlTag to set the tag name to null instead of "content" for anonymous tags. 191 /// This allows the application to differentiate betweeen "content" tags and anonymous tags. 192 @property @safe bool anonymousTagNameIsNull() pure nothrow const { return (flags & 8U) != 0;} 193 @property @safe void anonymousTagNameIsNull(bool v) pure nothrow { if (v) flags |= 8U;else flags &= ~8U;} 194 195 /// Causes parseSdlTag to accept non-quoted strings 196 @property @safe bool acceptUnquotedStrings() pure nothrow const { return (flags & 16U) != 0;} 197 @property @safe void acceptUnquotedStrings(bool v) pure nothrow { if (v) flags |= 16U;else flags &= ~16U;} 198 199 /// Prevents parseSdlTag from modifying the given sdl text for things such as 200 /// processing escaped strings 201 @property @safe bool preserveSdlText() pure nothrow const { return (flags & 128U) != 0;} 202 @property @safe void preserveSdlText(bool v) pure nothrow { if (v) flags |= 128U;else flags &= ~128U;} 203 204 205 // TODO: maybe add an option to specify that any values accessed should be copied to new buffers 206 // NOTE: Do not add an option to prevent parseSdlTag from throwing exceptions when the input has ended. 207 // It may have been useful for an input buffered object, however, the buffered input object will 208 // need to know when it has a full tag anyway so the sdl will already contain the characters to end the tag. 209 // Or in the case of braces on the next line, if the tag has alot of whitespace until the actual end-of-tag 210 // delimiter, the buffered input reader can insert a semi-colon or open_brace to signify the end of the tag 211 // earlier. 212 213 214 /// For now an alias for useStrictSdl. Use this function if you want your code to always use 215 /// the default mode whatever it may become. 216 alias useStrictSdl useDefaultSdl; 217 218 /// This is the default mode. 219 /// $(OL 220 /// $(LI Causes parseSdlTag to throw SdlParseException if a tag's open brace appears after a newline) 221 /// $(LI Causes parseSdlTag to throw SdlParseException if any tag value appears after any tag attribute) 222 /// $(LI Causes parseSdlTag to set anonymous tag names to "content") 223 /// ) 224 void useStrictSdl() { 225 this.allowMixedValuesAndAttributes = false; 226 this.allowBraceAfterNewline = false; 227 this.allowChildTagsOnSameLineAsBrace = false; 228 this.anonymousTagNameIsNull = false; 229 this.acceptUnquotedStrings = false; 230 } 231 /// $(OL 232 /// $(LI Causes parseSdlTag to throw SdlParseException if a tag's open brace appears after a newline) 233 /// $(LI Causes parseSdlTag to throw SdlParseException if any tag value appears after any tag attribute) 234 /// $(LI Causes parseSdlTag to set anonymous tag names to "content") 235 /// ) 236 void useLooseSdl() { 237 this.allowMixedValuesAndAttributes = true; 238 this.allowBraceAfterNewline = true; 239 this.allowChildTagsOnSameLineAsBrace = true; 240 this.anonymousTagNameIsNull = false; 241 this.acceptUnquotedStrings = true; 242 } 243 /// $(OL 244 /// $(LI Causes parseSdlTag to allow a tag's open brace appears after any number of newlines) 245 /// $(LI Causes parseSdlTag to allow tag values an attributes to mixed in any order) 246 /// $(LI Causes parseSdlTag to set anonymous tag names to null) 247 /// ) 248 void useProposedSdl() { 249 this.allowMixedValuesAndAttributes = true; 250 this.allowBraceAfterNewline = true; 251 this.allowChildTagsOnSameLineAsBrace = true; 252 this.anonymousTagNameIsNull = true; 253 this.acceptUnquotedStrings = true; 254 } 255 256 /// The depth of the tag, all root tags are at depth 0. 257 size_t depth = 0; 258 259 /// The line number of the SDL parser after parsing this tag. 260 size_t line; 261 size_t column; 262 263 /// The namespace of the tag 264 const(char)[] namespace; 265 /// The name of the tag 266 const(char)[] name; 267 /// The values of the tag 268 auto values = appender!(const(char)[][])(); 269 /// The attributes of the tag 270 auto attributes = appender!(Attribute[])(); 271 /// Indicates the tag has an open brace 272 bool hasOpenBrace; 273 274 version(unittest_sdl) 275 { 276 // This function is only so unit tests can create Tags to compare 277 // with tags parsed from the parseSdlTag function. This constructor 278 // should never be called in production code 279 this(const(char)[] name, const(char)[][] values...) { 280 auto colonIndex = name.indexOf(':'); 281 if(colonIndex > -1) { 282 this.namespace = name[0..colonIndex]; 283 this.name = name[colonIndex+1..$]; 284 } else { 285 this.namespace.length = 0; 286 this.name = name; 287 } 288 foreach(value; values) { 289 290 const(char)[] attributeNamespace = ""; 291 size_t equalIndex = size_t.max; 292 293 // check if it is an attribute 294 if(value.length && isIDStart(value[0])) { 295 size_t i = 1; 296 while(true) { 297 if(i >= value.length) break; 298 auto c = value[i]; 299 if(!isID(value[i])) { 300 if(c == ':') { 301 if(attributeNamespace.length) throw new Exception("contained 2 colons?"); 302 attributeNamespace = value[0..i]; 303 i++; 304 continue; 305 } 306 if(value[i] == '=') { 307 equalIndex = i; 308 } 309 break; 310 } 311 i++; 312 } 313 } 314 315 if(equalIndex == size_t.max) { 316 this.values.put(value); 317 } else { 318 Attribute a = {attributeNamespace, value[attributeNamespace.length..equalIndex], value[equalIndex+1..$]}; 319 this.attributes.put(a); 320 } 321 322 } 323 } 324 } 325 326 /// Gets the tag ready to parse a new sdl tree by resetting the depth and the line number. 327 /// It is unnecessary to call this before parsing the first sdl tree but would not be harmful. 328 /// It does not reset the namespace/name/values/attributes because those will 329 /// be reset by the parser on the next call to parseSdlTag when it calls $(D resetForNextTag()). 330 void resetForNewSdl() { 331 depth = 0; 332 line = 1; 333 } 334 335 /// Resets the tag state to get ready to parse the next tag. 336 /// Should only be called by the parseSdlTag function. 337 /// This will clear the namespace/name/values/attributes and increment the depth if the current tag 338 /// had an open brace. 339 void resetForNextTag() 340 { 341 this.namespace.length = 0; 342 this.name = null; 343 if(hasOpenBrace) { 344 hasOpenBrace = false; 345 this.depth++; 346 } 347 this.values.clear(); 348 this.attributes.clear(); 349 } 350 351 /// Returns true if the tag is anonymous. 352 bool isAnonymous() 353 { 354 return anonymousTagNameIsNull ? this.name is null : this.name == "content"; 355 } 356 /// Sets the tag as anonymous 357 void setIsAnonymous() 358 { 359 this.name = anonymousTagNameIsNull ? null : "content"; 360 } 361 /// Returns: true if the tag namespaces/names/values/attributes are 362 /// the same even if the depth/line/options are different. 363 bool opEquals(ref Tag other) { 364 return 365 namespace == other.namespace && 366 name == other.name && 367 values.data == other.values.data && 368 attributes.data == other.attributes.data; 369 } 370 371 /// Returns: A string of the Tag not including it's children. The string will be valid SDL 372 /// by itself but will not include the open brace if it has one. Use toSdl for that. 373 string toString() { 374 string str = ""; 375 if(namespace.length) { 376 str ~= namespace; 377 str ~= name; 378 } 379 if(!isAnonymous || (values.data.length == 0 && attributes.data.length == 0)) { 380 str ~= name; 381 } 382 foreach(value; values.data) { 383 str ~= ' '; 384 str ~= value; 385 } 386 foreach(attribute; attributes.data) { 387 str ~= ' '; 388 if(attribute.namespace.length) { 389 str ~= attribute.namespace; 390 str ~= ':'; 391 } 392 str ~= attribute.id; 393 str ~= '='; 394 str ~= attribute.value; 395 } 396 return str; 397 } 398 399 /// Writes the tag as standard SDL to sink. 400 /// It will write the open brace '{' but since the tag does not have a knowledge 401 /// about it's children, its up to the caller to write the close brace '}' after it 402 /// writes the children to the sink. 403 void toSdl(S, string indent = " ")(S sink) if(isOutputRange!(S,const(char)[])) { 404 //writefln("[DEBUG] converting to sdl namespace=%s name=%s values=%s attr=%s", 405 //namespace, name, values.data, attributes.data); 406 for(auto i = 0; i < depth; i++) { 407 sink.put(indent); 408 } 409 if(namespace.length) { 410 sink.put(namespace); 411 sink.put(":"); 412 } 413 if(!isAnonymous || (values.data.length == 0 && attributes.data.length == 0)) { 414 if(namespace.length == 0 && isSdlKeyword(name)) { 415 sink.put(":"); // Escape tag names that are keywords 416 } 417 sink.put(name); 418 } 419 foreach(value; values.data) { 420 sink.put(" "); 421 sink.put(value); 422 } 423 foreach(attribute; attributes.data) { 424 sink.put(" "); 425 if(attribute.namespace.length) { 426 sink.put(attribute.namespace); 427 sink.put(":"); 428 } 429 sink.put(attribute.id); 430 sink.put("="); 431 sink.put(attribute.value); 432 } 433 if(hasOpenBrace) { 434 sink.put(" {\n"); 435 } else { 436 sink.put("\n"); 437 } 438 } 439 440 441 442 443 // 444 // User Methods 445 // 446 void throwIsUnknown() { 447 throw new SdlParseException(line, format("unknown tag '%s'", name)); 448 } 449 void throwIsDuplicate() { 450 throw new SdlParseException(line, format("tag '%s' appeared more than once", name)); 451 } 452 void getOneValue(T)(ref T value) { 453 if(values.data.length != 1) { 454 throw new SdlParseException 455 (line,format("tag '%s' %s 1 value but had %s", 456 name, (values.data.length == 0) ? "must have at least" : "can only have", values.data.length)); 457 } 458 459 const(char)[] literal = values.data[0]; 460 461 462 static if( isSomeString!T ) { 463 464 if(!value.empty) throwIsDuplicate(); 465 466 } else static if( isIntegral!T || isFloatingPoint!T ) { 467 468 //if( value != 0 ) throwIsDuplicate(); 469 470 } else { 471 472 } 473 474 if(!sdlLiteralToD!(T)(literal, value)) throw new SdlParseException(line, format("cannot convert '%s' to %s", literal, typeid(T))); 475 } 476 477 void getValues(T, bool allowAppend=false)(ref T[] t, size_t minCount = 1) { 478 if(values.data.length < minCount) throw new SdlParseException(line, format("tag '%s' must have at least %s value(s)", name, minCount)); 479 480 size_t arrayOffset; 481 if(t.ptr is null) { 482 arrayOffset = 0; 483 t = new T[values.data.length]; 484 } else if(allowAppend) { 485 arrayOffset = t.length; 486 t.length += values.data.length; 487 } else throwIsDuplicate(); 488 489 foreach(literal; values.data) { 490 static if( isSomeString!T ) { 491 if(literal[0] != '"') throw new SdlParseException(line, format("tag '%s' must have exactly one string literal but had another literal type", name)); 492 t[arrayOffset++] = literal[1..$-1]; // remove surrounding quotes 493 } else { 494 assert(0, format("Cannot convert sdl literal to D '%s' type", typeid(T))); 495 } 496 } 497 } 498 499 500 void enforceNoValues() { 501 if(values.data.length) throw new SdlParseException(line, format("tag '%s' cannot have any values", name)); 502 } 503 void enforceNoAttributes() { 504 if(attributes.data.length) throw new SdlParseException(line, format("tag '%s' cannot have any attributes", name)); 505 } 506 void enforceNoChildren() { 507 if(hasOpenBrace) throw new SdlParseException(line, format("tag '%s' cannot have any children", name)); 508 } 509 510 511 } 512 513 version = use_lookup_tables; 514 515 bool isSdlKeyword(const char[] token) { 516 return 517 token == "null" || 518 token == "true" || 519 token == "false" || 520 token == "on" || 521 token == "off"; 522 } 523 bool isIDStart(dchar c) { 524 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 525 /+ 526 The lookup table doesn't seem to be as fast here, maybe this case I should just compare the ranges 527 version(use_lookup_tables) { 528 return (c < sdlLookup.length) ? ((sdlLookup[c] & idStartFlag) != 0) : false; 529 } else { 530 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 531 } 532 +/ 533 } 534 bool isID(dchar c) { 535 version(use_lookup_tables) { 536 return (c < sdlLookup.length) ? ((sdlLookup[c] & sdlIDFlag) != 0) : false; 537 } else { 538 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' || c == '$'; 539 } 540 } 541 enum tooManyEndingBraces = "too many ending braces '}'"; 542 enum noEndingQuote = "string missing ending quote"; 543 enum invalidBraceFmt = "found '{' on a different line than its tag '%s'. fix the sdl by moving '{' to the same line"; 544 enum mixedValuesAndAttributesFmt = "SDL values cannot appear after attributes, bring '%s' in front of the attributes for tag '%s'"; 545 enum notEnoughCloseBracesFmt = "reached end of sdl but missing %s close brace(s) '}'"; 546 547 548 struct SdlParser(A) 549 { 550 char[] buffer; 551 A allocator; 552 char[] leftover; 553 this(char[] buffer, A allocator) 554 { 555 this.buffer = buffer; 556 this.allocator = allocator; 557 } 558 ref Sink parse(Source,Sink)(Source source, Sink sink) 559 if (isInputRange!Source && 560 isOutputRange!(Sink, ElementType!Source)) 561 { 562 // todo implement 563 } 564 } 565 566 567 568 /// Converts literal to the given D type T. 569 /// This is a wrapper arround the $(D sdlLiteralToD) function that returns true on sucess, except 570 /// this function returns the value itself and throws an SdlParseException on error. 571 T sdlLiteralToD(T)(const(char)[] literal) { 572 T value; 573 if(!sdlLiteralToD!(T)(literal, value)) 574 throw new SdlParseException(format("failed to convert '%s' to a %s", literal, typeid(T))); 575 return value; 576 } 577 578 /// Converts literal to the given D type T. 579 /// If isSomeString!T, then it will remove the surrounding quotes if they are present. 580 /// Returns: true on succes, false on failure 581 bool sdlLiteralToD(T)(const(char)[] literal, ref T t) { 582 583 assert(literal.length); 584 585 586 static if( is( T == bool) ) { 587 588 if(literal == "true" || literal == "on" || literal == "1") t = true; 589 if(literal == "false" || literal == "off" || literal == "0") t = false; 590 591 } else static if( isSomeString!T ) { 592 593 if(literal[0] == '"' && literal.length > 1 && literal[$-1] == '"') { 594 t = cast(T)literal[1..$-1]; 595 } else { 596 t = cast(T)literal; 597 } 598 599 } else static if( isIntegral!T || isFloatingPoint!T ) { 600 601 // remove any postfix characters 602 while(true) { 603 char c = literal[$-1]; 604 if(c >= '0' && c <= '9') break; 605 literal.length--; 606 if(literal.length == 0) return false; 607 } 608 609 t = to!T(literal); 610 611 } else { 612 613 t = to!T(literal); 614 615 } 616 617 return true; 618 } 619 620 621 622 623 624 string arrayRange(char min, char max, string initializer) { 625 string initializers = ""; 626 for(char c = min; c < max; c++) { 627 initializers ~= "'"~c~"': "~initializer~",\n"; 628 } 629 initializers ~= "'"~max~"': "~initializer; 630 return initializers; 631 } 632 string rangeInitializers(string[] s...) { 633 if(s.length % 2 != 0) assert(0, "must supply an even number of arguments to rangeInitializers"); 634 string code = "["~rangeInitializersCurrent(s); 635 //assert(0, code); // uncomment to see the code 636 return code; 637 } 638 string rangeInitializersCurrent(string[] s) { 639 string range = s[0]; 640 if(range[0] == '\'') { 641 if(range.length == 3 || (range.length == 4 && range[1] == '\\')) { 642 if(range[$-1] != '\'') throw new Exception(format("a single-character range %s started with an apostrophe (') but did not end with one", range)); 643 return range ~ ":" ~ s[1] ~ rangeInitializersNext(s); 644 } 645 } else { 646 throw new Exception(format("range '%s' not supported", range)); 647 } 648 char min = range[1]; 649 char max = range[5]; 650 return arrayRange(min, max, s[1]) ~ rangeInitializersNext(s); 651 } 652 string rangeInitializersNext(string[] s...) { 653 if(s.length <= 2) return "]"; 654 return ",\n"~rangeInitializersCurrent(s[2..$]); 655 } 656 657 658 enum ubyte sdlIDFlag = 0x01; 659 enum ubyte sdlNumberFlag = 0x02; 660 enum ubyte sdlNumberPostfixFlag = 0x04; 661 enum ubyte sdlValidAfterTagItemFlag = 0x08; 662 663 version(use_lookup_tables) { 664 mixin("private __gshared ubyte[256] sdlLookup = "~rangeInitializers 665 ("'_'" , "sdlIDFlag", 666 667 `'a'` , "sdlIDFlag", 668 `'b'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 669 `'c'` , "sdlIDFlag", 670 `'d'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 671 `'e'` , "sdlIDFlag", 672 `'f'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 673 `'g'-'k'`, "sdlIDFlag", 674 `'l'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 675 `'m'-'z'`, "sdlIDFlag", 676 677 `'A'` , "sdlIDFlag", 678 `'B'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 679 `'C'` , "sdlIDFlag", 680 `'D'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 681 `'E'` , "sdlIDFlag", 682 `'F'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 683 `'G'-'K'`, "sdlIDFlag", 684 `'L'` , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 685 `'M'-'Z'`, "sdlIDFlag", 686 687 `'0'-'9'`, "sdlIDFlag | sdlNumberFlag", 688 `'-'` , "sdlIDFlag", 689 `'.'` , "sdlIDFlag | sdlNumberFlag", 690 `'$'` , "sdlIDFlag", 691 692 `' '` , "sdlValidAfterTagItemFlag", 693 `'\t'` , "sdlValidAfterTagItemFlag", 694 `'\n'` , "sdlValidAfterTagItemFlag", 695 `'\v'` , "sdlValidAfterTagItemFlag", 696 `'\f'` , "sdlValidAfterTagItemFlag", 697 `'\r'` , "sdlValidAfterTagItemFlag", 698 699 `'{'` , "sdlValidAfterTagItemFlag", 700 `'}'` , "sdlValidAfterTagItemFlag", 701 `';'` , "sdlValidAfterTagItemFlag", 702 `'\\'` , "sdlValidAfterTagItemFlag", 703 `'/'` , "sdlValidAfterTagItemFlag", 704 `'#'` , "sdlValidAfterTagItemFlag", 705 706 707 )~";"); 708 } 709 710 /// A convenience function to parse a single tag. 711 /// Calls $(D tag.resetForNewSdl) and then calls $(D parseSdlTag). 712 void parseOneSdlTag(Tag* tag, char[] sdlText) { 713 tag.resetForNewSdl(); 714 if(!parseSdlTag(tag, &sdlText)) throw new SdlParseException(tag.line, format("The sdl text '%s' did not contain any tags", sdlText)); 715 } 716 717 /// Parses one SDL tag (not including its children) from sdlText saving slices for every 718 /// name/value/attribute to the given tag struct. 719 /// This function assumes that sdlText contains at least one full SDL _tag. 720 /// The only time this function will allocate memory is if the value/attribute appenders 721 /// in the tag struct are not large enough to hold all the values. 722 /// Because of this, after the tag values/attributes are populated, it is up to the caller to copy 723 /// any memory they wish to save unless sdlText is going to persist in memory. 724 /// Note: this function does not handle the UTF-8 bom because it doesn't make sense to re-check 725 /// for the BOM before every tag. 726 /// Params: 727 /// tag = An address to a Tag structure to save the sdl tag's name/values/attributes. 728 /// sdlText = An address to the sdl text character array. 729 /// the function will move the front of the slice foward past 730 /// any sdl that was parsed. 731 /// Returns: true if a tag was found, false otherwise 732 /// Throws: SdlParseException or Utf8Exception 733 bool parseSdlTag(Tag* tag, char[]* sdlText) 734 { 735 // developer note: 736 // whenever reading the next character, the next pointer must be saved to cpos 737 // if the character could be used later, but if the next is guaranteed to 738 // be thrown away (such as when skipping till the next newline after a comment) 739 // then cpos does not need to be saved. 740 char *next = (*sdlText).ptr; 741 char *limit = next + sdlText.length; 742 743 744 tag.resetForNextTag(); // make sure this is done first 745 746 char* cpos; 747 dchar c; 748 char[] tokenNamespace; 749 char[] attributeID; 750 char[] token; 751 752 void enforceNoMoreTags() { 753 if(tag.depth > 0) throw new SdlParseException(tag.line, format(notEnoughCloseBracesFmt, tag.depth)); 754 } 755 756 void readNext() 757 { 758 cpos = next; 759 c = decodeUtf8(next, limit); 760 } 761 762 bool isIDStart() { 763 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 764 /+ 765 The lookup table actually seems to be slower in this case 766 version(use_lookup_tables) { 767 return (c < sdlLookup.length) ? ((sdlLookup[c] & idStartFlag) != 0) : false; 768 } else { 769 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 770 } 771 +/ 772 } 773 bool isID() { 774 version(use_lookup_tables) { 775 return c < sdlLookup.length && ((sdlLookup[c] & sdlIDFlag) != 0); 776 } else { 777 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' || c == '$'; 778 } 779 } 780 bool isValidAfterTagItem() { 781 version(use_lookup_tables) { 782 return c < sdlLookup.length && ((sdlLookup[c] & sdlValidAfterTagItemFlag) != 0); 783 } else { 784 implement("isValidAfterTagItem without lookup table"); 785 } 786 } 787 bool isNumber() { 788 version(use_lookup_tables) { 789 return c < sdlLookup.length && ((sdlLookup[c] & sdlNumberFlag) != 0); 790 } else { 791 implement("isNumber without lookup table"); 792 } 793 } 794 bool isNumberPostfix() { 795 version(use_lookup_tables) { 796 return c < sdlLookup.length && ((sdlLookup[c] & sdlNumberPostfixFlag) != 0); 797 } else { 798 implement("isNumberPostfix without lookup table"); 799 } 800 } 801 802 // expected c/cpos to b pointing at a character before newline, so will ready first 803 // before checking for newlines 804 void toNextLine() 805 { 806 while(true) { 807 if(next >= limit) { return; } 808 c = decodeUtf8(next, limit); // no need to save cpos since c will be thrown away 809 if(c == '\n') { tag.line++; return; } 810 } 811 } 812 813 // expects c/cpos to point at the first character of the id and for it to already be checked 814 // when this function is done, c/cpos will pointing to the first character after the id, or 815 // cpos == limit if there are no characters after the id 816 void parseID() 817 { 818 while(true) { 819 if(next >= limit) { cpos = limit; return; } 820 readNext(); 821 if(!isID()) return; 822 } 823 } 824 825 826 // expects c/cpos to point at the first character after the id 827 // Returns: true if the id is actually a value 828 // NOTE: this should only becaused if no namespace was found yet, this function 829 // will always return false if c/cpos is pointing to a ':' which indicates 830 // that it is a namespace even if the namespace could be a value like null or true/false 831 bool currentIDIsValue(char* startOfID) { 832 //switch on the length 833 switch(cpos - startOfID) { 834 case 0-1: return false; 835 case 2: return startOfID[0..2] == "on"; 836 case 3: return startOfID[0..3] == "off"; 837 case 4: return startOfID[0..4] == "null" || 838 startOfID[0..4] == "true"; 839 case 5: return startOfID[0..5] == "false"; 840 default: return false; 841 } 842 } 843 844 // Returns: true if a newline was found 845 // ExpectedState: 846 // c/cpos: points to the first character of the potential whitespace/comment 847 // ReturnState: 848 // c/cpos: points to the first character after all the whitespace/comments 849 bool skipWhitespaceAndComments() 850 { 851 uint lineBefore = tag.line; 852 853 while(true) { 854 855 // TODO: maybe use a lookup table here 856 if(c == ' ' || c == '\t' || c =='\v' || c == '\f' || c == '\r') { 857 858 // do nothing (check first as this is the most likely case) 859 860 } else if(c == '\n') { 861 862 tag.line++; 863 864 } else if(c == '#') { 865 866 toNextLine(); 867 868 } else if(c == '-' || c == '/') { 869 870 if(next >= limit) return tag.line > lineBefore; 871 872 dchar secondChar = decodeUtf8(next, limit); 873 874 if(secondChar == c) { // '--' or '//' 875 876 toNextLine(); 877 878 } else if(secondChar == '*') { 879 880 MULTILINE_COMMENT_LOOP: 881 while(next < limit) { 882 883 c = decodeUtf8(next, limit); // no need to save cpos since c will be thrown away 884 if(c == '\n') { 885 tag.line++; 886 } else if(c == '*') { 887 // loop assume c is pointing to a '*' and next is pointing to the next characer 888 while(next < limit) { 889 890 c = decodeUtf8(next, limit); 891 if(c == '/') break MULTILINE_COMMENT_LOOP; 892 if(c == '\n') { 893 tag.line++; 894 } else if(c != '*') { 895 break; 896 } 897 } 898 } 899 } 900 901 } else { 902 return tag.line > lineBefore; 903 } 904 905 } else { 906 return tag.line > lineBefore; 907 } 908 909 // 910 // Goto next character 911 // 912 if(next >= limit) {cpos = next; break;} 913 readNext(); 914 } 915 916 return tag.line > lineBefore; 917 } 918 919 920 // The atFirstToken will only parse tokens that are valid IDs 921 // Expected State: 922 // tokenNamespace/attributeID to be empty (sets these if they are found) 923 // c/cpos: points at the first character of the token 924 // Return State: 925 // If it finds a token, will set tokenNamespace and/or token with values 926 // c/cpos: points to the next character after the literal 927 // next: character after cpos 928 void tryParseToken(bool atFirstToken) { 929 token.length = 0; // clear the previous token 930 931 // Handles 932 // tag name 933 // tag namespace/ tag name 934 // unquoted string 935 // attribute 936 // attribute with namespace, does not handle the value after the attribute 937 if(isIDStart() || c == ':') { // colon for empty namespaces 938 auto start = cpos; 939 940 if(c != ':') { 941 while(true) { 942 cpos = next; 943 if(next >= limit) { token = start[0..next-start]; return; } 944 c = decodeUtf8(next, limit); 945 if(!isID()) break; 946 } 947 } 948 949 // Handle Namespaces 950 if(c == ':') { 951 952 tokenNamespace = start[0..next-start]; // include the colon so the calling function 953 // can differentiate between the empty namespace 954 // and no namespace 955 cpos = next; 956 if(next >= limit) { token.length = 0; return; } 957 c = decodeUtf8(next, limit); 958 959 if(!isIDStart()) { 960 if(atFirstToken) { 961 if(!isValidAfterTagItem()) throw new SdlParseException 962 (tag.line, format("Character '%s' cannot appear after the tag's namespace", c)); 963 return; 964 } 965 throw new SdlParseException(tag.line, 966 format("expected alphanum or '_' after an attribute namespace colon, but got '%s'", c)); 967 } 968 969 start = cpos; 970 while(true) { 971 cpos = next; 972 if(next >= limit) { token = start[0..next-start]; return; } 973 c = decodeUtf8(next, limit); 974 if(!isID()) break; 975 } 976 977 } 978 979 // Handle attributes 980 if(c != '=') { 981 if(!isValidAfterTagItem()) throw new SdlParseException 982 (tag.line, format("Character '%s' cannot appear after tag item", c)); 983 token = start[0..next-start-1]; 984 return; 985 } 986 987 // Setup to parse the value 988 attributeID = start[0..next-start-1]; // subtract 1 for the '=' 989 if(next >= limit) throw new SdlParseException 990 (tag.line, format("sdl cannot end with '=' character")); 991 cpos = next; 992 c = decodeUtf8(next, limit); 993 } 994 995 996 // Handles: keywords and, 997 // unquoted strings in attribute values 998 if(isIDStart()) { 999 auto start = cpos; 1000 1001 while(true) { 1002 cpos = next; 1003 if(next >= limit) { token = start[0..next-start]; return; } 1004 c = decodeUtf8(next, limit); 1005 if(!isID()) break; 1006 } 1007 1008 if(!isValidAfterTagItem()) throw new SdlParseException 1009 (tag.line, format("Character '%s' cannot appear after a tag item", c)); 1010 1011 token = start[0..next-start-1]; 1012 return; 1013 } 1014 1015 if(atFirstToken) return; 1016 1017 if(c == '"') { 1018 1019 bool containsEscapes = false; 1020 1021 while(true) { 1022 1023 if(next >= limit) throw new SdlParseException(tag.line, noEndingQuote); 1024 c = decodeUtf8(next, limit); // no need to save cpos since c will be thrown away 1025 if(c == '"') break; 1026 if(c == '\\') { 1027 containsEscapes = true; 1028 if(next >= limit) throw new SdlParseException(tag.line, noEndingQuote); 1029 // NOTE TODO: remember to handle escaped newlines 1030 c = decodeUtf8(next, limit); 1031 } else if(c == '\n') { 1032 throw new SdlParseException(tag.line, noEndingQuote); 1033 } 1034 1035 } 1036 1037 if(containsEscapes) { 1038 1039 /* do something differnt if immuable */ 1040 implement("escaped strings"); 1041 1042 } else { 1043 token = cpos[0..next - cpos]; 1044 } 1045 1046 cpos = next; 1047 if( next < limit) c = decodeUtf8(next, limit); 1048 1049 } else if(c == '`') { 1050 1051 implement("tick strings"); 1052 1053 } else if(c >= '0' && c <= '9' || c == '-' || c == '.') { 1054 1055 auto start = cpos; 1056 1057 while(true) { 1058 cpos = next; 1059 if(next >= limit) { token = start[0..cpos-start]; break; } 1060 c = decodeUtf8(next, limit); 1061 if(!isNumber()) { token = start[0..cpos-start]; break; } 1062 1063 //if(tag.rejectTypedNumbers && isNumberPostfix()) 1064 //throw new SdlParseException(tag.line, "using this sdl mode, postfix characters indicating the type after a number are not allowed"); 1065 } 1066 1067 } else if(c == '\'') { 1068 1069 implement("sing-quoted characters"); 1070 1071 } 1072 } 1073 1074 // 1075 // Read the first character 1076 // 1077 if(next >= limit) { enforceNoMoreTags(); goto RETURN_NO_TAG; } 1078 readNext(); 1079 1080 while(true) { 1081 1082 skipWhitespaceAndComments(); 1083 if(cpos >= limit) { enforceNoMoreTags(); goto RETURN_NO_TAG; } 1084 1085 // 1086 // 1087 // Get the tag name/namespace 1088 // 1089 tokenNamespace.length = 0; 1090 attributeID.length = 0; 1091 tryParseToken(true); 1092 1093 if(token.length || tokenNamespace.length) { 1094 1095 if(attributeID.length) { 1096 1097 tag.setIsAnonymous(); 1098 1099 if(tokenNamespace.length) tokenNamespace = tokenNamespace[0..$-1]; // Remove ending ':' on namespace 1100 Attribute attribute = {tokenNamespace, attributeID, token}; 1101 tag.attributes.put(attribute); 1102 1103 } else { 1104 1105 if(tokenNamespace.length) { 1106 tag.namespace = tokenNamespace[0..$-1]; 1107 if(token.length) { 1108 tag.name = token; 1109 } else { 1110 tag.setIsAnonymous(); 1111 if(tag.namespace.length == 0) 1112 throw new SdlParseException(tag.line, "A tag cannot have an empty namespace and an empty name"); 1113 } 1114 } else { 1115 1116 if(isSdlKeyword(token)) { 1117 tag.setIsAnonymous(); 1118 tag.values.put(token); 1119 } else { 1120 tag.name = token; 1121 } 1122 1123 } 1124 1125 } 1126 1127 } else if(c == '}') { 1128 1129 if(tag.depth == 0) throw new SdlParseException(tag.line, tooManyEndingBraces); 1130 tag.depth--; 1131 1132 // Read the next character 1133 if(next >= limit) { enforceNoMoreTags(); goto RETURN_NO_TAG; } 1134 cpos = next; 1135 c = decodeUtf8(next, limit); 1136 1137 continue; 1138 1139 } else if(c == '\\') { 1140 throw new SdlParseException(tag.line, "expected tag or '}' but got backslash '\\'"); 1141 } else if(c == '{') { 1142 throw new SdlParseException(tag.line, "expected tag or '}' but got '{'"); 1143 } else if(c == ':') { 1144 throw new SdlParseException(tag.line, "expected tag or '}' but got ':'"); 1145 } else { 1146 1147 tag.namespace.length = 0; 1148 tag.setIsAnonymous(); 1149 1150 } 1151 1152 // 1153 // 1154 // Found a valid tag, now get values and attributes 1155 // 1156 // 1157 GET_VALUES_AND_ATTRIBUTES: 1158 while(true) { 1159 1160 // At the beginning of this loop, it is expected that c/cpos will be pointing the 1161 // next character after the last thing (tag/value/attribute) 1162 1163 if(cpos >= limit) goto RETURN_TAG; // I may not need this check 1164 auto foundNewline = skipWhitespaceAndComments(); 1165 if(cpos >= limit) goto RETURN_TAG; 1166 if(foundNewline) { 1167 1168 // check if it is a curly brace to either print a useful error message 1169 // or determine if the tag has children 1170 if(c != '{') { 1171 next = cpos; // rewind so whatever character it is will 1172 // be parsed again on the next call 1173 goto RETURN_TAG; 1174 } 1175 if(tag.allowBraceAfterNewline) { 1176 tag.hasOpenBrace = true; 1177 goto RETURN_TAG; 1178 } 1179 1180 throw new SdlParseException(SdlErrorType.braceAfterNewline, tag.line, 1181 format(invalidBraceFmt, tag.name)); 1182 } 1183 1184 // 1185 // At this point c must contain a non-whitespace character 1186 // and we must have already parsed the tag name 1187 // 1188 1189 if(c == ';') goto RETURN_TAG; 1190 1191 // 1192 // Handle the '\' character to escape newlines 1193 // 1194 if(c == '\\') { 1195 if(next >= limit) goto RETURN_TAG; // (check to make sure ending an sdl file with a backslash is ok) 1196 c = decodeUtf8(next, limit); 1197 1198 foundNewline = skipWhitespaceAndComments(); 1199 if(cpos >= limit) goto RETURN_TAG; 1200 if(!foundNewline) throw new SdlParseException(tag.line, "only comments/whitespace can follow a backslash '\\'"); 1201 1202 continue; 1203 } 1204 1205 if(c == '{') { 1206 tag.hasOpenBrace = true; // depth will be incremented at the next parse 1207 goto RETURN_TAG; 1208 } 1209 1210 if(c == '}') { 1211 if(tag.depth == 0) throw new SdlParseException(tag.line, tooManyEndingBraces); 1212 next = cpos; // rewind so the '}' will be seen on the next call and 1213 // the depth will change on the next call 1214 goto RETURN_TAG; 1215 } 1216 1217 // 1218 // Try to parse an attribute 1219 // 1220 tokenNamespace.length = 0; 1221 attributeID.length = 0; 1222 tryParseToken(false); 1223 if(token.length || tokenNamespace.length) { 1224 1225 if(attributeID.length > 0) { 1226 if(tokenNamespace.length) tokenNamespace = tokenNamespace[0..$-1]; // Remove ending ':' on namespace 1227 Attribute attribute = {tokenNamespace, attributeID, token}; 1228 tag.attributes.put(attribute); 1229 } else { 1230 1231 if(tokenNamespace.length) 1232 throw new SdlParseException(tag.line, format("Found a tag value with a namespace '%s%s'?", tokenNamespace, token)); 1233 1234 if(tag.attributes.data.length) { 1235 if(!tag.allowMixedValuesAndAttributes) 1236 throw new SdlParseException(SdlErrorType.mixedValuesAndAttributes, tag.line, 1237 format(mixedValuesAndAttributesFmt, token, tag.name)); 1238 } 1239 1240 tag.values.put(token); 1241 } 1242 1243 if(cpos >= limit) goto RETURN_TAG; 1244 1245 } else { 1246 1247 if(attributeID.length > 0) throw new SdlParseException(tag.line, "expected sdl literal to follow attribute '=' but was not a literal"); 1248 1249 if(c == '\0') throw new Exception("possible code bug: found null"); 1250 throw new Exception(format("Unhandled character '%s' (code=0x%x)", c, cast(uint)c)); 1251 1252 } 1253 } 1254 1255 } 1256 1257 assert(0); 1258 1259 RETURN_TAG: 1260 *sdlText = next[0..limit-next]; 1261 return true; 1262 1263 RETURN_NO_TAG: 1264 (*sdlText) = limit[0..0]; 1265 return false; 1266 } 1267 1268 version(unittest) 1269 { 1270 char[2048] sdlBuffer; 1271 char[sdlBuffer.length] sdlBuffer2; 1272 char[] setupSdlText(const(char[]) sdlText, bool copySdl) 1273 { 1274 if(!copySdl) return cast(char[])sdlText; 1275 1276 if(sdlText.length >= sdlBuffer.length) throw new Exception(format("attempting to copy sdl of length %s but sdlBuffer is only of length %s", sdlText.length, sdlBuffer.length)); 1277 sdlBuffer[0..sdlText.length] = sdlText; 1278 return sdlBuffer[0..sdlText.length]; 1279 } 1280 1281 struct SdlBuffer2Sink 1282 { 1283 size_t offset; 1284 @property 1285 char[] slice() { return sdlBuffer2[0..offset]; } 1286 void put(inout(char)[] value) { 1287 sdlBuffer2[offset..offset+value.length] = value; 1288 offset += value.length; 1289 } 1290 } 1291 1292 } 1293 1294 1295 version(unittest_sdl) unittest 1296 { 1297 //return; // Uncomment to disable these tests 1298 1299 mixin(scopedTest!"SdlParse"); 1300 1301 Tag parsedTag; 1302 1303 void useProposed() { 1304 debug writefln("[TEST] SdlMode: Proposed"); 1305 parsedTag.useProposedSdl(); 1306 } 1307 void useStrict() { 1308 debug writefln("[TEST] SdlMode: Strict"); 1309 parsedTag.useStrictSdl(); 1310 } 1311 struct SdlTest 1312 { 1313 bool copySdl; 1314 string sdlText; 1315 Tag[] expectedTags; 1316 size_t line; 1317 this(string sdlText, Tag[] expectedTags, size_t line = __LINE__) { 1318 this.copySdl = false; 1319 this.sdlText = sdlText; 1320 this.expectedTags = expectedTags; 1321 this.line = line; 1322 } 1323 } 1324 1325 void testParseSdl(bool reparse = true)(bool copySdl, const(char)[] sdlText, Tag[] expectedTags = [], size_t line = __LINE__) 1326 { 1327 size_t previousDepth = size_t.max; 1328 SdlBuffer2Sink buffer2Sink; 1329 1330 auto escapedSdlText = escape(sdlText); 1331 1332 debug { 1333 static if(reparse) { 1334 writefln("[TEST] testing sdl '%s'", escapedSdlText); 1335 } else { 1336 writefln("[TEST] testing sdl (regenerated)'%s'", escapedSdlText); 1337 } 1338 } 1339 1340 char[] next = setupSdlText(sdlText, copySdl); 1341 1342 parsedTag.resetForNewSdl(); 1343 1344 1345 try { 1346 1347 for(auto i = 0; i < expectedTags.length; i++) { 1348 if(!parseSdlTag(&parsedTag, &next)) { 1349 writefln("Expected %s tag(s) but only got %s", expectedTags.length, i); 1350 writefln("Error: test on line %s", line); 1351 assert(0); 1352 } 1353 1354 static if(reparse) { 1355 if(previousDepth != size_t.max) { 1356 while(previousDepth > parsedTag.depth) { 1357 buffer2Sink.put("}"); 1358 previousDepth--; 1359 } 1360 } 1361 } 1362 1363 auto expectedTag = expectedTags[i]; 1364 if(parsedTag.namespace != expectedTag.namespace) { 1365 writefln("Error: expected tag namespace '%s' but got '%s'", expectedTag.namespace, parsedTag.namespace); 1366 writefln("Error: test on line %s", line); 1367 assert(0); 1368 } 1369 if(parsedTag.name != expectedTag.name) { 1370 writefln("Error: expected tag name '%s' but got '%s'", expectedTag.name, parsedTag.name); 1371 writefln("Error: test on line %s", line); 1372 assert(0); 1373 } 1374 //writefln("[DEBUG] expected value '%s', actual values '%s'", expectedTag.values.data, parsedTag.values.data); 1375 if(parsedTag.values.data != expectedTag.values.data) { 1376 writefln("Error: expected tag values '%s' but got '%s'", expectedTag.values.data, parsedTag.values.data); 1377 writefln("Error: test on line %s", line); 1378 assert(0); 1379 } 1380 if(parsedTag.attributes.data != expectedTag.attributes.data) { 1381 writefln("Error: expected tag attributes '%s' but got '%s'", expectedTag.attributes.data, parsedTag.attributes.data); 1382 writefln("Error: test on line %s", line); 1383 assert(0); 1384 } 1385 1386 // put the tag into the buffer2 sink to reparse again after 1387 static if(reparse) { 1388 parsedTag.toSdl(&buffer2Sink); 1389 previousDepth = parsedTag.depth; 1390 if(parsedTag.hasOpenBrace) previousDepth++; 1391 } 1392 } 1393 1394 if(parseSdlTag(&parsedTag, &next)) { 1395 writefln("Expected %s tag(s) but got at least one more (depth=%s, name='%s')", 1396 expectedTags.length, parsedTag.depth, parsedTag.name); 1397 writefln("Error: test on line %s", line); 1398 assert(0); 1399 } 1400 1401 } catch(SdlParseException e) { 1402 writefln("[TEST] this sdl threw an unexpected SdlParseException: '%s'", escape(sdlText)); 1403 writeln(e); 1404 writefln("Error: test on line %s", line); 1405 assert(0); 1406 } catch(Exception e) { 1407 writefln("[TEST] this sdl threw an unexpected Exception: '%s'", escape(sdlText)); 1408 writeln(e); 1409 writefln("Error: test on line %s", line); 1410 assert(0); 1411 } 1412 1413 static if(reparse) { 1414 if(previousDepth != size_t.max) { 1415 while(previousDepth > parsedTag.depth) { 1416 buffer2Sink.put("}"); 1417 previousDepth--; 1418 } 1419 } 1420 1421 if(buffer2Sink.slice != sdlText && 1422 (buffer2Sink.slice.length && buffer2Sink.slice[0..$-1] != sdlText)) { 1423 testParseSdl!false(false, buffer2Sink.slice, expectedTags, line); 1424 } 1425 } 1426 1427 } 1428 1429 void testInvalidSdl(bool copySdl, const(char)[] sdlText, SdlErrorType expectedErrorType = SdlErrorType.unknown, size_t line = __LINE__) { 1430 auto escapedSdlText = escape(sdlText); 1431 debug writefln("[TEST] testing invalid sdl '%s'", escapedSdlText); 1432 1433 SdlErrorType actualErrorType = SdlErrorType.unknown; 1434 1435 char[] next = setupSdlText(sdlText, copySdl); 1436 1437 parsedTag.resetForNewSdl(); 1438 try { 1439 while(parseSdlTag(&parsedTag, &next)) { } 1440 writefln("Error: invalid sdl was successfully parsed: %s", sdlText); 1441 writefln("Error: test was on line %s", line); 1442 assert(0); 1443 } catch(SdlParseException e) { 1444 debug writefln("[TEST] got expected error: %s", e.msg); 1445 actualErrorType = e.type; 1446 } catch(Utf8Exception e) { 1447 debug writefln("[TEST] got expected error: %s", e.msg); 1448 } 1449 1450 if(expectedErrorType != SdlErrorType.unknown && 1451 expectedErrorType != actualErrorType) { 1452 writefln("Error: expected error '%s' but got error '%s'", expectedErrorType, actualErrorType); 1453 writefln("Error: test was on line %s", line); 1454 assert(0); 1455 } 1456 1457 } 1458 1459 testParseSdl(false, ""); 1460 testParseSdl(false, " "); 1461 testParseSdl(false, "\n"); 1462 1463 testParseSdl(false, "#Comment"); 1464 testParseSdl(false, "#Comment copyright \u00a8"); 1465 testParseSdl(false, "#Comment\n"); 1466 testParseSdl(false, "#Comment\r\n"); 1467 testParseSdl(false, " # Comment\r\n"); 1468 1469 testParseSdl(false, " -- Comment\n"); 1470 testParseSdl(false, " ------ Comment\n"); 1471 1472 testParseSdl(false, " # Comment1 \r\n -- Comment 2"); 1473 1474 1475 testParseSdl(false, " // Comment\n"); 1476 testParseSdl(false, " //// Comment\n"); 1477 1478 testParseSdl(false, "/* a multiline comment \n\r\n\n\n\t hello stuff # -- // */"); 1479 1480 // TODO: test this using the allowBracesAfterNewline option 1481 // testParseSdl(false, "tag /*\n\n*/{ child }", Tag("tag"), Tag("child")); 1482 1483 1484 testParseSdl(false, "a", [Tag("a")]); 1485 testParseSdl(false, "ab", [Tag("ab")]); 1486 testParseSdl(false, "abc", [Tag("abc")]); 1487 testParseSdl(false, "firsttag", [Tag("firsttag")]); 1488 testParseSdl(false, "funky._-$tag", [Tag("funky._-$tag")]); 1489 1490 1491 { 1492 auto prefixes = ["", " ", "\t", "--comment\n"]; 1493 foreach(prefix; prefixes) { 1494 testInvalidSdl(false, prefix~":"); 1495 } 1496 } 1497 1498 1499 auto validCharactersAfterTag = 1500 [" ", "\t", "\n", "\v", "\f", "\r", ";", "//", "\\", "#", "{}"]; 1501 1502 1503 auto namespaces = ["a:", "ab:", "abc:"]; 1504 bool isProposedSdl = false; 1505 while(true) { 1506 string tagName; 1507 if(isProposedSdl) { 1508 tagName = null; 1509 useProposed(); 1510 } else { 1511 tagName = "content"; 1512 } 1513 foreach(namespace; namespaces) { 1514 testParseSdl(false, namespace, [Tag(namespace~tagName)]); 1515 foreach(suffix; validCharactersAfterTag) { 1516 testParseSdl(false, namespace~suffix, [Tag(namespace~tagName)]); 1517 } 1518 testParseSdl(false, "tag1{"~namespace~"}", [Tag("tag1"), Tag(namespace~tagName)]); 1519 } 1520 if(isProposedSdl) break; 1521 isProposedSdl = true; 1522 } 1523 useStrict(); 1524 1525 1526 testParseSdl(false, "a:a", [Tag("a:a")]); 1527 testParseSdl(false, "ab:a", [Tag("ab:a")]); 1528 1529 testParseSdl(false, "a:ab", [Tag("a:ab")]); 1530 testParseSdl(false, "ab:ab", [Tag("ab:ab")]); 1531 1532 testParseSdl(false, "html:table", [Tag("html:table")]); 1533 1534 testParseSdl(false, ";", [Tag("content")]); 1535 testParseSdl(false, "myid;", [Tag("myid")]); 1536 testParseSdl(false, "myid; ", [Tag("myid")]); 1537 testParseSdl(false, "myid #comment", [Tag("myid")]); 1538 testParseSdl(false, "myid # comment \n", [Tag("myid")]); 1539 testParseSdl(false, "myid -- comment \n # more comments\n", [Tag("myid")]); 1540 1541 1542 testParseSdl(false, "myid /* multiline comment */", [Tag("myid")]); 1543 testParseSdl(false, "myid /* multiline comment */ ", [Tag("myid")]); 1544 testParseSdl(false, "myid /* multiline comment */\n", [Tag("myid")]); 1545 testParseSdl(false, "myid /* multiline comment \n\n */", [Tag("myid")]); 1546 testParseSdl(false, "myid /* multiline comment **/ \"value\"", [Tag("myid", `"value"`)]); 1547 testParseSdl(false, "myid /* multiline comment \n\n */another-id", [Tag("myid"), Tag("another-id")]); 1548 testParseSdl(false, "myid /* multiline comment */ \"value\"", [Tag("myid", `"value"`)]); 1549 testParseSdl(false, "myid /* multiline comment \n */ \"value\"", [Tag("myid"), Tag("content", `"value"`)]); 1550 testInvalidSdl(false, "myid /* multiline comment \n */ { \n }"); 1551 useProposed(); 1552 testParseSdl(false, "myid /* multiline comment */ { \n }", [Tag("myid")]); 1553 testParseSdl(false, "myid /* multiline comment \n */ \"value\"", [Tag("myid"), Tag(null, `"value"`)]); 1554 useStrict(); 1555 1556 1557 testParseSdl(false, "tag1\ntag2", [Tag("tag1"), Tag("tag2")]); 1558 testParseSdl(false, "tag1;tag2\ntag3", [Tag("tag1"), Tag("tag2"), Tag("tag3")]); 1559 1560 testInvalidSdl(false, "myid {"); 1561 testInvalidSdl(false, "myid {\n\n"); 1562 1563 testInvalidSdl(false, "{}"); 1564 1565 testParseSdl(false, "tag1{}", [Tag("tag1")]); 1566 testParseSdl(false, "tag1{}tag2", [Tag("tag1"), Tag("tag2")]); 1567 testParseSdl(false, "tag1{}\ntag2", [Tag("tag1"), Tag("tag2")]); 1568 1569 testParseSdl(false, "tag1{tag1.1}tag2", [Tag("tag1"), Tag("tag1.1"), Tag("tag2")]); 1570 1571 // 1572 // Handling the backslash '\' character 1573 // 1574 testInvalidSdl(false, "\\"); // slash must in the context of a tag 1575 testInvalidSdl(false, `tag \ x`); 1576 1577 testParseSdl(false, "tag\\", [Tag("tag")]); // Make sure this is valid sdl 1578 testParseSdl(false, "tag \\ \n \\ \n \"hello\"", [Tag("tag", `"hello"`)]); 1579 1580 // 1581 // Test the keywords (white box tests trying to attain full code coverage) 1582 // 1583 auto keywords = ["null", "true", "false", "on", "off"]; 1584 1585 foreach(keyword; keywords) { 1586 testParseSdl(false, keyword, [Tag("content", keyword)]); 1587 } 1588 1589 namespaces = ["", "n:", "namespace:"]; 1590 foreach(namespace; namespaces) { 1591 sdlBuffer[0..namespace.length] = namespace; 1592 auto afterTagName = namespace.length + 4; 1593 sdlBuffer[namespace.length..afterTagName] = "tag "; 1594 string expectedTagName = namespace~"tag"; 1595 1596 foreach(keyword; keywords) { 1597 for(auto cutoff = 1; cutoff < keyword.length; cutoff++) { 1598 sdlBuffer[afterTagName..afterTagName+cutoff] = keyword[0..cutoff]; 1599 //testInvalidSdl(false, sdlBuffer[0..afterTagName+cutoff]); 1600 testParseSdl(false, sdlBuffer[0..afterTagName+cutoff], [Tag(expectedTagName, sdlBuffer[afterTagName..afterTagName+cutoff])]); 1601 } 1602 } 1603 auto suffixes = [";", " \t;", "\n", "{}", " \t {\n }"]; 1604 foreach(keyword; keywords) { 1605 auto limit = afterTagName+keyword.length; 1606 1607 sdlBuffer[afterTagName..limit] = keyword; 1608 testParseSdl(false, sdlBuffer[0..limit], [Tag(expectedTagName, keyword)]); 1609 1610 foreach(suffix; suffixes) { 1611 sdlBuffer[limit..limit+suffix.length] = suffix; 1612 testParseSdl(false, sdlBuffer[0..limit+suffix.length], [Tag(expectedTagName, keyword)]); 1613 } 1614 } 1615 foreach(keyword; keywords) { 1616 1617 foreach(attrNamespace; namespaces) { 1618 1619 for(auto cutoff = 1; cutoff <= keyword.length; cutoff++) { 1620 auto limit = afterTagName + attrNamespace.length; 1621 sdlBuffer[afterTagName..limit] = attrNamespace; 1622 limit += cutoff; 1623 sdlBuffer[limit - cutoff..limit] = keyword[0..cutoff]; 1624 sdlBuffer[limit..limit+8] = `="value"`; 1625 testParseSdl(false, sdlBuffer[0..limit+8], [Tag(expectedTagName, format(`%s%s="value"`, attrNamespace, keyword[0..cutoff]))]); 1626 1627 foreach(otherKeyword; keywords) { 1628 sdlBuffer[limit+1..limit+1+otherKeyword.length] = otherKeyword; 1629 testParseSdl(false, sdlBuffer[0..limit+1+otherKeyword.length], 1630 [Tag(expectedTagName, format("%s%s=%s", attrNamespace, keyword[0..cutoff], otherKeyword))]); 1631 } 1632 } 1633 1634 } 1635 1636 } 1637 } 1638 1639 1640 1641 1642 // 1643 // String Literals 1644 // 1645 testParseSdl(false, `a "apple"`, [Tag("a", `"apple"`)]); 1646 testParseSdl(false, "a \"pear\"\n", [Tag("a", `"pear"`)]); 1647 testParseSdl(false, "a \"left\"\nb \"right\"", [Tag("a", `"left"`), Tag("b", `"right"`)]); 1648 testParseSdl(false, "a \"cat\"\"dog\"\"bear\"\n", [Tag("a", `"cat"`, `"dog"`, `"bear"`)]); 1649 testParseSdl(false, "a \"tree\";b \"truck\"\n", [Tag("a", `"tree"`), Tag("b", `"truck"`)]); 1650 1651 1652 // 1653 // Unquoted Strings 1654 // 1655 testParseSdl(false, "tag string", [Tag("tag", "string")]); 1656 testParseSdl(false, "tag attr=string", [Tag("tag", "attr=string")]); 1657 1658 1659 1660 // 1661 // Attributes 1662 // 1663 testParseSdl(false, "tag attr=null", [Tag("tag", "attr=null")]); 1664 testParseSdl(false, "tag \"val\" attr=null", [Tag("tag", `"val"`, "attr=null")]); 1665 1666 auto mixedValuesAndAttributesTests = 1667 [ 1668 SdlTest("tag attr=null \"val\"", [Tag("tag", "attr=null", `"val"`)] ) 1669 ]; 1670 1671 foreach(test; mixedValuesAndAttributesTests) { 1672 testInvalidSdl(test.copySdl, test.sdlText, SdlErrorType.mixedValuesAndAttributes); 1673 } 1674 useProposed(); 1675 foreach(test; mixedValuesAndAttributesTests) { 1676 testParseSdl(test.copySdl, test.sdlText, test.expectedTags); 1677 } 1678 useStrict(); 1679 1680 foreach(suffix; validCharactersAfterTag) { 1681 testParseSdl(false, "tag attr=null"~suffix, [Tag("tag", "attr=null")]); 1682 testParseSdl(false, "tag attr=true"~suffix, [Tag("tag", "attr=true")]); 1683 testParseSdl(false, "tag attr=unquoted"~suffix, [Tag("tag", "attr=unquoted")]); 1684 testParseSdl(false, "tag attr=\"quoted\""~suffix, [Tag("tag", "attr=\"quoted\"")]); 1685 testParseSdl(false, "tag attr=1234"~suffix, [Tag("tag", "attr=1234")]); 1686 1687 testParseSdl(false, "attr=null"~suffix, [Tag("content", "attr=null")]); 1688 testParseSdl(false, "attr=true"~suffix, [Tag("content", "attr=true")]); 1689 testParseSdl(false, "attr=unquoted"~suffix, [Tag("content", "attr=unquoted")]); 1690 testParseSdl(false, "attr=\"quoted\""~suffix, [Tag("content", "attr=\"quoted\"")]); 1691 testParseSdl(false, "attr=1234"~suffix, [Tag("content", "attr=1234")]); 1692 } 1693 1694 // 1695 // Test parsing numbers without extracting them 1696 // 1697 enum numberPostfixes = ["", "l", "L", "f", "F", "d", "D", "bd", "BD"]; 1698 { 1699 enum sdlPostfixes = ["", " ", ";", "\n"]; 1700 1701 auto numbers = ["0", "12", "9876", "5432", /*".1",*/ "0.1", "12.4", /*"1.",*/ "8.04", "123.l"]; 1702 1703 1704 for(size_t negative = 0; negative < 2; negative++) { 1705 string prefix = negative ? "-" : ""; 1706 1707 foreach(postfix; numberPostfixes) { 1708 foreach(number; numbers) { 1709 1710 auto testNumber = prefix~number~postfix; 1711 1712 if(postfix.length) { 1713 useProposed(); 1714 //testInvalidSdl(false, "tag "~testNumber); 1715 useStrict(); 1716 } 1717 //testInvalidSdl(false, "tag "~testNumber~"="); 1718 1719 foreach(sdlPostfix; sdlPostfixes) { 1720 testParseSdl(false, "tag "~testNumber~sdlPostfix, [Tag("tag", testNumber)]); 1721 } 1722 } 1723 } 1724 1725 1726 } 1727 } 1728 1729 // 1730 // Test parsing numbers and extracting them 1731 // 1732 { 1733 for(size_t negative = 0; negative < 2; negative++) { 1734 string prefix = negative ? "-" : ""; 1735 1736 foreach(postfix; numberPostfixes) { 1737 1738 void testNumber(Types...)(ulong expectedValue) { 1739 long expectedSignedValue = negative ? -1 * (cast(long)expectedValue) : cast(long)expectedValue; 1740 1741 foreach(Type; Types) { 1742 if(negative && isUnsigned!Type) continue; 1743 if(expectedSignedValue > Type.max) continue; 1744 static if( is(Type == float) || is(Type == double) || is(Type == real)) { 1745 if(expectedSignedValue < Type.min_normal) continue; 1746 } else { 1747 if(expectedSignedValue < Type.min) continue; 1748 } 1749 1750 debug writefln("[DEBUG] testing %s on %s", typeid(Type), parsedTag.values.data[0]); 1751 Type t; 1752 parsedTag.getOneValue(t); 1753 assert(t == cast(Type) expectedSignedValue, format("Expected (%s) %s but got %s", typeid(Type), expectedSignedValue, t)); 1754 } 1755 } 1756 void testDecimalNumber(Types...)(real expectedValue) { 1757 foreach(Type; Types) { 1758 if(negative && isUnsigned!Type) continue; 1759 if(expectedValue > Type.max) continue; 1760 static if( is(Type == float) || is(Type == double) || is(Type == real)) { 1761 if(expectedValue < Type.min_normal) continue; 1762 } else { 1763 if(expectedValue < Type.min) continue; 1764 } 1765 1766 debug writefln("[DEBUG] testing %s on %s", typeid(Type), parsedTag.values.data[0]); 1767 Type t; 1768 parsedTag.getOneValue(t); 1769 assert(t - cast(Type) expectedValue < .01, format("Expected (%s) %s but got %s", typeid(Type), cast(Type)expectedValue, t)); 1770 } 1771 } 1772 1773 alias testNumber!(byte,ubyte,short,ushort,int,uint,long,ulong,float,double,real) testNumberOnAllTypes; 1774 alias testDecimalNumber!(float,double,real) testDecimalNumberOnAllTypes; 1775 1776 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"0"~postfix); 1777 testNumberOnAllTypes(0); 1778 1779 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"1"~postfix); 1780 testNumberOnAllTypes(1); 1781 1782 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"12"~postfix); 1783 testNumberOnAllTypes(12); 1784 1785 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"9987"~postfix); 1786 testNumberOnAllTypes(9987); 1787 1788 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"0.0"~postfix); 1789 testDecimalNumberOnAllTypes(0.0); 1790 1791 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~".1"~postfix); 1792 testDecimalNumberOnAllTypes(0.1); 1793 1794 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~".000001"~postfix); 1795 testDecimalNumberOnAllTypes(0.000001); 1796 1797 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"100384.999"~postfix); 1798 testDecimalNumberOnAllTypes(100384.999); 1799 1800 parseOneSdlTag(&parsedTag, cast(char[])"tag "~prefix~"3.14159265"~postfix); 1801 testDecimalNumberOnAllTypes(3.14159265); 1802 } 1803 } 1804 1805 } 1806 1807 1808 // 1809 // Children 1810 // 1811 testInvalidSdl(false, "{}"); // no line can start with a curly brace 1812 1813 auto braceAfterNewlineTests = 1814 [ 1815 SdlTest("tag\n{ child\n}", [Tag("tag"), Tag("child")]), 1816 SdlTest("colors \"hot\" \n{ yellow\n}", [Tag("colors", `"hot"`), Tag("yellow")]) 1817 ]; 1818 1819 foreach(test; braceAfterNewlineTests) { 1820 testInvalidSdl(test.copySdl, test.sdlText, SdlErrorType.braceAfterNewline); 1821 } 1822 useProposed(); 1823 foreach(test; braceAfterNewlineTests) { 1824 testParseSdl(test.copySdl, test.sdlText, test.expectedTags); 1825 } 1826 useStrict(); 1827 1828 // 1829 // Odd corner cases 1830 // 1831 testParseSdl(false, "tag null;", [Tag("tag", "null")]); 1832 testParseSdl(false, "tag null{}", [Tag("tag", "null")]); 1833 testParseSdl(false, "tag true;", [Tag("tag", "null")]); 1834 testParseSdl(false, "tag true{}", [Tag("tag", "null")]); 1835 testParseSdl(false, "tag false;", [Tag("tag", "null")]); 1836 testParseSdl(false, "tag false{}", [Tag("tag", "null")]); 1837 1838 testParseSdl(false, "namespace:true", [Tag("namespace:true")]); 1839 testParseSdl(false, ":true", [Tag("true")]); 1840 testParseSdl(false, "true", [Tag("content", "true")]); 1841 testParseSdl(false, "tag\\", [Tag("tag")]); 1842 testParseSdl(false, "tag/*comment*/null", [Tag("tag", "null")]); 1843 testParseSdl(false, "crazy--tag", [Tag("crazy--tag")]); 1844 testParseSdl(false, "tag# comment", [Tag("tag")]); 1845 testParseSdl(false, "tag// comment", [Tag("tag")]); 1846 testParseSdl(false, "a=what", [Tag("content", "a=what")]); 1847 1848 testParseSdl(false, "tag {\nattr=value//\n}", [Tag("tag"), Tag("content", "attr=value")]); 1849 testParseSdl(false, "tag {\nattr=value}", [Tag("tag"), Tag("content", "attr=value")]); 1850 1851 1852 testInvalidSdl(false, "tag what/huh"); 1853 testInvalidSdl(false, `tag"value"`); 1854 testInvalidSdl(false, "attr:123"); 1855 testInvalidSdl(false, "attr:\"what\""); 1856 testInvalidSdl(false, "attr:=what"); 1857 testInvalidSdl(false, "attr:=null"); 1858 testInvalidSdl(false, "attr:=345"); 1859 testInvalidSdl(false, "name:tag:weird"); 1860 testInvalidSdl(false, "tag namespace:what"); 1861 testInvalidSdl(false, "tag namespace:\"value\""); 1862 testInvalidSdl(false, "tag namespace:null"); 1863 testInvalidSdl(false, "tag namespace:true"); 1864 testInvalidSdl(false, "tag^"); 1865 testInvalidSdl(false, "tag<"); 1866 testInvalidSdl(false, "tag>"); 1867 1868 1869 1870 1871 1872 // TODO: testing using all keywords as namespaces true:id, etc. 1873 testParseSdl(false, "tag null:null=\"value\";", [Tag("tag", "null:null=\"value\"")]); 1874 testParseSdl(false, "null", [Tag("content", "null")]); 1875 1876 1877 1878 // 1879 // Full Parses 1880 // 1881 testParseSdl(false, ` 1882 name "joe" 1883 children { 1884 name "jim" 1885 }`, [Tag("name", `"joe"`), Tag("children"), Tag("name", `"jim"`)]); 1886 1887 testParseSdl(false, ` 1888 parent name="jim" { 1889 child "hasToys" name="joey" { 1890 # just a comment here for now 1891 } 1892 }`, [Tag("parent", "name=\"jim\""), Tag("child", "name=\"joey\"", `"hasToys"`)]); 1893 1894 1895 testParseSdl(false,`td 34 1896 html:td "Puggy" 1897 `, [Tag("td", `34`), 1898 Tag("html:td", `"Puggy"`)]); 1899 1900 1901 testParseSdl(false,`html:table { 1902 html:tr { 1903 html:th "Name" 1904 html:th "Age" 1905 html:th "Pet" 1906 } 1907 html:tr { 1908 html:td "Brian" 1909 html:td 34 1910 html:td "Puggy" 1911 html:td null 1912 html:td false 1913 } 1914 tr { 1915 td "Jackie" 1916 td 27 1917 td null 1918 } 1919 }`, [Tag("html:table"), 1920 Tag("html:tr"), 1921 Tag("html:th", `"Name"`), 1922 Tag("html:th", `"Age"`), 1923 Tag("html:th", `"Pet"`), 1924 Tag("html:tr"), 1925 Tag("html:td", `"Brian"`), 1926 Tag("html:td", `34`), 1927 Tag("html:td", `"Puggy"`), 1928 Tag("html:td", `null`), 1929 Tag("html:td", `false`), 1930 Tag("tr"), 1931 Tag("td", `"Jackie"`), 1932 Tag("td", `27`), 1933 Tag("td", `null`)]); 1934 } 1935 1936 /// Assists in walking an SDL tree which supports the StAX method of parsing. 1937 /// Examples: 1938 /// --- 1939 /// Tag tag; 1940 /// SdlWalker walker = SdlWalker(&tag, sdl); 1941 /// while(walker.pop()) { 1942 /// // use tag to process the current tag 1943 /// 1944 /// auto depth = tag.childrenDepth(); 1945 /// while(walker.pop(depth)) { 1946 /// // process tag again as a child tag 1947 /// } 1948 /// 1949 /// } 1950 /// --- 1951 struct SdlWalker 1952 { 1953 /// A pointer to the tag structure that will be populated after parsing every tag. 1954 Tag* tag; 1955 1956 // The sdl text that has yet to be parsed. 1957 private char[] sdl; 1958 1959 // Used for when a child walker has popped a parent tag 1960 bool tagAlreadyPopped; 1961 1962 this(Tag* tag, char[] sdl) { 1963 this.tag = tag; 1964 this.sdl = sdl; 1965 } 1966 1967 /// Parses the next tag at the given depth. 1968 /// Returns: true if it parsed a tag at the given depth and false if there are no more 1969 /// tags at the given depth. If depth is 0 it means the sdl has been fully parsed. 1970 /// Throws: Exception if the current tag has children and they were not parsed 1971 /// and allowSkipChildren is set to false. 1972 bool pop(size_t depth = 0, bool allowSkipChildren = false) { 1973 if(tagAlreadyPopped) { 1974 if(depth < tag.depth) throw new Exception("possible code bug here?"); 1975 if(tag.depth == depth) { 1976 tagAlreadyPopped = false; 1977 return true; 1978 } 1979 } 1980 1981 while(true) { 1982 size_t previousDepth; 1983 const(char)[] previousName; 1984 1985 if(!allowSkipChildren) { 1986 previousDepth = tag.depth; 1987 previousName = tag.name; 1988 } 1989 1990 if(!parseSdlTag(this.tag, &sdl)) { 1991 assert(tag.depth == 0, format("code bug: parseSdlTag returned end of input but tag.depth was %s (not 0)", tag.depth)); 1992 return false; 1993 } 1994 1995 if(this.tag.depth == depth) return true; 1996 1997 // Check if it is the end of this set of children 1998 if(this.tag.depth < depth) { 1999 tagAlreadyPopped = true; 2000 return false; 2001 } 2002 2003 if(!allowSkipChildren) throw new Exception(format("forgot to call children on tag '%s' at depth %s", previousName, previousDepth)); 2004 } 2005 } 2006 2007 public size_t childrenDepth() { return tag.depth + 1; } 2008 } 2009 2010 version(unittest_sdl) 2011 { 2012 struct Dependency { 2013 string name; 2014 string version_; 2015 } 2016 // Example of parsing a configuration file 2017 struct Package { 2018 const(char)[] name; 2019 const(char)[] description; 2020 2021 const(char)[][] authors; 2022 auto dependencies = appender!(Dependency[])(); 2023 auto subPackages = appender!(Package[])(); 2024 2025 void reset() { 2026 name = null; 2027 description = null; 2028 authors = null; 2029 dependencies.clear(); 2030 subPackages.clear(); 2031 } 2032 bool opEquals(ref const Package p) { 2033 return 2034 name == p.name && 2035 description == p.description && 2036 authors == p.authors && 2037 dependencies.data == p.dependencies.data && 2038 subPackages.data == p.subPackages.data; 2039 } 2040 void parseSdlPackage(bool copySdl, string sdlText) { 2041 parseSdlPackage(setupSdlText(sdlText, copySdl)); 2042 } 2043 void parseSdlPackage(char[] sdlText) { 2044 Tag tag; 2045 auto sdl = SdlWalker(&tag, sdlText); 2046 while(sdl.pop()) { 2047 2048 debug writefln("[sdl] (depth %s) tag '%s'%s", tag.depth, tag.name, 2049 tag.hasOpenBrace ? "(has children)" : ""); 2050 2051 if(tag.name == "name") { 2052 2053 tag.enforceNoAttributes(); 2054 tag.enforceNoChildren(); 2055 tag.getOneValue(this.name); 2056 2057 } else if(tag.name == "description") { 2058 2059 tag.enforceNoAttributes(); 2060 tag.enforceNoChildren(); 2061 tag.getOneValue(this.description); 2062 2063 } else if(tag.name == "authors") { 2064 2065 if(this.authors !is null) tag.throwIsDuplicate(); 2066 tag.enforceNoAttributes(); 2067 tag.enforceNoChildren(); 2068 tag.getValues(this.authors); 2069 2070 } else tag.throwIsUnknown(); 2071 2072 } 2073 2074 } 2075 } 2076 } 2077 2078 2079 version(unittest_sdl) unittest 2080 { 2081 mixin(scopedTest!"SdlWalker"); 2082 2083 void testPackage(bool copySdl, string sdlText, ref Package expectedPackage) 2084 { 2085 Package parsedPackage; 2086 2087 parsedPackage.parseSdlPackage(copySdl, sdlText); 2088 2089 if(expectedPackage != parsedPackage) { 2090 writefln("Expected package: %s", expectedPackage); 2091 writefln(" but got package: %s", parsedPackage); 2092 assert(0); 2093 } 2094 } 2095 2096 string sdl; 2097 Package expectedPackage; 2098 2099 expectedPackage = Package("my-package", "an example sdl package", 2100 ["Jonathan", "David", "Amy"]); 2101 2102 testPackage(false, ` 2103 name "my-package" 2104 description "an example sdl package" 2105 authors "Jonathan" "David" "Amy" 2106 `, expectedPackage); 2107 } 2108 2109 version(unittest_sdl) unittest 2110 { 2111 mixin(scopedTest!"SdlWalkerOnPerson"); 2112 2113 struct Person { 2114 const(char)[] name; 2115 ushort age; 2116 const(char)[][] nicknames; 2117 Person[] children; 2118 void reset() { 2119 name = null; 2120 age = 0; 2121 nicknames = null; 2122 children.clear(); 2123 } 2124 bool opEquals(ref const Person p) { 2125 return 2126 name == p.name && 2127 age == p.age && 2128 nicknames == p.nicknames && 2129 children == p.children; 2130 } 2131 string toString() { 2132 return format("Person(\"%s\", %s, %s, %s)", name, age, nicknames, children); 2133 } 2134 void validate() { 2135 if(name is null) throw new Exception("person is missing the 'name' tag"); 2136 if(age == 0) throw new Exception("person is missing the 'age' tag"); 2137 } 2138 void parseFromSdl(ref SdlWalker walker) { 2139 auto tag = walker.tag; 2140 2141 tag.enforceNoValues(); 2142 tag.enforceNoAttributes(); 2143 2144 reset(); 2145 2146 auto childBuilder = appender!(Person[])(); 2147 2148 auto depth = walker.childrenDepth(); 2149 while(walker.pop(depth)) { 2150 2151 //writefln("[sdl] (depth %s) tag '%s'%s", tag.depth, tag.name, 2152 //tag.hasOpenBrace ? "(has children)" : ""); 2153 //stdout.flush(); 2154 2155 if(tag.name == "name") { 2156 2157 tag.enforceNoAttributes(); 2158 tag.enforceNoChildren(); 2159 tag.getOneValue(name); 2160 2161 } else if(tag.name == "age") { 2162 2163 tag.enforceNoAttributes(); 2164 tag.enforceNoChildren(); 2165 tag.getOneValue(age); 2166 2167 } else if(tag.name == "nicknames") { 2168 2169 tag.enforceNoAttributes(); 2170 tag.enforceNoChildren(); 2171 tag.getValues(nicknames); 2172 2173 } else if(tag.name == "child") { 2174 2175 Person child = Person(); 2176 child.parseFromSdl(walker); 2177 childBuilder.put(child); 2178 2179 } else tag.throwIsUnknown(); 2180 2181 } 2182 2183 this.children = childBuilder.data.dup; 2184 childBuilder.clear(); 2185 validate(); 2186 } 2187 } 2188 2189 Appender!(Person[]) parsePeople(char[] sdl) { 2190 auto people = appender!(Person[])(); 2191 Person person; 2192 2193 Tag tag; 2194 auto walker = SdlWalker(&tag, sdl); 2195 while(walker.pop()) { 2196 if(tag.name == "person") { 2197 2198 person.parseFromSdl(walker); 2199 people.put(person); 2200 2201 } else tag.throwIsUnknown(); 2202 } 2203 2204 return people; 2205 } 2206 2207 void testParsePeople(bool copySdl, string sdlText, Person[] expectedPeople...) 2208 { 2209 Appender!(Person[]) parsedPeople; 2210 try { 2211 2212 parsedPeople = parsePeople(setupSdlText(sdlText, copySdl)); 2213 2214 } catch(Exception e) { 2215 writefln("the following sdl threw an unexpected exception: %s", sdlText); 2216 writeln(e); 2217 assert(0); 2218 } 2219 2220 if(expectedPeople.length != parsedPeople.data.length) { 2221 writefln("Expected: %s", expectedPeople); 2222 writefln(" but got: %s", parsedPeople.data); 2223 assert(0); 2224 } 2225 for(auto i = 0; i < expectedPeople.length; i++) { 2226 Person expectedPerson = expectedPeople[i]; 2227 if(expectedPerson != parsedPeople.data[i]) { 2228 writefln("Expected: %s", expectedPeople); 2229 writefln(" but got: %s", parsedPeople.data); 2230 assert(0); 2231 } 2232 } 2233 2234 } 2235 2236 testParsePeople(false, ` 2237 person { 2238 name "Robert" 2239 age 29 2240 nicknames "Bob" "Bobby" 2241 child { 2242 name "Jack" 2243 age 6 2244 nicknames "Little Jack" 2245 } 2246 child { 2247 name "Sally" 2248 age 8 2249 } 2250 }`, Person("Robert", 29, ["Bob", "Bobby"], [Person("Jack", 6, ["Little Jack"]),Person("Sally", 8)])); 2251 2252 } 2253