From 8ab7e935913c102fb039110e20b71f698a68c6ee Mon Sep 17 00:00:00 2001 From: Ralph Amissah Date: Thu, 16 Jun 2016 01:49:06 -0400 Subject: step5 sdlang used for config files and doc headers --- src/sdlang/ast.d | 1834 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1834 insertions(+) create mode 100644 src/sdlang/ast.d (limited to 'src/sdlang/ast.d') diff --git a/src/sdlang/ast.d b/src/sdlang/ast.d new file mode 100644 index 0000000..7ad1c30 --- /dev/null +++ b/src/sdlang/ast.d @@ -0,0 +1,1834 @@ +// SDLang-D +// Written in the D programming language. + +module sdlang.ast; + +import std.algorithm; +import std.array; +import std.conv; +import std.range; +import std.string; + +version(sdlangUnittest) +version(unittest) +{ + import std.stdio; + import std.exception; +} + +import sdlang.exception; +import sdlang.token; +import sdlang.util; + +class Attribute +{ + Value value; + Location location; + + private Tag _parent; + /// Get parent tag. To set a parent, attach this Attribute to its intended + /// parent tag by calling 'Tag.add(...)', or by passing it to + /// the parent tag's constructor. + @property Tag parent() + { + return _parent; + } + + private string _namespace; + @property string namespace() + { + return _namespace; + } + /// Not particularly efficient, but it works. + @property void namespace(string value) + { + if(_parent && _namespace != value) + { + // Remove + auto saveParent = _parent; + if(_parent) + this.remove(); + + // Change namespace + _namespace = value; + + // Re-add + if(saveParent) + saveParent.add(this); + } + else + _namespace = value; + } + + private string _name; + /// Not including namespace. Use 'fullName' if you want the namespace included. + @property string name() + { + return _name; + } + /// Not the most efficient, but it works. + @property void name(string value) + { + if(_parent && _name != value) + { + _parent.updateId++; + + void removeFromGroupedLookup(string ns) + { + // Remove from _parent._attributes[ns] + auto sameNameAttrs = _parent._attributes[ns][_name]; + auto targetIndex = sameNameAttrs.countUntil(this); + _parent._attributes[ns][_name].removeIndex(targetIndex); + } + + // Remove from _parent._tags + removeFromGroupedLookup(_namespace); + removeFromGroupedLookup("*"); + + // Change name + _name = value; + + // Add to new locations in _parent._attributes + _parent._attributes[_namespace][_name] ~= this; + _parent._attributes["*"][_name] ~= this; + } + else + _name = value; + } + + @property string fullName() + { + return _namespace==""? _name : text(_namespace, ":", _name); + } + + this(string namespace, string name, Value value, Location location = Location(0, 0, 0)) + { + this._namespace = namespace; + this._name = name; + this.location = location; + this.value = value; + } + + this(string name, Value value, Location location = Location(0, 0, 0)) + { + this._namespace = ""; + this._name = name; + this.location = location; + this.value = value; + } + + /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// Inefficient ATM, but it works. + Attribute remove() + { + if(!_parent) + return this; + + void removeFromGroupedLookup(string ns) + { + // Remove from _parent._attributes[ns] + auto sameNameAttrs = _parent._attributes[ns][_name]; + auto targetIndex = sameNameAttrs.countUntil(this); + _parent._attributes[ns][_name].removeIndex(targetIndex); + } + + // Remove from _parent._attributes + removeFromGroupedLookup(_namespace); + removeFromGroupedLookup("*"); + + // Remove from _parent.allAttributes + auto allAttrsIndex = _parent.allAttributes.countUntil(this); + _parent.allAttributes.removeIndex(allAttrsIndex); + + // Remove from _parent.attributeIndicies + auto sameNamespaceAttrs = _parent.attributeIndicies[_namespace]; + auto attrIndiciesIndex = sameNamespaceAttrs.countUntil(allAttrsIndex); + _parent.attributeIndicies[_namespace].removeIndex(attrIndiciesIndex); + + // Fixup other indicies + foreach(ns, ref nsAttrIndicies; _parent.attributeIndicies) + foreach(k, ref v; nsAttrIndicies) + if(v > allAttrsIndex) + v--; + + _parent.removeNamespaceIfEmpty(_namespace); + _parent.updateId++; + _parent = null; + return this; + } + + override bool opEquals(Object o) + { + auto a = cast(Attribute)o; + if(!a) + return false; + + return + _namespace == a._namespace && + _name == a._name && + value == a.value; + } + + string toSDLString()() + { + Appender!string sink; + this.toSDLString(sink); + return sink.data; + } + + void toSDLString(Sink)(ref Sink sink) if(isOutputRange!(Sink,char)) + { + if(_namespace != "") + { + sink.put(_namespace); + sink.put(':'); + } + + sink.put(_name); + sink.put('='); + value.toSDLString(sink); + } +} + +class Tag +{ + Location location; + Value[] values; + + private Tag _parent; + /// Get parent tag. To set a parent, attach this Tag to its intended + /// parent tag by calling 'Tag.add(...)', or by passing it to + /// the parent tag's constructor. + @property Tag parent() + { + return _parent; + } + + private string _namespace; + @property string namespace() + { + return _namespace; + } + /// Not particularly efficient, but it works. + @property void namespace(string value) + { + if(_parent && _namespace != value) + { + // Remove + auto saveParent = _parent; + if(_parent) + this.remove(); + + // Change namespace + _namespace = value; + + // Re-add + if(saveParent) + saveParent.add(this); + } + else + _namespace = value; + } + + private string _name; + /// Not including namespace. Use 'fullName' if you want the namespace included. + @property string name() + { + return _name; + } + /// Not the most efficient, but it works. + @property void name(string value) + { + if(_parent && _name != value) + { + _parent.updateId++; + + void removeFromGroupedLookup(string ns) + { + // Remove from _parent._tags[ns] + auto sameNameTags = _parent._tags[ns][_name]; + auto targetIndex = sameNameTags.countUntil(this); + _parent._tags[ns][_name].removeIndex(targetIndex); + } + + // Remove from _parent._tags + removeFromGroupedLookup(_namespace); + removeFromGroupedLookup("*"); + + // Change name + _name = value; + + // Add to new locations in _parent._tags + _parent._tags[_namespace][_name] ~= this; + _parent._tags["*"][_name] ~= this; + } + else + _name = value; + } + + /// This tag's name, including namespace if one exists. + @property string fullName() + { + return _namespace==""? _name : text(_namespace, ":", _name); + } + + // Tracks dirtiness. This is incremented every time a change is made which + // could invalidate existing ranges. This way, the ranges can detect when + // they've been invalidated. + private size_t updateId=0; + + this(Tag parent = null) + { + if(parent) + parent.add(this); + } + + this( + string namespace, string name, + Value[] values=null, Attribute[] attributes=null, Tag[] children=null + ) + { + this(null, namespace, name, values, attributes, children); + } + + this( + Tag parent, string namespace, string name, + Value[] values=null, Attribute[] attributes=null, Tag[] children=null + ) + { + this._namespace = namespace; + this._name = name; + + if(parent) + parent.add(this); + + this.values = values; + this.add(attributes); + this.add(children); + } + + private Attribute[] allAttributes; // In same order as specified in SDL file. + private Tag[] allTags; // In same order as specified in SDL file. + private string[] allNamespaces; // In same order as specified in SDL file. + + private size_t[][string] attributeIndicies; // allAttributes[ attributes[namespace][i] ] + private size_t[][string] tagIndicies; // allTags[ tags[namespace][i] ] + + private Attribute[][string][string] _attributes; // attributes[namespace or "*"][name][i] + private Tag[][string][string] _tags; // tags[namespace or "*"][name][i] + + /// Adds a Value, Attribute, Tag (or array of such) as a member/child of this Tag. + /// Returns 'this' for chaining. + /// Throws 'SDLangValidationException' if trying to add an Attribute or Tag + /// that already has a parent. + Tag add(Value val) + { + values ~= val; + updateId++; + return this; + } + + ///ditto + Tag add(Value[] vals) + { + foreach(val; vals) + add(val); + + return this; + } + + ///ditto + Tag add(Attribute attr) + { + if(attr._parent) + { + throw new SDLangValidationException( + "Attribute is already attached to a parent tag. "~ + "Use Attribute.remove() before adding it to another tag." + ); + } + + if(!allNamespaces.canFind(attr._namespace)) + allNamespaces ~= attr._namespace; + + attr._parent = this; + + allAttributes ~= attr; + attributeIndicies[attr._namespace] ~= allAttributes.length-1; + _attributes[attr._namespace][attr._name] ~= attr; + _attributes["*"] [attr._name] ~= attr; + + updateId++; + return this; + } + + ///ditto + Tag add(Attribute[] attrs) + { + foreach(attr; attrs) + add(attr); + + return this; + } + + ///ditto + Tag add(Tag tag) + { + if(tag._parent) + { + throw new SDLangValidationException( + "Tag is already attached to a parent tag. "~ + "Use Tag.remove() before adding it to another tag." + ); + } + + if(!allNamespaces.canFind(tag._namespace)) + allNamespaces ~= tag._namespace; + + tag._parent = this; + + allTags ~= tag; + tagIndicies[tag._namespace] ~= allTags.length-1; + _tags[tag._namespace][tag._name] ~= tag; + _tags["*"] [tag._name] ~= tag; + + updateId++; + return this; + } + + ///ditto + Tag add(Tag[] tags) + { + foreach(tag; tags) + add(tag); + + return this; + } + + /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// Inefficient ATM, but it works. + Tag remove() + { + if(!_parent) + return this; + + void removeFromGroupedLookup(string ns) + { + // Remove from _parent._tags[ns] + auto sameNameTags = _parent._tags[ns][_name]; + auto targetIndex = sameNameTags.countUntil(this); + _parent._tags[ns][_name].removeIndex(targetIndex); + } + + // Remove from _parent._tags + removeFromGroupedLookup(_namespace); + removeFromGroupedLookup("*"); + + // Remove from _parent.allTags + auto allTagsIndex = _parent.allTags.countUntil(this); + _parent.allTags.removeIndex(allTagsIndex); + + // Remove from _parent.tagIndicies + auto sameNamespaceTags = _parent.tagIndicies[_namespace]; + auto tagIndiciesIndex = sameNamespaceTags.countUntil(allTagsIndex); + _parent.tagIndicies[_namespace].removeIndex(tagIndiciesIndex); + + // Fixup other indicies + foreach(ns, ref nsTagIndicies; _parent.tagIndicies) + foreach(k, ref v; nsTagIndicies) + if(v > allTagsIndex) + v--; + + _parent.removeNamespaceIfEmpty(_namespace); + _parent.updateId++; + _parent = null; + return this; + } + + private void removeNamespaceIfEmpty(string namespace) + { + // If namespace has no attributes, remove it from attributeIndicies/_attributes + if(namespace in attributeIndicies && attributeIndicies[namespace].length == 0) + { + attributeIndicies.remove(namespace); + _attributes.remove(namespace); + } + + // If namespace has no tags, remove it from tagIndicies/_tags + if(namespace in tagIndicies && tagIndicies[namespace].length == 0) + { + tagIndicies.remove(namespace); + _tags.remove(namespace); + } + + // If namespace is now empty, remove it from allNamespaces + if( + namespace !in tagIndicies && + namespace !in attributeIndicies + ) + { + auto allNamespacesIndex = allNamespaces.length - allNamespaces.find(namespace).length; + allNamespaces = allNamespaces[0..allNamespacesIndex] ~ allNamespaces[allNamespacesIndex+1..$]; + } + } + + struct NamedMemberRange(T, string membersGrouped) + { + private Tag tag; + private string namespace; // "*" indicates "all namespaces" (ok since it's not a valid namespace name) + private string name; + private size_t updateId; // Tag's updateId when this range was created. + + this(Tag tag, string namespace, string name, size_t updateId) + { + this.tag = tag; + this.namespace = namespace; + this.name = name; + this.updateId = updateId; + frontIndex = 0; + + if( + namespace in mixin("tag."~membersGrouped) && + name in mixin("tag."~membersGrouped~"[namespace]") + ) + endIndex = mixin("tag."~membersGrouped~"[namespace][name].length"); + else + endIndex = 0; + } + + invariant() + { + assert( + this.updateId == tag.updateId, + "This range has been invalidated by a change to the tag." + ); + } + + @property bool empty() + { + return frontIndex == endIndex; + } + + private size_t frontIndex; + @property T front() + { + return this[0]; + } + void popFront() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + frontIndex++; + } + + private size_t endIndex; // One past the last element + @property T back() + { + return this[$-1]; + } + void popBack() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + endIndex--; + } + + alias length opDollar; + @property size_t length() + { + return endIndex - frontIndex; + } + + @property typeof(this) save() + { + auto r = typeof(this)(this.tag, this.namespace, this.name, this.updateId); + r.frontIndex = this.frontIndex; + r.endIndex = this.endIndex; + return r; + } + + typeof(this) opSlice() + { + return save(); + } + + typeof(this) opSlice(size_t start, size_t end) + { + auto r = save(); + r.frontIndex = this.frontIndex + start; + r.endIndex = this.frontIndex + end; + + if( + r.frontIndex > this.endIndex || + r.endIndex > this.endIndex || + r.frontIndex > r.endIndex + ) + throw new SDLangRangeException("Slice out of range"); + + return r; + } + + T opIndex(size_t index) + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + return mixin("tag."~membersGrouped~"[namespace][name][frontIndex+index]"); + } + } + + struct MemberRange(T, string allMembers, string memberIndicies, string membersGrouped) + { + private Tag tag; + private string namespace; // "*" indicates "all namespaces" (ok since it's not a valid namespace name) + private bool isMaybe; + private size_t updateId; // Tag's updateId when this range was created. + private size_t initialEndIndex; + + this(Tag tag, string namespace, bool isMaybe) + { + this.tag = tag; + this.namespace = namespace; + this.updateId = tag.updateId; + this.isMaybe = isMaybe; + frontIndex = 0; + + if(namespace == "*") + initialEndIndex = mixin("tag."~allMembers~".length"); + else if(namespace in mixin("tag."~memberIndicies)) + initialEndIndex = mixin("tag."~memberIndicies~"[namespace].length"); + else + initialEndIndex = 0; + + endIndex = initialEndIndex; + } + + invariant() + { + assert( + this.updateId == tag.updateId, + "This range has been invalidated by a change to the tag." + ); + } + + @property bool empty() + { + return frontIndex == endIndex; + } + + private size_t frontIndex; + @property T front() + { + return this[0]; + } + void popFront() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + frontIndex++; + } + + private size_t endIndex; // One past the last element + @property T back() + { + return this[$-1]; + } + void popBack() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + endIndex--; + } + + alias length opDollar; + @property size_t length() + { + return endIndex - frontIndex; + } + + @property typeof(this) save() + { + auto r = typeof(this)(this.tag, this.namespace, this.isMaybe); + r.frontIndex = this.frontIndex; + r.endIndex = this.endIndex; + r.initialEndIndex = this.initialEndIndex; + r.updateId = this.updateId; + return r; + } + + typeof(this) opSlice() + { + return save(); + } + + typeof(this) opSlice(size_t start, size_t end) + { + auto r = save(); + r.frontIndex = this.frontIndex + start; + r.endIndex = this.frontIndex + end; + + if( + r.frontIndex > this.endIndex || + r.endIndex > this.endIndex || + r.frontIndex > r.endIndex + ) + throw new SDLangRangeException("Slice out of range"); + + return r; + } + + T opIndex(size_t index) + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + if(namespace == "*") + return mixin("tag."~allMembers~"[ frontIndex+index ]"); + else + return mixin("tag."~allMembers~"[ tag."~memberIndicies~"[namespace][frontIndex+index] ]"); + } + + alias NamedMemberRange!(T,membersGrouped) ThisNamedMemberRange; + ThisNamedMemberRange opIndex(string name) + { + if(frontIndex != 0 || endIndex != initialEndIndex) + { + throw new SDLangRangeException( + "Cannot lookup tags/attributes by name on a subset of a range, "~ + "only across the entire tag. "~ + "Please make sure you haven't called popFront or popBack on this "~ + "range and that you aren't using a slice of the range." + ); + } + + if(!isMaybe && empty) + throw new SDLangRangeException("Range is empty"); + + if(!isMaybe && name !in this) + throw new SDLangRangeException(`No such `~T.stringof~` named: "`~name~`"`); + + return ThisNamedMemberRange(tag, namespace, name, updateId); + } + + bool opBinaryRight(string op)(string name) if(op=="in") + { + if(frontIndex != 0 || endIndex != initialEndIndex) + { + throw new SDLangRangeException( + "Cannot lookup tags/attributes by name on a subset of a range, "~ + "only across the entire tag. "~ + "Please make sure you haven't called popFront or popBack on this "~ + "range and that you aren't using a slice of the range." + ); + } + + return + namespace in mixin("tag."~membersGrouped) && + name in mixin("tag."~membersGrouped~"[namespace]") && + mixin("tag."~membersGrouped~"[namespace][name].length") > 0; + } + } + + struct NamespaceRange + { + private Tag tag; + private bool isMaybe; + private size_t updateId; // Tag's updateId when this range was created. + + this(Tag tag, bool isMaybe) + { + this.tag = tag; + this.isMaybe = isMaybe; + this.updateId = tag.updateId; + frontIndex = 0; + endIndex = tag.allNamespaces.length; + } + + invariant() + { + assert( + this.updateId == tag.updateId, + "This range has been invalidated by a change to the tag." + ); + } + + @property bool empty() + { + return frontIndex == endIndex; + } + + private size_t frontIndex; + @property NamespaceAccess front() + { + return this[0]; + } + void popFront() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + frontIndex++; + } + + private size_t endIndex; // One past the last element + @property NamespaceAccess back() + { + return this[$-1]; + } + void popBack() + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + endIndex--; + } + + alias length opDollar; + @property size_t length() + { + return endIndex - frontIndex; + } + + @property NamespaceRange save() + { + auto r = NamespaceRange(this.tag, this.isMaybe); + r.frontIndex = this.frontIndex; + r.endIndex = this.endIndex; + r.updateId = this.updateId; + return r; + } + + typeof(this) opSlice() + { + return save(); + } + + typeof(this) opSlice(size_t start, size_t end) + { + auto r = save(); + r.frontIndex = this.frontIndex + start; + r.endIndex = this.frontIndex + end; + + if( + r.frontIndex > this.endIndex || + r.endIndex > this.endIndex || + r.frontIndex > r.endIndex + ) + throw new SDLangRangeException("Slice out of range"); + + return r; + } + + NamespaceAccess opIndex(size_t index) + { + if(empty) + throw new SDLangRangeException("Range is empty"); + + auto namespace = tag.allNamespaces[frontIndex+index]; + return NamespaceAccess( + namespace, + AttributeRange(tag, namespace, isMaybe), + TagRange(tag, namespace, isMaybe) + ); + } + + NamespaceAccess opIndex(string namespace) + { + if(!isMaybe && empty) + throw new SDLangRangeException("Range is empty"); + + if(!isMaybe && namespace !in this) + throw new SDLangRangeException(`No such namespace: "`~namespace~`"`); + + return NamespaceAccess( + namespace, + AttributeRange(tag, namespace, isMaybe), + TagRange(tag, namespace, isMaybe) + ); + } + + /// Inefficient when range is a slice or has used popFront/popBack, but it works. + bool opBinaryRight(string op)(string namespace) if(op=="in") + { + if(frontIndex == 0 && endIndex == tag.allNamespaces.length) + { + return + namespace in tag.attributeIndicies || + namespace in tag.tagIndicies; + } + else + // Slower fallback method + return tag.allNamespaces[frontIndex..endIndex].canFind(namespace); + } + } + + struct NamespaceAccess + { + string name; + AttributeRange attributes; + TagRange tags; + } + + alias MemberRange!(Attribute, "allAttributes", "attributeIndicies", "_attributes") AttributeRange; + alias MemberRange!(Tag, "allTags", "tagIndicies", "_tags" ) TagRange; + static assert(isRandomAccessRange!AttributeRange); + static assert(isRandomAccessRange!TagRange); + static assert(isRandomAccessRange!NamespaceRange); + + /// Access all attributes that don't have a namespace + @property AttributeRange attributes() + { + return AttributeRange(this, "", false); + } + + /// Access all direct-child tags that don't have a namespace + @property TagRange tags() + { + return TagRange(this, "", false); + } + + /// Access all namespaces in this tag, and the attributes/tags within them. + @property NamespaceRange namespaces() + { + return NamespaceRange(this, false); + } + + /// Access all attributes and tags regardless of namespace. + @property NamespaceAccess all() + { + // "*" isn't a valid namespace name, so we can use it to indicate "all namespaces" + return NamespaceAccess( + "*", + AttributeRange(this, "*", false), + TagRange(this, "*", false) + ); + } + + struct MaybeAccess + { + Tag tag; + + /// Access all attributes that don't have a namespace + @property AttributeRange attributes() + { + return AttributeRange(tag, "", true); + } + + /// Access all direct-child tags that don't have a namespace + @property TagRange tags() + { + return TagRange(tag, "", true); + } + + /// Access all namespaces in this tag, and the attributes/tags within them. + @property NamespaceRange namespaces() + { + return NamespaceRange(tag, true); + } + + /// Access all attributes and tags regardless of namespace. + @property NamespaceAccess all() + { + // "*" isn't a valid namespace name, so we can use it to indicate "all namespaces" + return NamespaceAccess( + "*", + AttributeRange(tag, "*", true), + TagRange(tag, "*", true) + ); + } + } + + /// Access 'attributes', 'tags', 'namespaces' and 'all' like normal, + /// except that looking up a non-existant name/namespace with + /// opIndex(string) results in an empty array instead of a thrown SDLangRangeException. + @property MaybeAccess maybe() + { + return MaybeAccess(this); + } + + override bool opEquals(Object o) + { + auto t = cast(Tag)o; + if(!t) + return false; + + if(_namespace != t._namespace || _name != t._name) + return false; + + if( + values .length != t.values .length || + allAttributes .length != t.allAttributes.length || + allNamespaces .length != t.allNamespaces.length || + allTags .length != t.allTags .length + ) + return false; + + if(values != t.values) + return false; + + if(allNamespaces != t.allNamespaces) + return false; + + if(allAttributes != t.allAttributes) + return false; + + // Ok because cycles are not allowed + //TODO: Actually check for or prevent cycles. + return allTags == t.allTags; + } + + /// Treats 'this' as the root tag. Note that root tags cannot have + /// values or attributes, and cannot be part of a namespace. + /// If this isn't a valid root tag, 'SDLangValidationException' will be thrown. + string toSDLDocument()(string indent="\t", int indentLevel=0) + { + Appender!string sink; + toSDLDocument(sink, indent, indentLevel); + return sink.data; + } + + ///ditto + void toSDLDocument(Sink)(ref Sink sink, string indent="\t", int indentLevel=0) + if(isOutputRange!(Sink,char)) + { + if(values.length > 0) + throw new SDLangValidationException("Root tags cannot have any values, only child tags."); + + if(allAttributes.length > 0) + throw new SDLangValidationException("Root tags cannot have any attributes, only child tags."); + + if(_namespace != "") + throw new SDLangValidationException("Root tags cannot have a namespace."); + + foreach(tag; allTags) + tag.toSDLString(sink, indent, indentLevel); + } + + /// Output this entire tag in SDL format. Does *not* treat 'this' as + /// a root tag. If you intend this to be the root of a standard SDL + /// document, use 'toSDLDocument' instead. + string toSDLString()(string indent="\t", int indentLevel=0) + { + Appender!string sink; + toSDLString(sink, indent, indentLevel); + return sink.data; + } + + ///ditto + void toSDLString(Sink)(ref Sink sink, string indent="\t", int indentLevel=0) + if(isOutputRange!(Sink,char)) + { + if(_name == "" && values.length == 0) + throw new SDLangValidationException("Anonymous tags must have at least one value."); + + if(_name == "" && _namespace != "") + throw new SDLangValidationException("Anonymous tags cannot have a namespace."); + + // Indent + foreach(i; 0..indentLevel) + sink.put(indent); + + // Name + if(_namespace != "") + { + sink.put(_namespace); + sink.put(':'); + } + sink.put(_name); + + // Values + foreach(i, v; values) + { + // Omit the first space for anonymous tags + if(_name != "" || i > 0) + sink.put(' '); + + v.toSDLString(sink); + } + + // Attributes + foreach(attr; allAttributes) + { + sink.put(' '); + attr.toSDLString(sink); + } + + // Child tags + bool foundChild=false; + foreach(tag; allTags) + { + if(!foundChild) + { + sink.put(" {\n"); + foundChild = true; + } + + tag.toSDLString(sink, indent, indentLevel+1); + } + if(foundChild) + { + foreach(i; 0..indentLevel) + sink.put(indent); + + sink.put("}\n"); + } + else + sink.put("\n"); + } + + /// Not the most efficient, but it works. + string toDebugString() + { + import std.algorithm : sort; + + Appender!string buf; + + buf.put("\n"); + buf.put("Tag "); + if(_namespace != "") + { + buf.put("["); + buf.put(_namespace); + buf.put("]"); + } + buf.put("'%s':\n".format(_name)); + + // Values + foreach(val; values) + buf.put(" (%s): %s\n".format(.toString(val.type), val)); + + // Attributes + foreach(attrNamespace; _attributes.keys.sort()) + if(attrNamespace != "*") + foreach(attrName; _attributes[attrNamespace].keys.sort()) + foreach(attr; _attributes[attrNamespace][attrName]) + { + string namespaceStr; + if(attr._namespace != "") + namespaceStr = "["~attr._namespace~"]"; + + buf.put( + " %s%s(%s): %s\n".format( + namespaceStr, attr._name, .toString(attr.value.type), attr.value + ) + ); + } + + // Children + foreach(tagNamespace; _tags.keys.sort()) + if(tagNamespace != "*") + foreach(tagName; _tags[tagNamespace].keys.sort()) + foreach(tag; _tags[tagNamespace][tagName]) + buf.put( tag.toDebugString().replace("\n", "\n ") ); + + return buf.data; + } +} + +version(sdlangUnittest) +{ + private void testRandomAccessRange(R, E)(R range, E[] expected, bool function(E, E) equals=null) + { + static assert(isRandomAccessRange!R); + static assert(is(ElementType!R == E)); + static assert(hasLength!R); + static assert(!isInfinite!R); + + assert(range.length == expected.length); + if(range.length == 0) + { + assert(range.empty); + return; + } + + static bool defaultEquals(E e1, E e2) + { + return e1 == e2; + } + if(equals is null) + equals = &defaultEquals; + + assert(equals(range.front, expected[0])); + assert(equals(range.front, expected[0])); // Ensure consistent result from '.front' + assert(equals(range.front, expected[0])); // Ensure consistent result from '.front' + + assert(equals(range.back, expected[$-1])); + assert(equals(range.back, expected[$-1])); // Ensure consistent result from '.back' + assert(equals(range.back, expected[$-1])); // Ensure consistent result from '.back' + + // Forward iteration + auto original = range.save; + auto r2 = range.save; + foreach(i; 0..expected.length) + { + //trace("Forward iteration: ", i); + + // Test length/empty + assert(range.length == expected.length - i); + assert(range.length == r2.length); + assert(!range.empty); + assert(!r2.empty); + + // Test front + assert(equals(range.front, expected[i])); + assert(equals(range.front, r2.front)); + + // Test back + assert(equals(range.back, expected[$-1])); + assert(equals(range.back, r2.back)); + + // Test opIndex(0) + assert(equals(range[0], expected[i])); + assert(equals(range[0], r2[0])); + + // Test opIndex($-1) + assert(equals(range[$-1], expected[$-1])); + assert(equals(range[$-1], r2[$-1])); + + // Test popFront + range.popFront(); + assert(range.length == r2.length - 1); + r2.popFront(); + assert(range.length == r2.length); + } + assert(range.empty); + assert(r2.empty); + assert(original.length == expected.length); + + // Backwards iteration + range = original.save; + r2 = original.save; + foreach(i; iota(0, expected.length).retro()) + { + //trace("Backwards iteration: ", i); + + // Test length/empty + assert(range.length == i+1); + assert(range.length == r2.length); + assert(!range.empty); + assert(!r2.empty); + + // Test front + assert(equals(range.front, expected[0])); + assert(equals(range.front, r2.front)); + + // Test back + assert(equals(range.back, expected[i])); + assert(equals(range.back, r2.back)); + + // Test opIndex(0) + assert(equals(range[0], expected[0])); + assert(equals(range[0], r2[0])); + + // Test opIndex($-1) + assert(equals(range[$-1], expected[i])); + assert(equals(range[$-1], r2[$-1])); + + // Test popBack + range.popBack(); + assert(range.length == r2.length - 1); + r2.popBack(); + assert(range.length == r2.length); + } + assert(range.empty); + assert(r2.empty); + assert(original.length == expected.length); + + // Random access + range = original.save; + r2 = original.save; + foreach(i; 0..expected.length) + { + //trace("Random access: ", i); + + // Test length/empty + assert(range.length == expected.length); + assert(range.length == r2.length); + assert(!range.empty); + assert(!r2.empty); + + // Test front + assert(equals(range.front, expected[0])); + assert(equals(range.front, r2.front)); + + // Test back + assert(equals(range.back, expected[$-1])); + assert(equals(range.back, r2.back)); + + // Test opIndex(i) + assert(equals(range[i], expected[i])); + assert(equals(range[i], r2[i])); + } + assert(!range.empty); + assert(!r2.empty); + assert(original.length == expected.length); + } +} + +version(sdlangUnittest) +unittest +{ + import sdlang.parser; + writeln("Unittesting sdlang ast..."); + stdout.flush(); + + Tag root; + root = parseSource(""); + testRandomAccessRange(root.attributes, cast( Attribute[])[]); + testRandomAccessRange(root.tags, cast( Tag[])[]); + testRandomAccessRange(root.namespaces, cast(Tag.NamespaceAccess[])[]); + + root = parseSource(` + blue 3 "Lee" isThree=true + blue 5 "Chan" 12345 isThree=false + stuff:orange 1 2 3 2 1 + stuff:square points=4 dimensions=2 points="Still four" + stuff:triangle data:points=3 data:dimensions=2 + nothing + namespaces small:A=1 med:A=2 big:A=3 small:B=10 big:B=30 + + people visitor:a=1 b=2 { + chiyo "Small" "Flies?" nemesis="Car" score=100 + yukari + visitor:sana + tomo + visitor:hayama + } + `); + + auto blue3 = new Tag( + null, "", "blue", + [ Value(3), Value("Lee") ], + [ new Attribute("isThree", Value(true)) ], + null + ); + auto blue5 = new Tag( + null, "", "blue", + [ Value(5), Value("Chan"), Value(12345) ], + [ new Attribute("isThree", Value(false)) ], + null + ); + auto orange = new Tag( + null, "stuff", "orange", + [ Value(1), Value(2), Value(3), Value(2), Value(1) ], + null, + null + ); + auto square = new Tag( + null, "stuff", "square", + null, + [ + new Attribute("points", Value(4)), + new Attribute("dimensions", Value(2)), + new Attribute("points", Value("Still four")), + ], + null + ); + auto triangle = new Tag( + null, "stuff", "triangle", + null, + [ + new Attribute("data", "points", Value(3)), + new Attribute("data", "dimensions", Value(2)), + ], + null + ); + auto nothing = new Tag( + null, "", "nothing", + null, null, null + ); + auto namespaces = new Tag( + null, "", "namespaces", + null, + [ + new Attribute("small", "A", Value(1)), + new Attribute("med", "A", Value(2)), + new Attribute("big", "A", Value(3)), + new Attribute("small", "B", Value(10)), + new Attribute("big", "B", Value(30)), + ], + null + ); + auto chiyo = new Tag( + null, "", "chiyo", + [ Value("Small"), Value("Flies?") ], + [ + new Attribute("nemesis", Value("Car")), + new Attribute("score", Value(100)), + ], + null + ); + auto chiyo_ = new Tag( + null, "", "chiyo_", + [ Value("Small"), Value("Flies?") ], + [ + new Attribute("nemesis", Value("Car")), + new Attribute("score", Value(100)), + ], + null + ); + auto yukari = new Tag( + null, "", "yukari", + null, null, null + ); + auto sana = new Tag( + null, "visitor", "sana", + null, null, null + ); + auto sana_ = new Tag( + null, "visitor", "sana_", + null, null, null + ); + auto sanaVisitor_ = new Tag( + null, "visitor_", "sana_", + null, null, null + ); + auto tomo = new Tag( + null, "", "tomo", + null, null, null + ); + auto hayama = new Tag( + null, "visitor", "hayama", + null, null, null + ); + auto people = new Tag( + null, "", "people", + null, + [ + new Attribute("visitor", "a", Value(1)), + new Attribute("b", Value(2)), + ], + [chiyo, yukari, sana, tomo, hayama] + ); + + assert(blue3 .opEquals( blue3 )); + assert(blue5 .opEquals( blue5 )); + assert(orange .opEquals( orange )); + assert(square .opEquals( square )); + assert(triangle .opEquals( triangle )); + assert(nothing .opEquals( nothing )); + assert(namespaces .opEquals( namespaces )); + assert(people .opEquals( people )); + assert(chiyo .opEquals( chiyo )); + assert(yukari .opEquals( yukari )); + assert(sana .opEquals( sana )); + assert(tomo .opEquals( tomo )); + assert(hayama .opEquals( hayama )); + + assert(!blue3.opEquals(orange)); + assert(!blue3.opEquals(people)); + assert(!blue3.opEquals(sana)); + assert(!blue3.opEquals(blue5)); + assert(!blue5.opEquals(blue3)); + + alias Tag.NamespaceAccess NSA; + static bool namespaceEquals(NSA n1, NSA n2) + { + return n1.name == n2.name; + } + + testRandomAccessRange(root.attributes, cast(Attribute[])[]); + testRandomAccessRange(root.tags, [blue3, blue5, nothing, namespaces, people]); + testRandomAccessRange(root.namespaces, [NSA(""), NSA("stuff")], &namespaceEquals); + testRandomAccessRange(root.namespaces[0].tags, [blue3, blue5, nothing, namespaces, people]); + testRandomAccessRange(root.namespaces[1].tags, [orange, square, triangle]); + assert("" in root.namespaces); + assert("stuff" in root.namespaces); + assert("foobar" !in root.namespaces); + testRandomAccessRange(root.namespaces[ ""].tags, [blue3, blue5, nothing, namespaces, people]); + testRandomAccessRange(root.namespaces["stuff"].tags, [orange, square, triangle]); + testRandomAccessRange(root.all.attributes, cast(Attribute[])[]); + testRandomAccessRange(root.all.tags, [blue3, blue5, orange, square, triangle, nothing, namespaces, people]); + testRandomAccessRange(root.all.tags[], [blue3, blue5, orange, square, triangle, nothing, namespaces, people]); + testRandomAccessRange(root.all.tags[3..6], [square, triangle, nothing]); + assert("blue" in root.tags); + assert("nothing" in root.tags); + assert("people" in root.tags); + assert("orange" !in root.tags); + assert("square" !in root.tags); + assert("foobar" !in root.tags); + assert("blue" in root.all.tags); + assert("nothing" in root.all.tags); + assert("people" in root.all.tags); + assert("orange" in root.all.tags); + assert("square" in root.all.tags); + assert("foobar" !in root.all.tags); + assert("orange" in root.namespaces["stuff"].tags); + assert("square" in root.namespaces["stuff"].tags); + assert("square" in root.namespaces["stuff"].tags); + assert("foobar" !in root.attributes); + assert("foobar" !in root.all.attributes); + assert("foobar" !in root.namespaces["stuff"].attributes); + assert("blue" !in root.attributes); + assert("blue" !in root.all.attributes); + assert("blue" !in root.namespaces["stuff"].attributes); + testRandomAccessRange(root.tags["nothing"], [nothing]); + testRandomAccessRange(root.tags["blue"], [blue3, blue5]); + testRandomAccessRange(root.namespaces["stuff"].tags["orange"], [orange]); + testRandomAccessRange(root.all.tags["nothing"], [nothing]); + testRandomAccessRange(root.all.tags["blue"], [blue3, blue5]); + testRandomAccessRange(root.all.tags["orange"], [orange]); + + assertThrown!SDLangRangeException(root.tags["foobar"]); + assertThrown!SDLangRangeException(root.all.tags["foobar"]); + assertThrown!SDLangRangeException(root.attributes["foobar"]); + assertThrown!SDLangRangeException(root.all.attributes["foobar"]); + + // DMD Issue #12585 causes a segfault in these two tests when using 2.064 or 2.065, + // so work around it. + //assertThrown!SDLangRangeException(root.namespaces["foobar"].tags["foobar"]); + //assertThrown!SDLangRangeException(root.namespaces["foobar"].attributes["foobar"]); + bool didCatch = false; + try + auto x = root.namespaces["foobar"].tags["foobar"]; + catch(SDLangRangeException e) + didCatch = true; + assert(didCatch); + + didCatch = false; + try + auto x = root.namespaces["foobar"].attributes["foobar"]; + catch(SDLangRangeException e) + didCatch = true; + assert(didCatch); + + testRandomAccessRange(root.maybe.tags["nothing"], [nothing]); + testRandomAccessRange(root.maybe.tags["blue"], [blue3, blue5]); + testRandomAccessRange(root.maybe.namespaces["stuff"].tags["orange"], [orange]); + testRandomAccessRange(root.maybe.all.tags["nothing"], [nothing]); + testRandomAccessRange(root.maybe.all.tags["blue"], [blue3, blue5]); + testRandomAccessRange(root.maybe.all.tags["blue"][], [blue3, blue5]); + testRandomAccessRange(root.maybe.all.tags["blue"][0..1], [blue3]); + testRandomAccessRange(root.maybe.all.tags["blue"][1..2], [blue5]); + testRandomAccessRange(root.maybe.all.tags["orange"], [orange]); + testRandomAccessRange(root.maybe.tags["foobar"], cast(Tag[])[]); + testRandomAccessRange(root.maybe.all.tags["foobar"], cast(Tag[])[]); + testRandomAccessRange(root.maybe.namespaces["foobar"].tags["foobar"], cast(Tag[])[]); + testRandomAccessRange(root.maybe.attributes["foobar"], cast(Attribute[])[]); + testRandomAccessRange(root.maybe.all.attributes["foobar"], cast(Attribute[])[]); + testRandomAccessRange(root.maybe.namespaces["foobar"].attributes["foobar"], cast(Attribute[])[]); + + testRandomAccessRange(blue3.attributes, [ new Attribute("isThree", Value(true)) ]); + testRandomAccessRange(blue3.tags, cast(Tag[])[]); + testRandomAccessRange(blue3.namespaces, [NSA("")], &namespaceEquals); + testRandomAccessRange(blue3.all.attributes, [ new Attribute("isThree", Value(true)) ]); + testRandomAccessRange(blue3.all.tags, cast(Tag[])[]); + + testRandomAccessRange(blue5.attributes, [ new Attribute("isThree", Value(false)) ]); + testRandomAccessRange(blue5.tags, cast(Tag[])[]); + testRandomAccessRange(blue5.namespaces, [NSA("")], &namespaceEquals); + testRandomAccessRange(blue5.all.attributes, [ new Attribute("isThree", Value(false)) ]); + testRandomAccessRange(blue5.all.tags, cast(Tag[])[]); + + testRandomAccessRange(orange.attributes, cast(Attribute[])[]); + testRandomAccessRange(orange.tags, cast(Tag[])[]); + testRandomAccessRange(orange.namespaces, cast(NSA[])[], &namespaceEquals); + testRandomAccessRange(orange.all.attributes, cast(Attribute[])[]); + testRandomAccessRange(orange.all.tags, cast(Tag[])[]); + + testRandomAccessRange(square.attributes, [ + new Attribute("points", Value(4)), + new Attribute("dimensions", Value(2)), + new Attribute("points", Value("Still four")), + ]); + testRandomAccessRange(square.tags, cast(Tag[])[]); + testRandomAccessRange(square.namespaces, [NSA("")], &namespaceEquals); + testRandomAccessRange(square.all.attributes, [ + new Attribute("points", Value(4)), + new Attribute("dimensions", Value(2)), + new Attribute("points", Value("Still four")), + ]); + testRandomAccessRange(square.all.tags, cast(Tag[])[]); + + testRandomAccessRange(triangle.attributes, cast(Attribute[])[]); + testRandomAccessRange(triangle.tags, cast(Tag[])[]); + testRandomAccessRange(triangle.namespaces, [NSA("data")], &namespaceEquals); + testRandomAccessRange(triangle.namespaces[0].attributes, [ + new Attribute("data", "points", Value(3)), + new Attribute("data", "dimensions", Value(2)), + ]); + assert("data" in triangle.namespaces); + assert("foobar" !in triangle.namespaces); + testRandomAccessRange(triangle.namespaces["data"].attributes, [ + new Attribute("data", "points", Value(3)), + new Attribute("data", "dimensions", Value(2)), + ]); + testRandomAccessRange(triangle.all.attributes, [ + new Attribute("data", "points", Value(3)), + new Attribute("data", "dimensions", Value(2)), + ]); + testRandomAccessRange(triangle.all.tags, cast(Tag[])[]); + + testRandomAccessRange(nothing.attributes, cast(Attribute[])[]); + testRandomAccessRange(nothing.tags, cast(Tag[])[]); + testRandomAccessRange(nothing.namespaces, cast(NSA[])[], &namespaceEquals); + testRandomAccessRange(nothing.all.attributes, cast(Attribute[])[]); + testRandomAccessRange(nothing.all.tags, cast(Tag[])[]); + + testRandomAccessRange(namespaces.attributes, cast(Attribute[])[]); + testRandomAccessRange(namespaces.tags, cast(Tag[])[]); + testRandomAccessRange(namespaces.namespaces, [NSA("small"), NSA("med"), NSA("big")], &namespaceEquals); + testRandomAccessRange(namespaces.namespaces[], [NSA("small"), NSA("med"), NSA("big")], &namespaceEquals); + testRandomAccessRange(namespaces.namespaces[1..2], [NSA("med")], &namespaceEquals); + testRandomAccessRange(namespaces.namespaces[0].attributes, [ + new Attribute("small", "A", Value(1)), + new Attribute("small", "B", Value(10)), + ]); + testRandomAccessRange(namespaces.namespaces[1].attributes, [ + new Attribute("med", "A", Value(2)), + ]); + testRandomAccessRange(namespaces.namespaces[2].attributes, [ + new Attribute("big", "A", Value(3)), + new Attribute("big", "B", Value(30)), + ]); + testRandomAccessRange(namespaces.namespaces[1..2][0].attributes, [ + new Attribute("med", "A", Value(2)), + ]); + assert("small" in namespaces.namespaces); + assert("med" in namespaces.namespaces); + assert("big" in namespaces.namespaces); + assert("foobar" !in namespaces.namespaces); + assert("small" !in namespaces.namespaces[1..2]); + assert("med" in namespaces.namespaces[1..2]); + assert("big" !in namespaces.namespaces[1..2]); + assert("foobar" !in namespaces.namespaces[1..2]); + testRandomAccessRange(namespaces.namespaces["small"].attributes, [ + new Attribute("small", "A", Value(1)), + new Attribute("small", "B", Value(10)), + ]); + testRandomAccessRange(namespaces.namespaces["med"].attributes, [ + new Attribute("med", "A", Value(2)), + ]); + testRandomAccessRange(namespaces.namespaces["big"].attributes, [ + new Attribute("big", "A", Value(3)), + new Attribute("big", "B", Value(30)), + ]); + testRandomAccessRange(namespaces.all.attributes, [ + new Attribute("small", "A", Value(1)), + new Attribute("med", "A", Value(2)), + new Attribute("big", "A", Value(3)), + new Attribute("small", "B", Value(10)), + new Attribute("big", "B", Value(30)), + ]); + testRandomAccessRange(namespaces.all.attributes[], [ + new Attribute("small", "A", Value(1)), + new Attribute("med", "A", Value(2)), + new Attribute("big", "A", Value(3)), + new Attribute("small", "B", Value(10)), + new Attribute("big", "B", Value(30)), + ]); + testRandomAccessRange(namespaces.all.attributes[2..4], [ + new Attribute("big", "A", Value(3)), + new Attribute("small", "B", Value(10)), + ]); + testRandomAccessRange(namespaces.all.tags, cast(Tag[])[]); + assert("A" !in namespaces.attributes); + assert("B" !in namespaces.attributes); + assert("foobar" !in namespaces.attributes); + assert("A" in namespaces.all.attributes); + assert("B" in namespaces.all.attributes); + assert("foobar" !in namespaces.all.attributes); + assert("A" in namespaces.namespaces["small"].attributes); + assert("B" in namespaces.namespaces["small"].attributes); + assert("foobar" !in namespaces.namespaces["small"].attributes); + assert("A" in namespaces.namespaces["med"].attributes); + assert("B" !in namespaces.namespaces["med"].attributes); + assert("foobar" !in namespaces.namespaces["med"].attributes); + assert("A" in namespaces.namespaces["big"].attributes); + assert("B" in namespaces.namespaces["big"].attributes); + assert("foobar" !in namespaces.namespaces["big"].attributes); + assert("foobar" !in namespaces.tags); + assert("foobar" !in namespaces.all.tags); + assert("foobar" !in namespaces.namespaces["small"].tags); + assert("A" !in namespaces.tags); + assert("A" !in namespaces.all.tags); + assert("A" !in namespaces.namespaces["small"].tags); + testRandomAccessRange(namespaces.namespaces["small"].attributes["A"], [ + new Attribute("small", "A", Value(1)), + ]); + testRandomAccessRange(namespaces.namespaces["med"].attributes["A"], [ + new Attribute("med", "A", Value(2)), + ]); + testRandomAccessRange(namespaces.namespaces["big"].attributes["A"], [ + new Attribute("big", "A", Value(3)), + ]); + testRandomAccessRange(namespaces.all.attributes["A"], [ + new Attribute("small", "A", Value(1)), + new Attribute("med", "A", Value(2)), + new Attribute("big", "A", Value(3)), + ]); + testRandomAccessRange(namespaces.all.attributes["B"], [ + new Attribute("small", "B", Value(10)), + new Attribute("big", "B", Value(30)), + ]); + + testRandomAccessRange(chiyo.attributes, [ + new Attribute("nemesis", Value("Car")), + new Attribute("score", Value(100)), + ]); + testRandomAccessRange(chiyo.tags, cast(Tag[])[]); + testRandomAccessRange(chiyo.namespaces, [NSA("")], &namespaceEquals); + testRandomAccessRange(chiyo.all.attributes, [ + new Attribute("nemesis", Value("Car")), + new Attribute("score", Value(100)), + ]); + testRandomAccessRange(chiyo.all.tags, cast(Tag[])[]); + + testRandomAccessRange(yukari.attributes, cast(Attribute[])[]); + testRandomAccessRange(yukari.tags, cast(Tag[])[]); + testRandomAccessRange(yukari.namespaces, cast(NSA[])[], &namespaceEquals); + testRandomAccessRange(yukari.all.attributes, cast(Attribute[])[]); + testRandomAccessRange(yukari.all.tags, cast(Tag[])[]); + + testRandomAccessRange(sana.attributes, cast(Attribute[])[]); + testRandomAccessRange(sana.tags, cast(Tag[])[]); + testRandomAccessRange(sana.namespaces, cast(NSA[])[], &namespaceEquals); + testRandomAccessRange(sana.all.attributes, cast(Attribute[])[]); + testRandomAccessRange(sana.all.tags, cast(Tag[])[]); + + testRandomAccessRange(people.attributes, [new Attribute("b", Value(2))]); + testRandomAccessRange(people.tags, [chiyo, yukari, tomo]); + testRandomAccessRange(people.namespaces, [NSA("visitor"), NSA("")], &namespaceEquals); + testRandomAccessRange(people.namespaces[0].attributes, [new Attribute("visitor", "a", Value(1))]); + testRandomAccessRange(people.namespaces[1].attributes, [new Attribute("b", Value(2))]); + testRandomAccessRange(people.namespaces[0].tags, [sana, hayama]); + testRandomAccessRange(people.namespaces[1].tags, [chiyo, yukari, tomo]); + assert("visitor" in people.namespaces); + assert("" in people.namespaces); + assert("foobar" !in people.namespaces); + testRandomAccessRange(people.namespaces["visitor"].attributes, [new Attribute("visitor", "a", Value(1))]); + testRandomAccessRange(people.namespaces[ ""].attributes, [new Attribute("b", Value(2))]); + testRandomAccessRange(people.namespaces["visitor"].tags, [sana, hayama]); + testRandomAccessRange(people.namespaces[ ""].tags, [chiyo, yukari, tomo]); + testRandomAccessRange(people.all.attributes, [ + new Attribute("visitor", "a", Value(1)), + new Attribute("b", Value(2)), + ]); + testRandomAccessRange(people.all.tags, [chiyo, yukari, sana, tomo, hayama]); + + people.attributes["b"][0].name = "b_"; + people.namespaces["visitor"].attributes["a"][0].name = "a_"; + people.tags["chiyo"][0].name = "chiyo_"; + people.namespaces["visitor"].tags["sana"][0].name = "sana_"; + + assert("b_" in people.attributes); + assert("a_" in people.namespaces["visitor"].attributes); + assert("chiyo_" in people.tags); + assert("sana_" in people.namespaces["visitor"].tags); + + assert(people.attributes["b_"][0] == new Attribute("b_", Value(2))); + assert(people.namespaces["visitor"].attributes["a_"][0] == new Attribute("visitor", "a_", Value(1))); + assert(people.tags["chiyo_"][0] == chiyo_); + assert(people.namespaces["visitor"].tags["sana_"][0] == sana_); + + assert("b" !in people.attributes); + assert("a" !in people.namespaces["visitor"].attributes); + assert("chiyo" !in people.tags); + assert("sana" !in people.namespaces["visitor"].tags); + + assert(people.maybe.attributes["b"].length == 0); + assert(people.maybe.namespaces["visitor"].attributes["a"].length == 0); + assert(people.maybe.tags["chiyo"].length == 0); + assert(people.maybe.namespaces["visitor"].tags["sana"].length == 0); + + people.tags["tomo"][0].remove(); + people.namespaces["visitor"].tags["hayama"][0].remove(); + people.tags["chiyo_"][0].remove(); + testRandomAccessRange(people.tags, [yukari]); + testRandomAccessRange(people.namespaces, [NSA("visitor"), NSA("")], &namespaceEquals); + testRandomAccessRange(people.namespaces[0].tags, [sana_]); + testRandomAccessRange(people.namespaces[1].tags, [yukari]); + assert("visitor" in people.namespaces); + assert("" in people.namespaces); + assert("foobar" !in people.namespaces); + testRandomAccessRange(people.namespaces["visitor"].tags, [sana_]); + testRandomAccessRange(people.namespaces[ ""].tags, [yukari]); + testRandomAccessRange(people.all.tags, [yukari, sana_]); + + people.attributes["b_"][0].namespace = "_"; + people.namespaces["visitor"].attributes["a_"][0].namespace = "visitor_"; + assert("_" in people.namespaces); + assert("visitor_" in people.namespaces); + assert("" in people.namespaces); + assert("visitor" in people.namespaces); + people.namespaces["visitor"].tags["sana_"][0].namespace = "visitor_"; + assert("_" in people.namespaces); + assert("visitor_" in people.namespaces); + assert("" in people.namespaces); + assert("visitor" !in people.namespaces); + + assert(people.namespaces["_" ].attributes["b_"][0] == new Attribute("_", "b_", Value(2))); + assert(people.namespaces["visitor_"].attributes["a_"][0] == new Attribute("visitor_", "a_", Value(1))); + assert(people.namespaces["visitor_"].tags["sana_"][0] == sanaVisitor_); + + people.tags["yukari"][0].remove(); + people.namespaces["visitor_"].tags["sana_"][0].remove(); + people.namespaces["visitor_"].attributes["a_"][0].namespace = "visitor"; + people.namespaces["_"].attributes["b_"][0].namespace = ""; + testRandomAccessRange(people.tags, cast(Tag[])[]); + testRandomAccessRange(people.namespaces, [NSA("visitor"), NSA("")], &namespaceEquals); + testRandomAccessRange(people.namespaces[0].tags, cast(Tag[])[]); + testRandomAccessRange(people.namespaces[1].tags, cast(Tag[])[]); + assert("visitor" in people.namespaces); + assert("" in people.namespaces); + assert("foobar" !in people.namespaces); + testRandomAccessRange(people.namespaces["visitor"].tags, cast(Tag[])[]); + testRandomAccessRange(people.namespaces[ ""].tags, cast(Tag[])[]); + testRandomAccessRange(people.all.tags, cast(Tag[])[]); + + people.namespaces["visitor"].attributes["a_"][0].remove(); + testRandomAccessRange(people.attributes, [new Attribute("b_", Value(2))]); + testRandomAccessRange(people.namespaces, [NSA("")], &namespaceEquals); + testRandomAccessRange(people.namespaces[0].attributes, [new Attribute("b_", Value(2))]); + assert("visitor" !in people.namespaces); + assert("" in people.namespaces); + assert("foobar" !in people.namespaces); + testRandomAccessRange(people.namespaces[""].attributes, [new Attribute("b_", Value(2))]); + testRandomAccessRange(people.all.attributes, [ + new Attribute("b_", Value(2)), + ]); + + people.attributes["b_"][0].remove(); + testRandomAccessRange(people.attributes, cast(Attribute[])[]); + testRandomAccessRange(people.namespaces, cast(NSA[])[], &namespaceEquals); + assert("visitor" !in people.namespaces); + assert("" !in people.namespaces); + assert("foobar" !in people.namespaces); + testRandomAccessRange(people.all.attributes, cast(Attribute[])[]); +} + +// Regression test, issue #11: https://github.com/Abscissa/SDLang-D/issues/11 +version(sdlangUnittest) +unittest +{ + import sdlang.parser; + writeln("ast: Regression test issue #11..."); + stdout.flush(); + + auto root = parseSource( +`// +a`); + + assert("a" in root.tags); + + root = parseSource( +`// +parent { + child +} +`); + + auto child = new Tag( + null, "", "child", + null, null, null + ); + + assert("parent" in root.tags); + assert("child" !in root.tags); + testRandomAccessRange(root.tags["parent"][0].tags, [child]); + assert("child" in root.tags["parent"][0].tags); +} -- cgit v1.2.3