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