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