SDLang-D offers two ways to work with SDLang: DOM style and pull (aka "StAX") style. DOM style is easier and more convenient and can both read and write SDLang. Pull style is faster and more efficient, although it's only used for reading SDLang, not writing it.
This document explains how to use SDLang-D in the DOM style. If you're familiar with pull/StAX style parsing for other languages, such as XML, then SDLang-D's pull parser should be straightforward to understand. See pullParseFile and pullParseSource in the API reference for details and a simple example. You can also see SDLang-D's source as a real-world example, as the DOM tree itself is built using the pull parser, in less than 50 lines of code (just search parser.d
for DOMParser
).
Contents
The list of officially supported D compiler versions is always available in .travis.yml.
The recommended way to use SDLang-D is via DUB. Just add a dependency to sdlang-d
in your project's dub.json
or dub.sdl
file as shown here. Then simply build your project with DUB as usual.
Alternatively, you can git clone
both SDLang-D and the latest versions of libInputVisitor and TaggedAlgebraic, and include -I{path to SDLang-D}/src -I{path to libInputVisitor} -I{path to TaggedAlgebraic}/source
when running the compiler.
Note that prior to DMD 2.071.0, -inline
causes some problems. It causes SDLang-D to segfault when parsing a Base64 value. As of DMD 2.071.0 and up, -inline
works fine.
To use SDLang-D, first import the module sdlang
:
import sdlang;
If you're not using DUB, then you must also include the paths to the sources of SDLang-D and its dependencies when you compile:
rdmd --build-only -I{path to sdlang-d}/src -I{path to libInputVisitor} -I{path to TaggedAlgebraic}/source {other flags} yourProgram.d
If doing your own pull parsing, follow the documentation and example for pullParseFile and pullParseSource.
If using DOM mode instead of the pull parser, the main interface for SDLang-D is the two parse functions:
/// Returns root tag.
Tag parseFile(string filename);
/// Returns root tag. The optional 'filename' parameter can be included
/// so that the SDLang document's filename (if any) can be displayed with
/// any syntax error messages.
Tag parseSource(string source, string filename=null);
Beyond that, your interactions with SDLang-D will be via class Tag
,
struct Attribute
and alias Value
(an instantiation of std.variant.Algebraic).
/+ dub.sdl:
name "example"
dependency "sdlang-d" version="~>0.10.1"
+/
import std.algorithm;
import std.stdio;
import sdlang;
/// To run: dub basicExample.d
int main()
{
Tag root;
try
{
// Or: parseFile("path/to/somefile.sdl");
root = parseSource(`
message "Hello world!" // Required
// Optional, default is "127.0.0.1" port=80
ip-address "192.168.1.100" port=8080
// Uncomment this for an error:
//badSuffix 12Q
misc-values 11 "Up" 3.14 null "On the roof" 22
misc-attrs a=11 a="Up" foo:a=22 flag=true
// Name is required
devs:person "Joe Coder" id=7 {
has-cake true
}
`);
}
catch(ParseException e)
{
// Sample error:
// myFile.sdl(6:17): Error: Invalid integer suffix.
stderr.writeln(e.msg);
return 1;
}
// Basics
auto ipAddress = root.getTagValue!string("ip-address", "127.0.0.1");
auto port = root.getTagAttribute!int("ip-address", "port", 80);
auto message = root.expectTagValue!string("message"); // Throws if not found
writeln(message, " Address is ", ipAddress, ":", port);
// Person tag
Tag person = root.getTag("devs:person");
assert(person.name == "person");
assert(person.namespace == "devs"); // Default namespace is ""
assert(person.getFullName.toString == "devs:person");
if(person !is null)
{
try
writeln("Person's Name: ", person.expectValue!string());
catch(AttributeNotFoundException e)
stderr.writeln(person.location, ": Error: 'person' requires a string value");
int id = person.getAttribute!int("id", 99999);
writeln("Id: ", id);
if(person.getTagValue!bool("has-cake"))
writeln("Yum!");
}
// List top-level tags in all namespaces
// (omit "all" to only search in default namespace)
writeln("------------------------");
root.all.tags.each!( (Tag t) => writeln(t.getFullName) );
// Get values and their types
Tag miscValues = root.getTag("misc-values");
writeln("------------------------");
writeln("All integer values in misc-values:");
miscValues.values
.filter!((Value v) => v.type == typeid(int))
.map!((Value v) => v.get!int)
.each!writeln;
// Misc attributes and range support
Tag miscAttrs = root.getTag("misc-attrs");
writeln("------------------------");
auto allAttrs = miscAttrs.all.attributes;
writeln("All misc-attrs attributes:");
allAttrs.each!(
(Attribute a) => writeln(a.value.type, " ", a.getFullName, "=", a.value)
);
// Add new data to person tag
person.values ~= Value(1.5); // Values is an array
person.add( new Attribute("extras", "has-kid", Value(true)) );
auto childId = new Attribute(null, "id", Value(12));
auto messageCopy = root.getTag("message").clone;
person.add( new Tag("namespace", "person", [Value("Sam Coder")], [childId], [messageCopy]) );
// Output back to SDLang
writeln("------------------------");
writeln(root.toSDLDocument());
return 0;
}
Compile and run:
> dub example.d
Hello world! Address is 192.168.1.100:8080
Person's Name: Joe Coder
Id: 7
Yum!
------------------------
message
ip-address
misc-values
misc-attrs
devs:person
------------------------
All integer values in misc-values:
11
22
------------------------
All misc-attrs attributes:
int a=11
immutable(char)[] a=Up
int foo:a=22
bool flag=true
------------------------
message "Hello world!"
ip-address "192.168.1.100" port=8080
misc-values 11 "Up" 3.14000000000000012434497875802D null "On the roof" 22
misc-attrs a=11 a="Up" foo:a=22 flag=true
devs:person "Joe Coder" 1.5D id=7 extras:has-kid=true {
has-cake true
namespace:person "Sam Coder" id=12 {
message "Hello world!"
}
}
Another example, using the more powerful range-based DOM interfaces instead of the get/expect convenience functions, is in example2.d
. Be aware however, the integer-based indexing might get removed in a later version of SDLang-D.
You can view the full API reference for Tag
and Attribute
, but put simply:
Value is an instantiation of std.variant.Algebraic. See the summary of Value
.
The Tag
and Attribute
APIs work as follows (where {...}
means optional, and |
means or):
// Attribute: ------------------------------------------------------------
// Constructors:
Attribute.this(string namespace, string name, Value value,
Location location = Location(0, 0, 0))
Attribute.this(string name, Value value,
Location location = Location(0, 0, 0))
Attribute.namespace // string: "" if no namespace
Attribute.name // string: "" if anonymous
Attribute.location // Location: filename, line, column and index in original SDLang file
Attribute.value // Value
Attribute.parent // Tag: Read-only
Tag.getFullName().toString() // string: Returns "namespace:name" if there's a namespace
// Tag: ------------------------------------------------------------------
// Constructors:
Tag.this(Tag parent = null)
Tag.this(string namespace, string name,
Value[] values=null, Attribute[] attributes=null, Tag[] children=null)
Tag.this(Tag parent,
string namespace, string name,
Value[] values=null, Attribute[] attributes=null, Tag[] children=null)
Tag.namespace // string: "" if no namespace
Tag.name // string: "" if anonymous
Tag.location // Location: filename, line, column and index in original SDLang file
Tag.values // Value[]
Tag.parent // Tag: Read-only
Tag.getFullName().toString() // string: Returns "namespace:name" if there's a namespace
Tag.remove() // Removes this tag from its parent
Tag.add(Tag | Attribute | Value ) // Adds a member to this tag
Tag.add(Tag[] | Attribute[] | Value[]) // Adds multiple members to this tag
Tag.toSDLDocument // Create a full SDLang document. Call on a root tag.
Tag.toSDLString // A fragment of an SDLang document. Just one tag.
// Convenience functions: ------------------------------------------------
// Accept "namespace:name" notation for all names.
//
// If what you're searching for can't be found:
// "get" functions: Return an optional default value
// "expect" functions: Throw a subclass of DOMNotFoundException
//
// Functions returning one Value or Attribute are templates on value's actual
// type, so you can just use 'int', 'string', etc, no need to use the 'Value'
// algebraic type.
Tag.getTag(name, [defaultVal])
Tag.getValue!T([defaultVal])
Tag.getTagValue!T(name, [defaultVal])
Tag.getAttribute!T(name, [defaultVal])
Tag.getTagAttribute!T(tagName, attrName, [defaultVal])
Tag.expectTag(name)
Tag.expectValue!T()
Tag.expectTagValue!T(name)
Tag.expectAttribute!T(name)
Tag.expectTagAttribute!T(tagName, attrName)
// Returns all values of a tag in a 'Value[]'
Tag.getTagValues(string fullTagName, Value[] defaultValues = null)
// Returns random-access range of all attributes of a tag (defaults to only attrs without a namespace)
Tag.getTagAttributes(string fullTagName, string attributeNamespace = null)
// Full-featured range interfaces: ---------------------------------------
// Optional '.maybe' implies "If I lookup (by string) a name or namespace
// that doesn't exist, then return an empty range instead of throwing."
Tag{.maybe}.namespaces // RandomAccessRange
Tag{.maybe}.namespaces[string or index] // Access child tags/attributes (see below)
Tag{.maybe}.namespaces[startIndex..endIndex] // Slicable
string (in|!in) Tag{.maybe}.namespaces // Check if namespace exists
// Access child tags/attributes:
// - Optional '.maybe' explained above.
// - Optional '.all' means "All namespaces".
// - The default namespace is "".
Tag{.maybe}{.all | .namespaces[string or index]}.tags // RandomAccessRange
Tag{.maybe}{.all | .namespaces[string or index]}.attributes // RandomAccessRange
(tags|attributes)[index] // Normal RandomAccessRange indexing
(tags|attributes)[startIndex..endIndex] // Slicable (but the slice can't use 'in' or '[string]')
string (in|!in) (tags|attributes) // Check if a tag/attribute name exists
(tags|attributes)[string] // Slicable RandomAccessRange of
// tags/attributes with a specific name
All of the interfaces above are shallow. For example:
Tag{.maybe}.namespaces
only contains namespaces used by the current tag's attributes and immediate children - not descendants..maybe
-ness ends when you reach another Tag. So,Tag.maybe.namespaces["invalid-namespace"].tags["invalid-name"]
is ok and won't throw, but the following will throw:Tag.maybe.namespaces["ok-namespace"].tags["ok-name"][0].tags["invalid-name"]
.
Ranges will be invalidated if you add/remove/rename any child tags, attributes or namespaces, on the Tag which the Range operates on. But, if assertions and struct invariants are enabled, then this will be detected and any further attempt to use the invalidated range will throw an assertion failure.
Since this library is designed primarily for reading and writing SDLang files, it's optimized for building and navigating trees rather than manipulating them. Keep in mind that removing or renaming tags, attributes or namespaces may be slow. If you're concerned about speed, it might be best to minimize direct manipulations and prefer using use the SDLang-D data structures as pure input/output.
To generate SDLang, first build up a DOM using Tag.this
, Attribute.this
, Tag.add
, Tag.values
, and Attribute.value
:
auto nameTag = new Tag(null, "name", [Value("my-cool-project")]);
auto root = new Tag(null, null, null, null, [nameTag]);
auto sdlangDependencyTag = new Tag(null, "dependency", [Value("sdlang-d")]);
auto sdlangVersionAttr = new Attribute(null, "version", [Value("~>0.10.1")]);
sdlangDependencyTag.add(sdlangVersionAttr);
root.add(sdlangDependencyTag);
Remember, the root tag is simply used as a collection of tags. As such, its namespace must be blank and it cannot have any values or attributes. It can, however, have any name (which will be ignored), and it is allowed to have a parent (also ignored).
Then, simply call Tag.toSDLDocument()
on your root tag:
import std.file;
std.file.write("dub.sdl", root.toSDLDocument());
Additionally, you can generate just a portion of SDLang, instead of a full ducument, via Tag.toSDLString()
, Attribute.toSDLString()
and toSDLString(Value)
.
The Tag.toSDLDocument()
function and toSDLString()
functions can optionally take an OutputRange sink instead of allocating and returning a string. The Tag-based functions also have optional parameters to customize the indent style and starting depth.