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