diff options
Diffstat (limited to 'src/sdlang/ast.d')
-rw-r--r-- | src/sdlang/ast.d | 1275 |
1 files changed, 1193 insertions, 82 deletions
diff --git a/src/sdlang/ast.d b/src/sdlang/ast.d index 7ad1c30..87dd0bd 100644 --- a/src/sdlang/ast.d +++ b/src/sdlang/ast.d @@ -9,13 +9,6 @@ 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; @@ -27,7 +20,7 @@ class Attribute 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 + /// parent tag by calling `Tag.add(...)`, or by passing it to /// the parent tag's constructor. @property Tag parent() { @@ -35,11 +28,20 @@ class Attribute } 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; } - /// Not particularly efficient, but it works. + ///ditto @property void namespace(string value) { if(_parent && _namespace != value) @@ -61,12 +63,22 @@ class Attribute } private string _name; - /// Not including namespace. Use 'fullName' if you want the namespace included. + /++ + 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; } - /// Not the most efficient, but it works. + ///ditto @property void name(string value) { if(_parent && _name != value) @@ -96,9 +108,17 @@ class Attribute _name = value; } + /// This tag's name, including namespace if one exists. + deprecated("Use 'getFullName().toString()'") @property string fullName() { - return _namespace==""? _name : text(_namespace, ":", _name); + 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)) @@ -117,7 +137,14 @@ class Attribute this.value = value; } - /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// 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() { @@ -190,14 +217,31 @@ class Attribute } } +/// 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 + /// parent tag by calling `Tag.add(...)`, or by passing it to /// the parent tag's constructor. @property Tag parent() { @@ -205,13 +249,24 @@ class Tag } 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; } - /// Not particularly efficient, but it works. + ///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 @@ -231,18 +286,31 @@ class Tag } private string _name; - /// Not including namespace. Use 'fullName' if you want the namespace included. + /++ + 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; } - /// Not the most efficient, but it works. + ///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] @@ -259,6 +327,7 @@ class Tag _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; } @@ -267,11 +336,18 @@ class Tag } /// This tag's name, including namespace if one exists. + deprecated("Use 'getFullName().toString()'") @property string fullName() { - return _namespace==""? _name : text(_namespace, ":", _name); + 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. @@ -307,6 +383,15 @@ class Tag 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. @@ -318,8 +403,8 @@ class Tag 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 + /// Returns `this` for chaining. + /// Throws `ValidationException` if trying to add an Attribute or Tag /// that already has a parent. Tag add(Value val) { @@ -342,7 +427,7 @@ class Tag { if(attr._parent) { - throw new SDLangValidationException( + throw new ValidationException( "Attribute is already attached to a parent tag. "~ "Use Attribute.remove() before adding it to another tag." ); @@ -376,7 +461,7 @@ class Tag { if(tag._parent) { - throw new SDLangValidationException( + throw new ValidationException( "Tag is already attached to a parent tag. "~ "Use Tag.remove() before adding it to another tag." ); @@ -405,7 +490,7 @@ class Tag return this; } - /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// Removes `this` from its parent, if any. Returns `this` for chaining. /// Inefficient ATM, but it works. Tag remove() { @@ -488,6 +573,7 @@ class Tag frontIndex = 0; if( + tag !is null && namespace in mixin("tag."~membersGrouped) && name in mixin("tag."~membersGrouped~"[namespace]") ) @@ -506,7 +592,7 @@ class Tag @property bool empty() { - return frontIndex == endIndex; + return tag is null || frontIndex == endIndex; } private size_t frontIndex; @@ -517,7 +603,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -530,7 +616,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -565,7 +651,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -573,7 +659,7 @@ class Tag T opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); return mixin("tag."~membersGrouped~"[namespace][name][frontIndex+index]"); } @@ -595,14 +681,20 @@ class Tag 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"); + if(tag is null) + endIndex = 0; else - initialEndIndex = 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; + endIndex = initialEndIndex; + } } invariant() @@ -615,7 +707,7 @@ class Tag @property bool empty() { - return frontIndex == endIndex; + return tag is null || frontIndex == endIndex; } private size_t frontIndex; @@ -626,7 +718,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -639,7 +731,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -676,7 +768,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -684,7 +776,7 @@ class Tag T opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(namespace == "*") return mixin("tag."~allMembers~"[ frontIndex+index ]"); @@ -697,7 +789,7 @@ class Tag { if(frontIndex != 0 || endIndex != initialEndIndex) { - throw new SDLangRangeException( + 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 "~ @@ -706,10 +798,10 @@ class Tag } if(!isMaybe && empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(!isMaybe && name !in this) - throw new SDLangRangeException(`No such `~T.stringof~` named: "`~name~`"`); + throw new DOMRangeException(tag, `No such `~T.stringof~` named: "`~name~`"`); return ThisNamedMemberRange(tag, namespace, name, updateId); } @@ -718,7 +810,7 @@ class Tag { if(frontIndex != 0 || endIndex != initialEndIndex) { - throw new SDLangRangeException( + 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 "~ @@ -726,6 +818,9 @@ class Tag ); } + if(tag is null) + return false; + return namespace in mixin("tag."~membersGrouped) && name in mixin("tag."~membersGrouped~"[namespace]") && @@ -769,7 +864,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -782,7 +877,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -818,7 +913,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -826,7 +921,7 @@ class Tag NamespaceAccess opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); auto namespace = tag.allNamespaces[frontIndex+index]; return NamespaceAccess( @@ -839,10 +934,10 @@ class Tag NamespaceAccess opIndex(string namespace) { if(!isMaybe && empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(!isMaybe && namespace !in this) - throw new SDLangRangeException(`No such namespace: "`~namespace~`"`); + throw new DOMRangeException(tag, `No such namespace: "`~namespace~`"`); return NamespaceAccess( namespace, @@ -866,7 +961,7 @@ class Tag } } - struct NamespaceAccess + static struct NamespaceAccess { string name; AttributeRange attributes; @@ -879,25 +974,66 @@ class Tag static assert(isRandomAccessRange!TagRange); static assert(isRandomAccessRange!NamespaceRange); - /// Access all attributes that don't have a namespace + /++ + 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 + /++ + 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. + /++ + 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" @@ -942,14 +1078,972 @@ class Tag } } - /// Access 'attributes', 'tags', 'namespaces' and 'all' like normal, + /// 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. + /// 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; @@ -981,9 +2075,10 @@ class Tag return allTags == t.allTags; } - /// Treats 'this' as the root tag. Note that root tags cannot have + /// 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. + /// 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; @@ -996,21 +2091,21 @@ class Tag if(isOutputRange!(Sink,char)) { if(values.length > 0) - throw new SDLangValidationException("Root tags cannot have any values, only child tags."); + throw new ValidationException("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."); + throw new ValidationException("Root tags cannot have any attributes, only child tags."); if(_namespace != "") - throw new SDLangValidationException("Root tags cannot have a 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 *not* treat 'this' as + /// 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. + /// document, use `toSDLDocument` instead. string toSDLString()(string indent="\t", int indentLevel=0) { Appender!string sink; @@ -1023,10 +2118,10 @@ class Tag if(isOutputRange!(Sink,char)) { if(_name == "" && values.length == 0) - throw new SDLangValidationException("Anonymous tags must have at least one value."); + throw new ValidationException("Anonymous tags must have at least one value."); if(_name == "" && _namespace != "") - throw new SDLangValidationException("Anonymous tags cannot have a namespace."); + throw new ValidationException("Anonymous tags cannot have a namespace."); // Indent foreach(i; 0..indentLevel) @@ -1080,7 +2175,7 @@ class Tag sink.put("\n"); } - /// Not the most efficient, but it works. + /// Outputs full information on the tag. string toDebugString() { import std.algorithm : sort; @@ -1129,7 +2224,7 @@ class Tag } } -version(sdlangUnittest) +version(unittest) { private void testRandomAccessRange(R, E)(R range, E[] expected, bool function(E, E) equals=null) { @@ -1269,12 +2364,11 @@ version(sdlangUnittest) } } -version(sdlangUnittest) +@("*: Test sdlang ast") unittest { + import std.exception; import sdlang.parser; - writeln("Unittesting sdlang ast..."); - stdout.flush(); Tag root; root = parseSource(""); @@ -1473,26 +2567,26 @@ unittest 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"]); + 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!SDLangRangeException(root.namespaces["foobar"].tags["foobar"]); - //assertThrown!SDLangRangeException(root.namespaces["foobar"].attributes["foobar"]); + //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(SDLangRangeException e) + catch(DOMRangeException e) didCatch = true; assert(didCatch); didCatch = false; try auto x = root.namespaces["foobar"].attributes["foobar"]; - catch(SDLangRangeException e) + catch(DOMRangeException e) didCatch = true; assert(didCatch); @@ -1799,16 +2893,33 @@ unittest 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 -version(sdlangUnittest) +@("*: Regression test issue #11") unittest { import sdlang.parser; - writeln("ast: Regression test issue #11..."); - stdout.flush(); - + auto root = parseSource( `// a`); |