// 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; 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; /++ This tag's namespace. Empty string if no namespace. Note that setting this value is O(n) because internal lookup structures need to be updated. Note also, that setting this may change where this tag is ordered among its parent's list of tags. +/ @property string namespace() { return _namespace; } ///ditto @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; /++ This attribute's name, not including namespace. Use `getFullName().toString` if you want the namespace included. Note that setting this value is O(n) because internal lookup structures need to be updated. Note also, that setting this may change where this attribute is ordered among its parent's list of tags. +/ @property string name() { return _name; } ///ditto @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; } /// This tag's name, including namespace if one exists. deprecated("Use 'getFullName().toString()'") @property string fullName() { return getFullName().toString(); } /// This tag's name, including namespace if one exists. FullName getFullName() { return FullName(_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; } /// Copy this Attribute. /// The clone does $(B $(I not)) have a parent, even if the original does. Attribute clone() { return new Attribute(_namespace, _name, value, location); } /// 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); } } /// Deep-copy an array of Tag or Attribute. /// The top-level clones are $(B $(I not)) attached to any parent, even if the originals are. T[] clone(T)(T[] arr) if(is(T==Tag) || is(T==Attribute)) { T[] newArr; newArr.length = arr.length; foreach(i; 0..arr.length) newArr[i] = arr[i].clone(); return newArr; } class Tag { /// File/Line/Column/Index information for where this tag was located in /// its original SDLang file. Location location; /// Access all this tag's values, as an array of type `sdlang.token.Value`. 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; /++ This tag's namespace. Empty string if no namespace. Note that setting this value is O(n) because internal lookup structures need to be updated. Note also, that setting this may change where this tag is ordered among its parent's list of tags. +/ @property string namespace() { return _namespace; } ///ditto @property void namespace(string value) { //TODO: Can we do this in-place, without removing/adding and thus // modyfying the internal order? 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; /++ This tag's name, not including namespace. Use `getFullName().toString` if you want the namespace included. Note that setting this value is O(n) because internal lookup structures need to be updated. Note also, that setting this may change where this tag is ordered among its parent's list of tags. +/ @property string name() { return _name; } ///ditto @property void name(string value) { //TODO: Seriously? Can't we at least do the "*" modification *in-place*? if(_parent && _name != value) { _parent.updateId++; // Not the most efficient, but it works. 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 //TODO: Can we re-insert while preserving the original order? _parent._tags[_namespace][_name] ~= this; _parent._tags["*"][_name] ~= this; } else _name = value; } /// This tag's name, including namespace if one exists. deprecated("Use 'getFullName().toString()'") @property string fullName() { return getFullName().toString(); } /// This tag's name, including namespace if one exists. FullName getFullName() { return FullName(_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); } /// Deep-copy this Tag. /// The clone does $(B $(I not)) have a parent, even if the original does. Tag clone() { auto newTag = new Tag(_namespace, _name, values.dup, allAttributes.clone(), allTags.clone()); newTag.location = location; return newTag; } 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 `ValidationException` 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 ValidationException( "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 ValidationException( "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( tag !is null && 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 tag is null || frontIndex == endIndex; } private size_t frontIndex; @property T front() { return this[0]; } void popFront() { if(empty) throw new DOMRangeException(tag, "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 DOMRangeException(tag, "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 DOMRangeException(tag, "Slice out of range"); return r; } T opIndex(size_t index) { if(empty) throw new DOMRangeException(tag, "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(tag is null) endIndex = 0; else { 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 tag is null || frontIndex == endIndex; } private size_t frontIndex; @property T front() { return this[0]; } void popFront() { if(empty) throw new DOMRangeException(tag, "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 DOMRangeException(tag, "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 DOMRangeException(tag, "Slice out of range"); return r; } T opIndex(size_t index) { if(empty) throw new DOMRangeException(tag, "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 DOMRangeException(tag, "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 DOMRangeException(tag, "Range is empty"); if(!isMaybe && name !in this) throw new DOMRangeException(tag, `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 DOMRangeException(tag, "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(tag is null) return false; 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 DOMRangeException(tag, "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 DOMRangeException(tag, "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 DOMRangeException(tag, "Slice out of range"); return r; } NamespaceAccess opIndex(size_t index) { if(empty) throw new DOMRangeException(tag, "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 DOMRangeException(tag, "Range is empty"); if(!isMaybe && namespace !in this) throw new DOMRangeException(tag, `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); } } static 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 Returns a random access range of `Attribute` objects that supports numeric-indexing, string-indexing, slicing and length. Since SDLang allows multiple attributes with the same name, string-indexing returns a random access range of all attributes with the given name. The string-indexing does $(B $(I not)) support namespace prefixes. Use `namespace[string]`.`attributes` or `all`.`attributes` for that. See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) for a high-level overview (and examples) of how to use this. +/ @property AttributeRange attributes() { return AttributeRange(this, "", false); } /++ Access all direct-child tags that don't have a namespace. Returns a random access range of `Tag` objects that supports numeric-indexing, string-indexing, slicing and length. Since SDLang allows multiple tags with the same name, string-indexing returns a random access range of all immediate child tags with the given name. The string-indexing does $(B $(I not)) support namespace prefixes. Use `namespace[string]`.`attributes` or `all`.`attributes` for that. See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) for a high-level overview (and examples) of how to use this. +/ @property TagRange tags() { return TagRange(this, "", false); } /++ Access all namespaces in this tag, and the attributes/tags within them. Returns a random access range of `NamespaceAccess` elements that supports numeric-indexing, string-indexing, slicing and length. See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) for a high-level overview (and examples) of how to use this. +/ @property NamespaceRange namespaces() { return NamespaceRange(this, false); } /// Access all attributes and tags regardless of namespace. /// /// See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) /// for a better understanding (and examples) of how to use this. @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 `sdlang.exception.DOMRangeException`. /// /// See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) /// for a more information (and examples) of how to use this. @property MaybeAccess maybe() { return MaybeAccess(this); } // Internal implementations for the get/expect functions further below: private Tag getTagImpl(FullName tagFullName, Tag defaultValue=null, bool useDefaultValue=true) { auto tagNS = tagFullName.namespace; auto tagName = tagFullName.name; // Can find namespace? if(tagNS !in _tags) { if(useDefaultValue) return defaultValue; else throw new TagNotFoundException(this, tagFullName, "No tags found in namespace '"~namespace~"'"); } // Can find tag in namespace? if(tagName !in _tags[tagNS] || _tags[tagNS][tagName].length == 0) { if(useDefaultValue) return defaultValue; else throw new TagNotFoundException(this, tagFullName, "Can't find tag '"~tagFullName.toString()~"'"); } // Return last matching tag found return _tags[tagNS][tagName][$-1]; } private T getValueImpl(T)(T defaultValue, bool useDefaultValue=true) if(isValueType!T) { // Find value foreach(value; this.values) { if(value.type == typeid(T)) return value.get!T(); } // No value of type T found if(useDefaultValue) return defaultValue; else { throw new ValueNotFoundException( this, FullName(this.namespace, this.name), typeid(T), "No value of type "~T.stringof~" found." ); } } private T getAttributeImpl(T)(FullName attrFullName, T defaultValue, bool useDefaultValue=true) if(isValueType!T) { auto attrNS = attrFullName.namespace; auto attrName = attrFullName.name; // Can find namespace and attribute name? if(attrNS !in this._attributes || attrName !in this._attributes[attrNS]) { if(useDefaultValue) return defaultValue; else { throw new AttributeNotFoundException( this, this.getFullName(), attrFullName, typeid(T), "Can't find attribute '"~FullName.combine(attrNS, attrName)~"'" ); } } // Find value with chosen type foreach(attr; this._attributes[attrNS][attrName]) { if(attr.value.type == typeid(T)) return attr.value.get!T(); } // Chosen type not found if(useDefaultValue) return defaultValue; else { throw new AttributeNotFoundException( this, this.getFullName(), attrFullName, typeid(T), "Can't find attribute '"~FullName.combine(attrNS, attrName)~"' of type "~T.stringof ); } } // High-level interfaces for get/expect funtions: /++ Lookup a child tag by name. Returns null if not found. Useful if you only expect one, and only one, child tag of a given name. Only looks for immediate child tags of `this`, doesn't search recursively. If you expect multiple tags by the same name and want to get them all, use `maybe`.`tags[string]` instead. The name can optionally include a namespace, as in `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. Use an empty string to search for anonymous tags, or `"namespace:"` for anonymous tags inside a namespace. Wildcard searching is only supported for namespaces, not names. Use `maybe`.`tags[0]` if you don't care about the name. If there are multiple tags by the chosen name, the $(B $(I last tag)) will always be chosen. That is, this function considers later tags with the same name to override previous ones. If the tag cannot be found, and you provides a default value, the default value is returned. Otherwise null is returned. If you'd prefer an exception thrown, use `expectTag` instead. +/ Tag getTag(string fullTagName, Tag defaultValue=null) { auto parsedName = FullName.parse(fullTagName); parsedName.ensureNoWildcardName( "Instead, use 'Tag.maybe.tags[0]', 'Tag.maybe.all.tags[0]' or 'Tag.maybe.namespace[ns].tags[0]'." ); return getTagImpl(parsedName, defaultValue); } /// @("Tag.getTag") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo 1 foo 2 // getTag considers this to override the first foo ns1:foo 3 ns1:foo 4 // getTag considers this to override the first ns1:foo ns2:foo 33 ns2:foo 44 // getTag considers this to override the first ns2:foo `); assert( root.getTag("foo" ).values[0].get!int() == 2 ); assert( root.getTag("ns1:foo").values[0].get!int() == 4 ); assert( root.getTag("*:foo" ).values[0].get!int() == 44 ); // Search all namespaces // Not found // If you'd prefer an exception, use `expectTag` instead. assert( root.getTag("doesnt-exist") is null ); // Default value auto foo = root.getTag("foo"); assert( root.getTag("doesnt-exist", foo) is foo ); } /++ Lookup a child tag by name. Throws if not found. Useful if you only expect one, and only one, child tag of a given name. Only looks for immediate child tags of `this`, doesn't search recursively. If you expect multiple tags by the same name and want to get them all, use `tags[string]` instead. The name can optionally include a namespace, as in `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. Use an empty string to search for anonymous tags, or `"namespace:"` for anonymous tags inside a namespace. Wildcard searching is only supported for namespaces, not names. Use `tags[0]` if you don't care about the name. If there are multiple tags by the chosen name, the $(B $(I last tag)) will always be chosen. That is, this function considers later tags with the same name to override previous ones. If no such tag is found, an `sdlang.exception.TagNotFoundException` will be thrown. If you'd rather receive a default value, use `getTag` instead. +/ Tag expectTag(string fullTagName) { auto parsedName = FullName.parse(fullTagName); parsedName.ensureNoWildcardName( "Instead, use 'Tag.tags[0]', 'Tag.all.tags[0]' or 'Tag.namespace[ns].tags[0]'." ); return getTagImpl(parsedName, null, false); } /// @("Tag.expectTag") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo 1 foo 2 // expectTag considers this to override the first foo ns1:foo 3 ns1:foo 4 // expectTag considers this to override the first ns1:foo ns2:foo 33 ns2:foo 44 // expectTag considers this to override the first ns2:foo `); assert( root.expectTag("foo" ).values[0].get!int() == 2 ); assert( root.expectTag("ns1:foo").values[0].get!int() == 4 ); assert( root.expectTag("*:foo" ).values[0].get!int() == 44 ); // Search all namespaces // Not found // If you'd rather receive a default value than an exception, use `getTag` instead. assertThrown!TagNotFoundException( root.expectTag("doesnt-exist") ); } /++ Retrieve a value of type T from `this` tag. Returns a default value if not found. Useful if you only expect one value of type T from this tag. Only looks for values of `this` tag, it does not search child tags. If you wish to search for a value in a child tag (for example, if this current tag is a root tag), try `getTagValue`. If you want to get more than one value from this tag, use `values` instead. If this tag has multiple values, the $(B $(I first)) value matching the requested type will be returned. Ie, Extra values in the tag are ignored. You may provide a default value to be returned in case no value of the requested type can be found. If you don't provide a default value, `T.init` will be used. If you'd rather an exception be thrown when a value cannot be found, use `expectValue` instead. +/ T getValue(T)(T defaultValue = T.init) if(isValueType!T) { return getValueImpl!T(defaultValue, true); } /// @("Tag.getValue") unittest { import std.exception; import std.math; import sdlang.parser; auto root = parseSource(` foo 1 true 2 false `); auto foo = root.getTag("foo"); assert( foo.getValue!int() == 1 ); assert( foo.getValue!bool() == true ); // Value found, default value ignored. assert( foo.getValue!int(999) == 1 ); // No strings found // If you'd prefer an exception, use `expectValue` instead. assert( foo.getValue!string("Default") == "Default" ); assert( foo.getValue!string() is null ); // No floats found assert( foo.getValue!float(99.9).approxEqual(99.9) ); assert( foo.getValue!float().isNaN() ); } /++ Retrieve a value of type T from `this` tag. Throws if not found. Useful if you only expect one value of type T from this tag. Only looks for values of `this` tag, it does not search child tags. If you wish to search for a value in a child tag (for example, if this current tag is a root tag), try `expectTagValue`. If you want to get more than one value from this tag, use `values` instead. If this tag has multiple values, the $(B $(I first)) value matching the requested type will be returned. Ie, Extra values in the tag are ignored. An `sdlang.exception.ValueNotFoundException` will be thrown if no value of the requested type can be found. If you'd rather receive a default value, use `getValue` instead. +/ T expectValue(T)() if(isValueType!T) { return getValueImpl!T(T.init, false); } /// @("Tag.expectValue") unittest { import std.exception; import std.math; import sdlang.parser; auto root = parseSource(` foo 1 true 2 false `); auto foo = root.getTag("foo"); assert( foo.expectValue!int() == 1 ); assert( foo.expectValue!bool() == true ); // No strings or floats found // If you'd rather receive a default value than an exception, use `getValue` instead. assertThrown!ValueNotFoundException( foo.expectValue!string() ); assertThrown!ValueNotFoundException( foo.expectValue!float() ); } /++ Lookup a child tag by name, and retrieve a value of type T from it. Returns a default value if not found. Useful if you only expect one value of type T from a given tag. Only looks for immediate child tags of `this`, doesn't search recursively. This is a shortcut for `getTag().getValue()`, except if the tag isn't found, then instead of a null reference error, it will return the requested `defaultValue` (or T.init by default). +/ T getTagValue(T)(string fullTagName, T defaultValue = T.init) if(isValueType!T) { auto tag = getTag(fullTagName); if(!tag) return defaultValue; return tag.getValue!T(defaultValue); } /// @("Tag.getTagValue") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo 1 "a" 2 "b" foo 3 "c" 4 "d" // getTagValue considers this to override the first foo bar "hi" bar 379 // getTagValue considers this to override the first bar `); assert( root.getTagValue!int("foo") == 3 ); assert( root.getTagValue!string("foo") == "c" ); // Value found, default value ignored. assert( root.getTagValue!int("foo", 999) == 3 ); // Tag not found // If you'd prefer an exception, use `expectTagValue` instead. assert( root.getTagValue!int("doesnt-exist", 999) == 999 ); assert( root.getTagValue!int("doesnt-exist") == 0 ); // The last "bar" tag doesn't have an int (only the first "bar" tag does) assert( root.getTagValue!string("bar", "Default") == "Default" ); assert( root.getTagValue!string("bar") is null ); // Using namespaces: root = parseSource(` ns1:foo 1 "a" 2 "b" ns1:foo 3 "c" 4 "d" ns2:foo 11 "aa" 22 "bb" ns2:foo 33 "cc" 44 "dd" ns1:bar "hi" ns1:bar 379 // getTagValue considers this to override the first bar `); assert( root.getTagValue!int("ns1:foo") == 3 ); assert( root.getTagValue!int("*:foo" ) == 33 ); // Search all namespaces assert( root.getTagValue!string("ns1:foo") == "c" ); assert( root.getTagValue!string("*:foo" ) == "cc" ); // Search all namespaces // The last "bar" tag doesn't have a string (only the first "bar" tag does) assert( root.getTagValue!string("*:bar", "Default") == "Default" ); assert( root.getTagValue!string("*:bar") is null ); } /++ Lookup a child tag by name, and retrieve a value of type T from it. Throws if not found, Useful if you only expect one value of type T from a given tag. Only looks for immediate child tags of `this`, doesn't search recursively. This is a shortcut for `expectTag().expectValue()`. +/ T expectTagValue(T)(string fullTagName) if(isValueType!T) { return expectTag(fullTagName).expectValue!T(); } /// @("Tag.expectTagValue") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo 1 "a" 2 "b" foo 3 "c" 4 "d" // expectTagValue considers this to override the first foo bar "hi" bar 379 // expectTagValue considers this to override the first bar `); assert( root.expectTagValue!int("foo") == 3 ); assert( root.expectTagValue!string("foo") == "c" ); // The last "bar" tag doesn't have a string (only the first "bar" tag does) // If you'd rather receive a default value than an exception, use `getTagValue` instead. assertThrown!ValueNotFoundException( root.expectTagValue!string("bar") ); // Tag not found assertThrown!TagNotFoundException( root.expectTagValue!int("doesnt-exist") ); // Using namespaces: root = parseSource(` ns1:foo 1 "a" 2 "b" ns1:foo 3 "c" 4 "d" ns2:foo 11 "aa" 22 "bb" ns2:foo 33 "cc" 44 "dd" ns1:bar "hi" ns1:bar 379 // expectTagValue considers this to override the first bar `); assert( root.expectTagValue!int("ns1:foo") == 3 ); assert( root.expectTagValue!int("*:foo" ) == 33 ); // Search all namespaces assert( root.expectTagValue!string("ns1:foo") == "c" ); assert( root.expectTagValue!string("*:foo" ) == "cc" ); // Search all namespaces // The last "bar" tag doesn't have a string (only the first "bar" tag does) assertThrown!ValueNotFoundException( root.expectTagValue!string("*:bar") ); // Namespace not found assertThrown!TagNotFoundException( root.expectTagValue!int("doesnt-exist:bar") ); } /++ Lookup an attribute of `this` tag by name, and retrieve a value of type T from it. Returns a default value if not found. Useful if you only expect one attribute of the given name and type. Only looks for attributes of `this` tag, it does not search child tags. If you wish to search for a value in a child tag (for example, if this current tag is a root tag), try `getTagAttribute`. If you expect multiple attributes by the same name and want to get them all, use `maybe`.`attributes[string]` instead. The attribute name can optionally include a namespace, as in `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. (Note that unlike tags. attributes can't be anonymous - that's what values are.) Wildcard searching is only supported for namespaces, not names. Use `maybe`.`attributes[0]` if you don't care about the name. If this tag has multiple attributes, the $(B $(I first)) attribute matching the requested name and type will be returned. Ie, Extra attributes in the tag are ignored. You may provide a default value to be returned in case no attribute of the requested name and type can be found. If you don't provide a default value, `T.init` will be used. If you'd rather an exception be thrown when an attribute cannot be found, use `expectAttribute` instead. +/ T getAttribute(T)(string fullAttributeName, T defaultValue = T.init) if(isValueType!T) { auto parsedName = FullName.parse(fullAttributeName); parsedName.ensureNoWildcardName( "Instead, use 'Attribute.maybe.tags[0]', 'Attribute.maybe.all.tags[0]' or 'Attribute.maybe.namespace[ns].tags[0]'." ); return getAttributeImpl!T(parsedName, defaultValue); } /// @("Tag.getAttribute") unittest { import std.exception; import std.math; import sdlang.parser; auto root = parseSource(` foo z=0 X=1 X=true X=2 X=false `); auto foo = root.getTag("foo"); assert( foo.getAttribute!int("X") == 1 ); assert( foo.getAttribute!bool("X") == true ); // Value found, default value ignored. assert( foo.getAttribute!int("X", 999) == 1 ); // Attribute name not found // If you'd prefer an exception, use `expectValue` instead. assert( foo.getAttribute!int("doesnt-exist", 999) == 999 ); assert( foo.getAttribute!int("doesnt-exist") == 0 ); // No strings found assert( foo.getAttribute!string("X", "Default") == "Default" ); assert( foo.getAttribute!string("X") is null ); // No floats found assert( foo.getAttribute!float("X", 99.9).approxEqual(99.9) ); assert( foo.getAttribute!float("X").isNaN() ); // Using namespaces: root = parseSource(` foo ns1:z=0 ns1:X=1 ns1:X=2 ns2:X=3 ns2:X=4 `); foo = root.getTag("foo"); assert( foo.getAttribute!int("ns2:X") == 3 ); assert( foo.getAttribute!int("*:X") == 1 ); // Search all namespaces // Namespace not found assert( foo.getAttribute!int("doesnt-exist:X", 999) == 999 ); // No attribute X is in the default namespace assert( foo.getAttribute!int("X", 999) == 999 ); // Attribute name not found assert( foo.getAttribute!int("ns1:doesnt-exist", 999) == 999 ); } /++ Lookup an attribute of `this` tag by name, and retrieve a value of type T from it. Throws if not found. Useful if you only expect one attribute of the given name and type. Only looks for attributes of `this` tag, it does not search child tags. If you wish to search for a value in a child tag (for example, if this current tag is a root tag), try `expectTagAttribute`. If you expect multiple attributes by the same name and want to get them all, use `attributes[string]` instead. The attribute name can optionally include a namespace, as in `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. (Note that unlike tags. attributes can't be anonymous - that's what values are.) Wildcard searching is only supported for namespaces, not names. Use `attributes[0]` if you don't care about the name. If this tag has multiple attributes, the $(B $(I first)) attribute matching the requested name and type will be returned. Ie, Extra attributes in the tag are ignored. An `sdlang.exception.AttributeNotFoundException` will be thrown if no value of the requested type can be found. If you'd rather receive a default value, use `getAttribute` instead. +/ T expectAttribute(T)(string fullAttributeName) if(isValueType!T) { auto parsedName = FullName.parse(fullAttributeName); parsedName.ensureNoWildcardName( "Instead, use 'Attribute.tags[0]', 'Attribute.all.tags[0]' or 'Attribute.namespace[ns].tags[0]'." ); return getAttributeImpl!T(parsedName, T.init, false); } /// @("Tag.expectAttribute") unittest { import std.exception; import std.math; import sdlang.parser; auto root = parseSource(` foo z=0 X=1 X=true X=2 X=false `); auto foo = root.getTag("foo"); assert( foo.expectAttribute!int("X") == 1 ); assert( foo.expectAttribute!bool("X") == true ); // Attribute name not found // If you'd rather receive a default value than an exception, use `getAttribute` instead. assertThrown!AttributeNotFoundException( foo.expectAttribute!int("doesnt-exist") ); // No strings found assertThrown!AttributeNotFoundException( foo.expectAttribute!string("X") ); // No floats found assertThrown!AttributeNotFoundException( foo.expectAttribute!float("X") ); // Using namespaces: root = parseSource(` foo ns1:z=0 ns1:X=1 ns1:X=2 ns2:X=3 ns2:X=4 `); foo = root.getTag("foo"); assert( foo.expectAttribute!int("ns2:X") == 3 ); assert( foo.expectAttribute!int("*:X") == 1 ); // Search all namespaces // Namespace not found assertThrown!AttributeNotFoundException( foo.expectAttribute!int("doesnt-exist:X") ); // No attribute X is in the default namespace assertThrown!AttributeNotFoundException( foo.expectAttribute!int("X") ); // Attribute name not found assertThrown!AttributeNotFoundException( foo.expectAttribute!int("ns1:doesnt-exist") ); } /++ Lookup a child tag and attribute by name, and retrieve a value of type T from it. Returns a default value if not found. Useful if you only expect one attribute of type T from given the tag and attribute names. Only looks for immediate child tags of `this`, doesn't search recursively. This is a shortcut for `getTag().getAttribute()`, except if the tag isn't found, then instead of a null reference error, it will return the requested `defaultValue` (or T.init by default). +/ T getTagAttribute(T)(string fullTagName, string fullAttributeName, T defaultValue = T.init) if(isValueType!T) { auto tag = getTag(fullTagName); if(!tag) return defaultValue; return tag.getAttribute!T(fullAttributeName, defaultValue); } /// @("Tag.getTagAttribute") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo X=1 X="a" X=2 X="b" foo X=3 X="c" X=4 X="d" // getTagAttribute considers this to override the first foo bar X="hi" bar X=379 // getTagAttribute considers this to override the first bar `); assert( root.getTagAttribute!int("foo", "X") == 3 ); assert( root.getTagAttribute!string("foo", "X") == "c" ); // Value found, default value ignored. assert( root.getTagAttribute!int("foo", "X", 999) == 3 ); // Tag not found // If you'd prefer an exception, use `expectTagAttribute` instead of `getTagAttribute` assert( root.getTagAttribute!int("doesnt-exist", "X", 999) == 999 ); assert( root.getTagAttribute!int("doesnt-exist", "X") == 0 ); assert( root.getTagAttribute!int("foo", "doesnt-exist", 999) == 999 ); assert( root.getTagAttribute!int("foo", "doesnt-exist") == 0 ); // The last "bar" tag doesn't have a string (only the first "bar" tag does) assert( root.getTagAttribute!string("bar", "X", "Default") == "Default" ); assert( root.getTagAttribute!string("bar", "X") is null ); // Using namespaces: root = parseSource(` ns1:foo X=1 X="a" X=2 X="b" ns1:foo X=3 X="c" X=4 X="d" ns2:foo X=11 X="aa" X=22 X="bb" ns2:foo X=33 X="cc" X=44 X="dd" ns1:bar attrNS:X="hi" ns1:bar attrNS:X=379 // getTagAttribute considers this to override the first bar `); assert( root.getTagAttribute!int("ns1:foo", "X") == 3 ); assert( root.getTagAttribute!int("*:foo", "X") == 33 ); // Search all namespaces assert( root.getTagAttribute!string("ns1:foo", "X") == "c" ); assert( root.getTagAttribute!string("*:foo", "X") == "cc" ); // Search all namespaces // bar's attribute X is't in the default namespace assert( root.getTagAttribute!int("*:bar", "X", 999) == 999 ); assert( root.getTagAttribute!int("*:bar", "X") == 0 ); // The last "bar" tag's "attrNS:X" attribute doesn't have a string (only the first "bar" tag does) assert( root.getTagAttribute!string("*:bar", "attrNS:X", "Default") == "Default" ); assert( root.getTagAttribute!string("*:bar", "attrNS:X") is null); } /++ Lookup a child tag and attribute by name, and retrieve a value of type T from it. Throws if not found. Useful if you only expect one attribute of type T from given the tag and attribute names. Only looks for immediate child tags of `this`, doesn't search recursively. This is a shortcut for `expectTag().expectAttribute()`. +/ T expectTagAttribute(T)(string fullTagName, string fullAttributeName) if(isValueType!T) { return expectTag(fullTagName).expectAttribute!T(fullAttributeName); } /// @("Tag.expectTagAttribute") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo X=1 X="a" X=2 X="b" foo X=3 X="c" X=4 X="d" // expectTagAttribute considers this to override the first foo bar X="hi" bar X=379 // expectTagAttribute considers this to override the first bar `); assert( root.expectTagAttribute!int("foo", "X") == 3 ); assert( root.expectTagAttribute!string("foo", "X") == "c" ); // The last "bar" tag doesn't have an int attribute named "X" (only the first "bar" tag does) // If you'd rather receive a default value than an exception, use `getAttribute` instead. assertThrown!AttributeNotFoundException( root.expectTagAttribute!string("bar", "X") ); // Tag not found assertThrown!TagNotFoundException( root.expectTagAttribute!int("doesnt-exist", "X") ); // Using namespaces: root = parseSource(` ns1:foo X=1 X="a" X=2 X="b" ns1:foo X=3 X="c" X=4 X="d" ns2:foo X=11 X="aa" X=22 X="bb" ns2:foo X=33 X="cc" X=44 X="dd" ns1:bar attrNS:X="hi" ns1:bar attrNS:X=379 // expectTagAttribute considers this to override the first bar `); assert( root.expectTagAttribute!int("ns1:foo", "X") == 3 ); assert( root.expectTagAttribute!int("*:foo", "X") == 33 ); // Search all namespaces assert( root.expectTagAttribute!string("ns1:foo", "X") == "c" ); assert( root.expectTagAttribute!string("*:foo", "X") == "cc" ); // Search all namespaces // bar's attribute X is't in the default namespace assertThrown!AttributeNotFoundException( root.expectTagAttribute!int("*:bar", "X") ); // The last "bar" tag's "attrNS:X" attribute doesn't have a string (only the first "bar" tag does) assertThrown!AttributeNotFoundException( root.expectTagAttribute!string("*:bar", "attrNS:X") ); // Tag's namespace not found assertThrown!TagNotFoundException( root.expectTagAttribute!int("doesnt-exist:bar", "attrNS:X") ); } /++ Lookup a child tag by name, and retrieve all values from it. This just like using `getTag()`.`values`, except if the tag isn't found, it safely returns null (or an optional array of default values) instead of a dereferencing null error. Note that, unlike `getValue`, this doesn't discriminate by the value's type. It simply returns all values of a single tag as a `Value[]`. If you'd prefer an exception thrown when the tag isn't found, use `expectTag`.`values` instead. +/ Value[] getTagValues(string fullTagName, Value[] defaultValues = null) { auto tag = getTag(fullTagName); if(tag) return tag.values; else return defaultValues; } /// @("getTagValues") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo 1 "a" 2 "b" foo 3 "c" 4 "d" // getTagValues considers this to override the first foo `); assert( root.getTagValues("foo") == [Value(3), Value("c"), Value(4), Value("d")] ); // Tag not found // If you'd prefer an exception, use `expectTag.values` instead. assert( root.getTagValues("doesnt-exist") is null ); assert( root.getTagValues("doesnt-exist", [ Value(999), Value("Not found") ]) == [ Value(999), Value("Not found") ] ); } /++ Lookup a child tag by name, and retrieve all attributes in a chosen (or default) namespace from it. This just like using `getTag()`.`attributes` (or `getTag()`.`namespace[...]`.`attributes`, or `getTag()`.`all`.`attributes`), except if the tag isn't found, it safely returns an empty range instead of a dereferencing null error. If provided, the `attributeNamespace` parameter can be either the name of a namespace, or an empty string for the default namespace (the default), or `"*"` to retreive attributes from all namespaces. Note that, unlike `getAttributes`, this doesn't discriminate by the value's type. It simply returns the usual `attributes` range. If you'd prefer an exception thrown when the tag isn't found, use `expectTag`.`attributes` instead. +/ auto getTagAttributes(string fullTagName, string attributeNamespace = null) { auto tag = getTag(fullTagName); if(tag) { if(attributeNamespace && attributeNamespace in tag.namespaces) return tag.namespaces[attributeNamespace].attributes; else if(attributeNamespace == "*") return tag.all.attributes; else return tag.attributes; } return AttributeRange(null, null, false); } /// @("getTagAttributes") unittest { import std.exception; import sdlang.parser; auto root = parseSource(` foo X=1 X=2 // getTagAttributes considers this to override the first foo foo X1=3 X2="c" namespace:bar=7 X3=4 X4="d" `); auto fooAttrs = root.getTagAttributes("foo"); assert( !fooAttrs.empty ); assert( fooAttrs.length == 4 ); assert( fooAttrs[0].name == "X1" && fooAttrs[0].value == Value(3) ); assert( fooAttrs[1].name == "X2" && fooAttrs[1].value == Value("c") ); assert( fooAttrs[2].name == "X3" && fooAttrs[2].value == Value(4) ); assert( fooAttrs[3].name == "X4" && fooAttrs[3].value == Value("d") ); fooAttrs = root.getTagAttributes("foo", "namespace"); assert( !fooAttrs.empty ); assert( fooAttrs.length == 1 ); assert( fooAttrs[0].name == "bar" && fooAttrs[0].value == Value(7) ); fooAttrs = root.getTagAttributes("foo", "*"); assert( !fooAttrs.empty ); assert( fooAttrs.length == 5 ); assert( fooAttrs[0].name == "X1" && fooAttrs[0].value == Value(3) ); assert( fooAttrs[1].name == "X2" && fooAttrs[1].value == Value("c") ); assert( fooAttrs[2].name == "bar" && fooAttrs[2].value == Value(7) ); assert( fooAttrs[3].name == "X3" && fooAttrs[3].value == Value(4) ); assert( fooAttrs[4].name == "X4" && fooAttrs[4].value == Value("d") ); // Tag not found // If you'd prefer an exception, use `expectTag.attributes` instead. assert( root.getTagValues("doesnt-exist").empty ); } @("*: Disallow wildcards for names") unittest { import std.exception; import std.math; import sdlang.parser; auto root = parseSource(` foo 1 X=2 ns:foo 3 ns:X=4 `); auto foo = root.getTag("foo"); auto nsfoo = root.getTag("ns:foo"); // Sanity check assert( foo !is null ); assert( foo.name == "foo" ); assert( foo.namespace == "" ); assert( nsfoo !is null ); assert( nsfoo.name == "foo" ); assert( nsfoo.namespace == "ns" ); assert( foo.getValue !int() == 1 ); assert( foo.expectValue !int() == 1 ); assert( nsfoo.getValue !int() == 3 ); assert( nsfoo.expectValue!int() == 3 ); assert( root.getTagValue !int("foo") == 1 ); assert( root.expectTagValue!int("foo") == 1 ); assert( root.getTagValue !int("ns:foo") == 3 ); assert( root.expectTagValue!int("ns:foo") == 3 ); assert( foo.getAttribute !int("X") == 2 ); assert( foo.expectAttribute !int("X") == 2 ); assert( nsfoo.getAttribute !int("ns:X") == 4 ); assert( nsfoo.expectAttribute!int("ns:X") == 4 ); assert( root.getTagAttribute !int("foo", "X") == 2 ); assert( root.expectTagAttribute!int("foo", "X") == 2 ); assert( root.getTagAttribute !int("ns:foo", "ns:X") == 4 ); assert( root.expectTagAttribute!int("ns:foo", "ns:X") == 4 ); // No namespace assertThrown!ArgumentException( root.getTag ("*") ); assertThrown!ArgumentException( root.expectTag("*") ); assertThrown!ArgumentException( root.getTagValue !int("*") ); assertThrown!ArgumentException( root.expectTagValue!int("*") ); assertThrown!ArgumentException( foo.getAttribute !int("*") ); assertThrown!ArgumentException( foo.expectAttribute !int("*") ); assertThrown!ArgumentException( root.getTagAttribute !int("*", "X") ); assertThrown!ArgumentException( root.expectTagAttribute!int("*", "X") ); assertThrown!ArgumentException( root.getTagAttribute !int("foo", "*") ); assertThrown!ArgumentException( root.expectTagAttribute!int("foo", "*") ); // With namespace assertThrown!ArgumentException( root.getTag ("ns:*") ); assertThrown!ArgumentException( root.expectTag("ns:*") ); assertThrown!ArgumentException( root.getTagValue !int("ns:*") ); assertThrown!ArgumentException( root.expectTagValue!int("ns:*") ); assertThrown!ArgumentException( nsfoo.getAttribute !int("ns:*") ); assertThrown!ArgumentException( nsfoo.expectAttribute !int("ns:*") ); assertThrown!ArgumentException( root.getTagAttribute !int("ns:*", "ns:X") ); assertThrown!ArgumentException( root.expectTagAttribute!int("ns:*", "ns:X") ); assertThrown!ArgumentException( root.getTagAttribute !int("ns:foo", "ns:*") ); assertThrown!ArgumentException( root.expectTagAttribute!int("ns:foo", "ns:*") ); // With wildcard namespace assertThrown!ArgumentException( root.getTag ("*:*") ); assertThrown!ArgumentException( root.expectTag("*:*") ); assertThrown!ArgumentException( root.getTagValue !int("*:*") ); assertThrown!ArgumentException( root.expectTagValue!int("*:*") ); assertThrown!ArgumentException( nsfoo.getAttribute !int("*:*") ); assertThrown!ArgumentException( nsfoo.expectAttribute !int("*:*") ); assertThrown!ArgumentException( root.getTagAttribute !int("*:*", "*:X") ); assertThrown!ArgumentException( root.expectTagAttribute!int("*:*", "*:X") ); assertThrown!ArgumentException( root.getTagAttribute !int("*:foo", "*:*") ); assertThrown!ArgumentException( root.expectTagAttribute!int("*:foo", "*:*") ); } 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, `sdlang.exception.ValidationException` /// 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 ValidationException("Root tags cannot have any values, only child tags."); if(allAttributes.length > 0) throw new ValidationException("Root tags cannot have any attributes, only child tags."); if(_namespace != "") throw new ValidationException("Root tags cannot have a namespace."); foreach(tag; allTags) tag.toSDLString(sink, indent, indentLevel); } /// Output this entire tag in SDL format. Does $(B $(I 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 ValidationException("Anonymous tags must have at least one value."); if(_name == "" && _namespace != "") throw new ValidationException("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"); } /// Outputs full information on the tag. 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(unittest) { 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); } } @("*: Test sdlang ast") unittest { import std.exception; import sdlang.parser; 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!DOMRangeException(root.tags["foobar"]); assertThrown!DOMRangeException(root.all.tags["foobar"]); assertThrown!DOMRangeException(root.attributes["foobar"]); assertThrown!DOMRangeException(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!DOMRangeException(root.namespaces["foobar"].tags["foobar"]); //assertThrown!DOMRangeException(root.namespaces["foobar"].attributes["foobar"]); bool didCatch = false; try auto x = root.namespaces["foobar"].tags["foobar"]; catch(DOMRangeException e) didCatch = true; assert(didCatch); didCatch = false; try auto x = root.namespaces["foobar"].attributes["foobar"]; catch(DOMRangeException 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[])[]); // Test clone() auto rootClone = root.clone(); assert(rootClone !is root); assert(rootClone.parent is null); assert(rootClone.name == root.name); assert(rootClone.namespace == root.namespace); assert(rootClone.location == root.location); assert(rootClone.values == root.values); assert(rootClone.toSDLDocument() == root.toSDLDocument()); auto peopleClone = people.clone(); assert(peopleClone !is people); assert(peopleClone.parent is null); assert(peopleClone.name == people.name); assert(peopleClone.namespace == people.namespace); assert(peopleClone.location == people.location); assert(peopleClone.values == people.values); assert(peopleClone.toSDLString() == people.toSDLString()); } // Regression test, issue #11: https://github.com/Abscissa/SDLang-D/issues/11 @("*: Regression test issue #11") unittest { import sdlang.parser; 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); }