1 // Written in the D programming language.
2 
3 /**
4 This module contains implementation DOM - document object model.
5 
6 Port of CoolReader Engine written in C++.
7 
8 Synopsis:
9 
10 ----
11 import dlangui.core.dom;
12 
13 ----
14 
15 Copyright: Vadim Lopatin, 2015
16 License:   Boost License 1.0
17 Authors:   Vadim Lopatin, coolreader.org@gmail.com
18 */
19 module dlangui.core.dom;
20 
21 import dlangui.core.collections;
22 
23 import std.traits;
24 import std.conv : to;
25 import std.string : startsWith, endsWith;
26 import std.array : empty;
27 import std.algorithm : equal;
28 
29 // Namespace, element tag and attribute names are stored as numeric ids for better performance and lesser memory consumption.
30 
31 /// id type for interning namespaces
32 alias ns_id = short;
33 /// id type for interning element names
34 alias elem_id = int;
35 /// id type for interning attribute names
36 alias attr_id = short;
37 
38 
39 /// Base class for DOM nodes
40 class Node {
41 private:
42     Node _parent;
43     Document _document;
44 public:
45     /// returns parent node
46     @property Node parent() { return _parent; }
47     /// returns document node
48     @property Document document() { return _document; }
49 
50     /// return element tag id
51     @property elem_id id() { return 0; }
52     /// return element namespace id
53     @property ns_id nsid() { return 0; }
54     /// return element tag name
55     @property string name() { return document.tagName(id); }
56     /// return element namespace name
57     @property string nsname() { return document.nsName(nsid); }
58 
59     // node properties
60 
61     /// returns true if node is text
62     @property bool isText() { return false; }
63     /// returns true if node is element
64     @property bool isElement() { return false; }
65     /// returns true if node has child nodes
66     @property bool hasChildren() { return false; }
67 
68     // attributes
69 
70     /// returns attribute count
71     @property int attrCount() { return 0; }
72 
73     /// get attribute by index
74     Attribute attr(int index) { return null; }
75     /// get attribute by namespace and attribute ids
76     Attribute attr(ns_id nsid, attr_id attrid) { return null; }
77     /// get attribute by namespace and attribute names
78     Attribute attr(string nsname, string attrname) { return attr(_document.nsId(nsname), _document.attrId(attrname)); }
79 
80     /// set attribute value by namespace and attribute ids
81     Attribute setAttr(ns_id nsid, attr_id attrid, string value) { assert(false); }
82     /// set attribute value by namespace and attribute names
83     Attribute setAttr(string nsname, string attrname, string value) { return setAttr(_document.nsId(nsname), _document.attrId(attrname), value); }
84     /// get attribute value by namespace and attribute ids
85     string attrValue(ns_id nsid, attr_id attrid) { return null; }
86     /// get attribute value by namespace and attribute ids
87     string attrValue(string nsname, string attrname) { return attrValue(_document.nsId(nsname), _document.attrId(attrname)); }
88     /// returns true if node has attribute with specified name
89     bool hasAttr(string attrname) {
90         return hasAttr(document.attrId(attrname));
91     }
92     /// returns true if node has attribute with specified id
93     bool hasAttr(attr_id attrid) {
94         if (Attribute a = attr(Ns.any, attrid))
95             return true;
96         return false;
97     }
98 
99     // child nodes
100 
101     /// returns child node count
102     @property int childCount() { return 0; }
103     /// returns child node by index
104     @property Node child(int index) { return null; }
105     /// returns first child node
106     @property Node firstChild() { return null; }
107     /// returns last child node
108     @property Node lastChild() { return null; }
109 
110     /// find child node, return its index if found, -1 if not found or not child of this node
111     int childIndex(Node child) { return -1; }
112     /// return node index in parent's child node collection, -1 if not found
113     @property int index() { return _parent ? _parent.childIndex(this) : -1; }
114 
115     /// returns child node by index and optionally compares its tag id, returns null if child with this index is not an element or id does not match
116     Element childElement(int index, elem_id id = 0) {
117         Element res = cast(Element)child(index);
118         if (res && (id == 0 || res.id == id))
119             return res;
120         return null;
121     }
122 
123     /// append text child
124     Node appendText(dstring s, int index = -1) { assert(false); }
125     /// append element child - by namespace and tag names
126     Node appendElement(string ns, string tag, int index = -1) { return appendElement(_document.nsId(ns), _document.tagId(tag), index); }
127     /// append element child - by namespace and tag ids
128     Node appendElement(ns_id ns, elem_id tag, int index = -1) { assert(false); }
129 
130     // Text methods
131 
132     /// node text
133     @property dstring text() { return null; }
134     /// ditto
135     @property void text(dstring s) { }
136 
137 }
138 
139 /// Text node
140 class Text : Node {
141 private:
142     dstring _text;
143     this(Document doc, dstring text = null) {
144         _document = doc;
145         _text = text;
146     }
147 public:
148     /// node text
149     override @property dstring text() { return _text; }
150     /// ditto
151     override @property void text(dstring s) { _text = s; }
152 }
153 
154 /// Element node
155 class Element : Node {
156 private:
157     Collection!Node _children;
158     Collection!Attribute _attrs;
159     elem_id _id; // element tag id
160     ns_id _ns; // element namespace id
161 
162     this(Document doc, ns_id ns, elem_id id) {
163         _document = doc;
164         _ns = ns;
165         _id = id;
166     }
167 public:
168 
169     /// return element tag id
170     override @property elem_id id() { return _id; }
171     /// return element namespace id
172     override @property ns_id nsid() { return _ns; }
173 
174     // Attributes
175 
176     /// returns attribute count
177     override @property int attrCount() { return cast(int)_attrs.length; }
178 
179     /// get attribute by index
180     override Attribute attr(int index) { return index >= 0 && index < _attrs.length ? _attrs[index] : null; }
181     /// get attribute by namespace and attribute ids
182     override Attribute attr(ns_id nsid, attr_id attrid) {
183         foreach (a; _attrs)
184             if ((nsid == Ns.any || nsid == a.nsid) && attrid == a.id)
185                 return a;
186         return null;
187     }
188     /// get attribute by namespace and attribute names
189     override Attribute attr(string nsname, string attrname) { return attr(_document.nsId(nsname), _document.attrId(attrname)); }
190 
191     /// set attribute value by namespace and attribute ids
192     override Attribute setAttr(ns_id nsid, attr_id attrid, string value) {
193         Attribute a = attr(nsid, attrid);
194         if (!a) {
195             a = new Attribute(this, nsid, attrid, value);
196             _attrs.add(a);
197         } else {
198             a.value = value;
199         }
200         return a;
201     }
202     /// set attribute value by namespace and attribute names
203     override Attribute setAttr(string nsname, string attrname, string value) { return setAttr(_document.nsId(nsname), _document.attrId(attrname), value); }
204     /// get attribute value by namespace and attribute ids
205     override string attrValue(ns_id nsid, attr_id attrid) {
206         if (Attribute a = attr(nsid, attrid))
207             return a.value;
208         return null;
209     }
210     /// get attribute value by namespace and attribute ids
211     override string attrValue(string nsname, string attrname) { return attrValue(_document.nsId(nsname), _document.attrId(attrname)); }
212 
213     // child nodes
214 
215     /// returns child node count
216     override @property int childCount() { return cast(int)_children.length; }
217     /// returns child node by index
218     override @property Node child(int index) { return index >= 0 && index < _children.length ? _children[index] : null; }
219     /// returns first child node
220     override @property Node firstChild() { return _children.length > 0 ? _children[0] : null; }
221     /// returns last child node
222     override @property Node lastChild() { return _children.length > 0 ? _children[_children.length - 1] : null; }
223     /// find child node, return its index if found, -1 if not found or not child of this node
224     override int childIndex(Node child) {
225         for (int i = 0; i < _children.length; i++)
226             if (child is _children[i])
227                 return i;
228         return -1;
229     }
230 
231     /// append text child
232     override Node appendText(dstring s, int index = -1) {
233         Node item = document.createText(s);
234         _children.add(item, index >= 0 ? index : size_t.max);
235         return item;
236     }
237     /// append element child - by namespace and tag ids
238     override Node appendElement(ns_id ns, elem_id tag, int index = -1) {
239         Node item = document.createElement(ns, tag);
240         _children.add(item, index >= 0 ? index : size_t.max);
241         return item;
242     }
243     /// append element child - by namespace and tag names
244     override Node appendElement(string ns, string tag, int index = -1) { return appendElement(_document.nsId(ns), _document.tagId(tag), index); }
245 }
246 
247 /// Document node
248 class Document : Element {
249 public:
250     this() {
251         super(null, 0, 0);
252         _elemIds.initialize!Tag();
253         _attrIds.initialize!Attr();
254         _nsIds.initialize!Ns();
255         _document = this;
256     }
257     /// create text node
258     Text createText(dstring text) {
259         return new Text(this, text);
260     }
261     /// create element node by namespace and tag ids
262     Element createElement(ns_id ns, elem_id tag) {
263         return new Element(this, ns, tag);
264     }
265     /// create element node by namespace and tag names
266     Element createElement(string ns, string tag) {
267         return new Element(this, nsId(ns), tagId(tag));
268     }
269 
270     // Ids
271 
272     /// return name for element tag id
273     string tagName(elem_id id) {
274         return _elemIds[id];
275     }
276     /// return name for namespace id
277     string nsName(ns_id id) {
278         return _nsIds[id];
279     }
280     /// return name for attribute id
281     string attrName(ns_id id) {
282         return _attrIds[id];
283     }
284     /// get id for element tag name
285     elem_id tagId(string s) {
286         if (s.empty)
287             return 0;
288         return _elemIds.intern(s);
289     }
290     /// get id for namespace name
291     ns_id nsId(string s) {
292         if (s.empty)
293             return 0;
294         return _nsIds.intern(s);
295     }
296     /// get id for attribute name
297     attr_id attrId(string s) {
298         if (s.empty)
299             return 0;
300         return _attrIds.intern(s);
301     }
302 private:
303     IdentMap!(elem_id) _elemIds;
304     IdentMap!(attr_id) _attrIds;
305     IdentMap!(ns_id) _nsIds;
306 }
307 
308 class Attribute {
309 private:
310     attr_id _id;
311     ns_id _nsid;
312     string _value;
313     Node _parent;
314     this(Node parent, ns_id nsid, attr_id id, string value) {
315         _parent = parent;
316         _nsid = nsid;
317         _id = id;
318         _value = value;
319     }
320 public:
321     /// Parent element which owns this attribute
322     @property Node parent() { return _parent; }
323     /// Parent element document
324     @property Document document() { return _parent.document; }
325 
326     /// get attribute id
327     @property attr_id id() { return _id; }
328     /// get attribute namespace id
329     @property ns_id nsid() { return _nsid; }
330     /// get attribute tag name
331     @property string name() { return document.tagName(_id); }
332     /// get attribute namespace name
333     @property string nsname() { return document.nsName(_nsid); }
334 
335     /// get attribute value
336     @property string value() { return _value; }
337     /// set attribute value
338     @property void value(string s) { _value = s; }
339 }
340 
341 /// remove trailing _ from string, e.g. "body_" -> "body"
342 private string removeTrailingUnderscore(string s) {
343     if (s.endsWith("_"))
344         return s[0..$-1];
345     return s;
346 }
347 
348 /// String identifier to Id map - for interning strings
349 struct IdentMap(ident_t) {
350     /// initialize with elements of enum
351     void initialize(E)() if (is(E == enum)) {
352         foreach(member; EnumMembers!E) {
353             static if (member.to!int > 0) {
354                 //pragma(msg, "interning string '" ~ removeTrailingUnderscore(member.to!string) ~ "' for " ~ E.stringof);
355                 intern(removeTrailingUnderscore(member.to!string), member);
356             }
357         }
358     }
359     /// intern string - return ID assigned for it
360     ident_t intern(string s, ident_t id = 0) {
361         if (auto p = s in _stringToId)
362             return *p;
363         ident_t res;
364         if (id > 0) {
365             if (_nextId <= id)
366                 _nextId = cast(ident_t)(id + 1);
367             res = id;
368         } else {
369            res = _nextId++;
370         }
371         _idToString[res] = s;
372         _stringToId[s] = res;
373         return res;
374     }
375     /// lookup id for string, return 0 if string is not found
376     ident_t opIndex(string s) {
377         if (s.empty)
378             return 0;
379         if (auto p = s in _stringToId)
380             return *p;
381         return 0;
382     }
383     /// lookup name for id, return null if not found
384     string opIndex(ident_t id) {
385         if (!id)
386             return null;
387         if (auto p = id in _idToString)
388             return *p;
389         return null;
390     }
391 private:
392     string[ident_t] _idToString;
393     ident_t[string] _stringToId;
394     ident_t _nextId = 1;
395 }
396 
397 /// standard tags
398 enum Tag : elem_id {
399     none,
400     body_,
401     pre,
402     div,
403     span
404 }
405 
406 /// standard attributes
407 enum Attr : attr_id {
408     none,
409     id,
410     class_,
411     style
412 }
413 
414 /// standard namespaces
415 enum Ns : ns_id {
416     any = -1,
417     none = 0,
418     xmlns,
419     xs,
420     xlink,
421     l,
422     xsi
423 }
424 
425 unittest {
426     import std.algorithm : equal;
427     //import std.stdio;
428     IdentMap!(elem_id) map;
429     map.initialize!Tag();
430     //writeln("running DOM unit test");
431     assert(map["pre"] == Tag.pre);
432     assert(map["body"] == Tag.body_);
433     assert(map[Tag.div].equal("div"));
434 
435     Document doc = new Document();
436     auto body_ = doc.appendElement(null, "body");
437     assert(body_.id == Tag.body_);
438     assert(body_.name.equal("body"));
439     auto div = body_.appendElement(null, "div");
440     assert(body_.childCount == 1);
441     assert(div.id == Tag.div);
442     assert(div.name.equal("div"));
443     auto t1 = div.appendText("Some text"d);
444     assert(div.childCount == 1);
445     assert(div.child(0).text.equal("Some text"d));
446     auto t2 = div.appendText("Some more text"d);
447     assert(div.childCount == 2);
448     assert(div.childIndex(t1) == 0);
449     assert(div.childIndex(t2) == 1);
450 
451     div.setAttr(Ns.none, Attr.id, "div_id");
452     assert(div.attrValue(Ns.none, Attr.id).equal("div_id"));
453 
454     destroy(doc);
455 }
456