1 module text.xml.Tree;
2 
3 import boilerplate;
4 import dxml.parser;
5 import std.algorithm;
6 import std.range;
7 import std.typecons;
8 import text.xml.Writer;
9 
10 alias Attribute = Tuple!(string, "name", string, "value");
11 
12 /**
13  * This struct holds a list of XML attributes as name/value pairs.
14  */
15 struct Attributes
16 {
17 
18     private Attribute[] attributes;
19 
20     private alias lookup = (Attribute attr, string name) => attr.name == name;
21 
22     public this(R)(R range)
23     if (isInputRange!R && is(ElementType!R == Attribute))
24     {
25         this = range;
26     }
27 
28     public typeof(this) opAssign(R)(R range)
29     if (isInputRange!R && is(ElementType!R == Attribute))
30     {
31         this.attributes = range.array;
32         return this;
33     }
34 
35     public inout(Attribute)[] opIndex() inout @nogc nothrow pure @safe
36     {
37         return this.attributes;
38     }
39 
40     public string opIndex(string name) nothrow pure @safe
41     in (!this.attributes.find!lookup(name).empty, "Attribute not found")
42     {
43         return this.attributes.find!lookup(name).front.value;
44     }
45 
46     public ref string opIndexAssign(string value, string name) pure @safe
47     {
48         auto result = this.attributes.find!lookup(name);
49 
50         if (result.empty)
51         {
52             this.attributes ~= Attribute(name, value);
53             return this.attributes.back.value;
54         }
55         return result.front.value = value;
56     }
57 
58     public bool opBinaryRight(string op : "in")(string name) const
59     {
60         return this.attributes.canFind!lookup(name);
61     }
62 
63     public void free()
64     {
65         import core.memory : GC;
66 
67         GC.free(this.attributes.ptr);
68     }
69 }
70 
71 /**
72  * This struct represents an XML node.
73  */
74 struct XmlNode
75 {
76 
77     enum Type : EntityType
78     {
79         cdata = EntityType.cdata,
80         comment = EntityType.comment,
81         text = EntityType.text,
82         pi = EntityType.pi,
83         element = EntityType.elementStart,
84     }
85 
86     private Type type_;
87 
88     private string tag_;
89 
90     public XmlNode[] children;
91 
92     public Attributes attributes;
93 
94     public @property string tag() const @nogc nothrow pure @safe
95     {
96         return this.tag_;
97     }
98 
99     public @property Type type() const @nogc nothrow pure @safe
100     {
101         return this.type_;
102     }
103 
104     public auto findChildren(string tag) @nogc pure @safe
105     {
106         import std.traits : CopyConstness, Unqual;
107 
108         // Use mixin template to work around template symbol issue with
109         // `FindChildrenRange(T)` instantiating itself with `const(T)`.
110         template FindChildrenRangeImpl(T)
111         {
112             private T[] children;
113 
114             private string tag;
115 
116             @disable this();
117 
118             public this(T[] children, string tag)
119             {
120                 this.children = children;
121                 this.tag = tag;
122                 prime;
123             }
124 
125             public @property bool empty() const @nogc nothrow pure @safe
126             {
127                 return this.children.empty;
128             }
129 
130             public void popFront() @nogc nothrow pure @safe
131             {
132                 this.children.popFront;
133                 prime;
134             }
135 
136             public @property T front() @nogc nothrow pure @safe
137             in (!empty)
138             {
139                 return this.children.front;
140             }
141 
142             public @property auto save() const @nogc nothrow pure @safe
143             {
144                 return ConstFindChildrenRange(children, tag);
145             }
146 
147             private void prime() @nogc nothrow pure @safe
148             {
149                 while (!empty && (front.type_ != Type.element || front.tag_ != this.tag))
150                 {
151                     this.children.popFront;
152                 }
153             }
154         }
155 
156         static struct ConstFindChildrenRange
157         {
158             mixin FindChildrenRangeImpl!(const XmlNode);
159         }
160 
161         static struct FindChildrenRange
162         {
163             mixin FindChildrenRangeImpl!XmlNode;
164         }
165 
166         return FindChildrenRange(this.children, tag);
167     }
168 
169     public Nullable!XmlNode findChild(string tag) @nogc pure @safe
170     {
171         auto result = findChildren(tag);
172 
173         return result.empty ? typeof(return)() : nullable(result.front);
174     }
175 
176     public @property string text() const pure @safe
177     {
178         if (this.type == Type.text)
179         {
180             return this.tag;
181         }
182         return this.children.map!(child => child.text).join;
183     }
184 
185     public string toString() const
186     {
187         auto sink = appender!string();
188         auto writer = customXmlWriter!(No.pretty)(sink);
189 
190         writer.write(this);
191         return sink.data;
192     }
193 
194     public void toString(scope void delegate(const(char)[]) sink) const
195     {
196         auto writer = customXmlWriter!(No.pretty)(sink);
197 
198         writer.write(this);
199     }
200 
201     public XmlNode addAttribute(string name, string value)
202     {
203         this.attributes.attributes ~= Attribute(name, value);
204         return this;
205     }
206 
207     public void free()
208     {
209         import core.memory : GC;
210 
211         this.children.each!"a.free";
212         GC.free(this.children.ptr);
213         this.attributes.free;
214     }
215 }