1 /** 2 This is an html DOM implementation, started with cloning 3 what the browser offers in Javascript, but going well beyond 4 it in convenience. 5 6 If you can do it in Javascript, you can probably do it with 7 this module. 8 9 And much more. 10 11 12 Note: some of the documentation here writes html with added 13 spaces. That's because ddoc doesn't bother encoding html output, 14 and adding spaces is easier than using LT macros everywhere. 15 16 17 BTW: this file depends on arsd.characterencodings, so help it 18 correctly read files from the internet. You should be able to 19 get characterencodings.d from the same place you got this file. 20 */ 21 module arsd.dom; 22 23 // FIXME: do parent selector picking in get selector 24 // FIXME: do :has too... or instead, :has is quite nice. 25 26 version(with_arsd_jsvar) 27 import arsd.jsvar; 28 else { 29 enum Scriptable; 30 } 31 32 // this is only meant to be used at compile time, as a filter for opDispatch 33 // lists the attributes we want to allow without the use of .attr 34 bool isConvenientAttribute(string name) { 35 static immutable list = [ 36 "name", "id", "href", "value", 37 "checked", "selected", "type", 38 "src", "content", "pattern", 39 "placeholder", "required", "alt", 40 "rel", 41 ]; 42 foreach(l; list) 43 if(name == l) return true; 44 return false; 45 } 46 47 // FIXME: might be worth doing Element.attrs and taking opDispatch off that 48 // so more UFCS works. 49 50 51 // FIXME: something like <ol>spam <ol> with no closing </ol> should read the second tag as the closer in garbage mode 52 // FIXME: failing to close a paragraph sometimes messes things up too 53 54 // FIXME: it would be kinda cool to have some support for internal DTDs 55 // and maybe XPath as well, to some extent 56 /* 57 we could do 58 meh this sux 59 60 auto xpath = XPath(element); 61 62 // get the first p 63 xpath.p[0].a["href"] 64 */ 65 66 // public import arsd.domconvenience; // merged for now 67 68 /* domconvenience follows { */ 69 70 71 import std..string; 72 73 // the reason this is separated is so I can plug it into D->JS as well, which uses a different base Element class 74 75 import arsd.dom; 76 77 mixin template DomConvenienceFunctions() { 78 79 /// Calls getElementById, but throws instead of returning null if the element is not found. You can also ask for a specific subclass of Element to dynamically cast to, which also throws if it cannot be done. 80 final SomeElementType requireElementById(SomeElementType = Element)(string id, string file = __FILE__, size_t line = __LINE__) 81 if( 82 is(SomeElementType : Element) 83 ) 84 out(ret) { 85 assert(ret !is null); 86 } 87 do { 88 auto e = cast(SomeElementType) getElementById(id); 89 if(e is null) 90 throw new ElementNotFoundException(SomeElementType.stringof, "id=" ~ id, file, line); 91 return e; 92 } 93 94 /// ditto but with selectors instead of ids 95 final SomeElementType requireSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__) 96 if( 97 is(SomeElementType : Element) 98 ) 99 out(ret) { 100 assert(ret !is null); 101 } 102 do { 103 auto e = cast(SomeElementType) querySelector(selector); 104 if(e is null) 105 throw new ElementNotFoundException(SomeElementType.stringof, selector, file, line); 106 return e; 107 } 108 109 110 111 112 /// get all the classes on this element 113 @property string[] classes() { 114 return split(className, " "); 115 } 116 117 /// Adds a string to the class attribute. The class attribute is used a lot in CSS. 118 Element addClass(string c) { 119 if(hasClass(c)) 120 return this; // don't add it twice 121 122 string cn = getAttribute("class"); 123 if(cn.length == 0) { 124 setAttribute("class", c); 125 return this; 126 } else { 127 setAttribute("class", cn ~ " " ~ c); 128 } 129 130 return this; 131 } 132 133 /// Removes a particular class name. 134 Element removeClass(string c) { 135 if(!hasClass(c)) 136 return this; 137 string n; 138 foreach(name; classes) { 139 if(c == name) 140 continue; // cut it out 141 if(n.length) 142 n ~= " "; 143 n ~= name; 144 } 145 146 className = n.strip(); 147 148 return this; 149 } 150 151 /// Returns whether the given class appears in this element. 152 bool hasClass(string c) { 153 string cn = className; 154 155 auto idx = cn.indexOf(c); 156 if(idx == -1) 157 return false; 158 159 foreach(cla; cn.split(" ")) 160 if(cla == c) 161 return true; 162 return false; 163 164 /* 165 int rightSide = idx + c.length; 166 167 bool checkRight() { 168 if(rightSide == cn.length) 169 return true; // it's the only class 170 else if(iswhite(cn[rightSide])) 171 return true; 172 return false; // this is a substring of something else.. 173 } 174 175 if(idx == 0) { 176 return checkRight(); 177 } else { 178 if(!iswhite(cn[idx - 1])) 179 return false; // substring 180 return checkRight(); 181 } 182 183 assert(0); 184 */ 185 } 186 187 188 /* ******************************* 189 DOM Mutation 190 *********************************/ 191 192 /// Removes all inner content from the tag; all child text and elements are gone. 193 void removeAllChildren() 194 out { 195 assert(this.children.length == 0); 196 } 197 do { 198 children = null; 199 } 200 /// convenience function to quickly add a tag with some text or 201 /// other relevant info (for example, it's a src for an <img> element 202 /// instead of inner text) 203 Element addChild(string tagName, string childInfo = null, string childInfo2 = null) 204 in { 205 assert(tagName !is null); 206 } 207 out(e) { 208 assert(e.parentNode is this); 209 assert(e.parentDocument is this.parentDocument); 210 } 211 do { 212 auto e = Element.make(tagName, childInfo, childInfo2); 213 // FIXME (maybe): if the thing is self closed, we might want to go ahead and 214 // return the parent. That will break existing code though. 215 return appendChild(e); 216 } 217 218 /// Another convenience function. Adds a child directly after the current one, returning 219 /// the new child. 220 /// 221 /// Between this, addChild, and parentNode, you can build a tree as a single expression. 222 Element addSibling(string tagName, string childInfo = null, string childInfo2 = null) 223 in { 224 assert(tagName !is null); 225 assert(parentNode !is null); 226 } 227 out(e) { 228 assert(e.parentNode is this.parentNode); 229 assert(e.parentDocument is this.parentDocument); 230 } 231 do { 232 auto e = Element.make(tagName, childInfo, childInfo2); 233 return parentNode.insertAfter(this, e); 234 } 235 236 Element addSibling(Element e) { 237 return parentNode.insertAfter(this, e); 238 } 239 240 Element addChild(Element e) { 241 return this.appendChild(e); 242 } 243 244 /// Convenience function to append text intermixed with other children. 245 /// For example: div.addChildren("You can visit my website by ", new Link("mysite.com", "clicking here"), "."); 246 /// or div.addChildren("Hello, ", user.name, "!"); 247 248 /// See also: appendHtml. This might be a bit simpler though because you don't have to think about escaping. 249 void addChildren(T...)(T t) { 250 foreach(item; t) { 251 static if(is(item : Element)) 252 appendChild(item); 253 else static if (is(isSomeString!(item))) 254 appendText(to!string(item)); 255 else static assert(0, "Cannot pass " ~ typeof(item).stringof ~ " to addChildren"); 256 } 257 } 258 259 ///. 260 Element addChild(string tagName, Element firstChild, string info2 = null) 261 in { 262 assert(firstChild !is null); 263 } 264 out(ret) { 265 assert(ret !is null); 266 assert(ret.parentNode is this); 267 assert(firstChild.parentNode is ret); 268 269 assert(ret.parentDocument is this.parentDocument); 270 //assert(firstChild.parentDocument is this.parentDocument); 271 } 272 do { 273 auto e = Element.make(tagName, "", info2); 274 e.appendChild(firstChild); 275 this.appendChild(e); 276 return e; 277 } 278 279 Element addChild(string tagName, in Html innerHtml, string info2 = null) 280 in { 281 } 282 out(ret) { 283 assert(ret !is null); 284 assert(ret.parentNode is this); 285 assert(ret.parentDocument is this.parentDocument); 286 } 287 do { 288 auto e = Element.make(tagName, "", info2); 289 this.appendChild(e); 290 e.innerHTML = innerHtml.source; 291 return e; 292 } 293 294 295 /// . 296 void appendChildren(Element[] children) { 297 foreach(ele; children) 298 appendChild(ele); 299 } 300 301 ///. 302 void reparent(Element newParent) 303 in { 304 assert(newParent !is null); 305 assert(parentNode !is null); 306 } 307 out { 308 assert(this.parentNode is newParent); 309 //assert(isInArray(this, newParent.children)); 310 } 311 do { 312 parentNode.removeChild(this); 313 newParent.appendChild(this); 314 } 315 316 /** 317 Strips this tag out of the document, putting its inner html 318 as children of the parent. 319 320 For example, given: <p>hello <b>there</b></p>, if you 321 call stripOut() on the b element, you'll be left with 322 <p>hello there<p>. 323 324 The idea here is to make it easy to get rid of garbage 325 markup you aren't interested in. 326 */ 327 void stripOut() 328 in { 329 assert(parentNode !is null); 330 } 331 out { 332 assert(parentNode is null); 333 assert(children.length == 0); 334 } 335 do { 336 foreach(c; children) 337 c.parentNode = null; // remove the parent 338 if(children.length) 339 parentNode.replaceChild(this, this.children); 340 else 341 parentNode.removeChild(this); 342 this.children.length = 0; // we reparented them all above 343 } 344 345 /// shorthand for this.parentNode.removeChild(this) with parentNode null check 346 /// if the element already isn't in a tree, it does nothing. 347 Element removeFromTree() 348 in { 349 350 } 351 out(var) { 352 assert(this.parentNode is null); 353 assert(var is this); 354 } 355 do { 356 if(this.parentNode is null) 357 return this; 358 359 this.parentNode.removeChild(this); 360 361 return this; 362 } 363 364 /// Wraps this element inside the given element. 365 /// It's like this.replaceWith(what); what.appendchild(this); 366 /// 367 /// Given: < b >cool</ b >, if you call b.wrapIn(new Link("site.com", "my site is ")); 368 /// you'll end up with: < a href="site.com">my site is < b >cool< /b ></ a >. 369 Element wrapIn(Element what) 370 in { 371 assert(what !is null); 372 } 373 out(ret) { 374 assert(this.parentNode is what); 375 assert(ret is what); 376 } 377 do { 378 this.replaceWith(what); 379 what.appendChild(this); 380 381 return what; 382 } 383 384 /// Replaces this element with something else in the tree. 385 Element replaceWith(Element e) 386 in { 387 assert(this.parentNode !is null); 388 } 389 do { 390 e.removeFromTree(); 391 this.parentNode.replaceChild(this, e); 392 return e; 393 } 394 395 /** 396 Splits the className into an array of each class given 397 */ 398 string[] classNames() const { 399 return className().split(" "); 400 } 401 402 /** 403 Fetches the first consecutive nodes, if text nodes, concatenated together 404 405 If the first node is not text, returns null. 406 407 See also: directText, innerText 408 */ 409 string firstInnerText() const { 410 string s; 411 foreach(child; children) { 412 if(child.nodeType != NodeType.Text) 413 break; 414 415 s ~= child.nodeValue(); 416 } 417 return s; 418 } 419 420 421 /** 422 Returns the text directly under this element, 423 not recursively like innerText. 424 425 See also: firstInnerText 426 */ 427 @property string directText() { 428 string ret; 429 foreach(e; children) { 430 if(e.nodeType == NodeType.Text) 431 ret ~= e.nodeValue(); 432 } 433 434 return ret; 435 } 436 437 /** 438 Sets the direct text, keeping the same place. 439 440 Unlike innerText, this does *not* remove existing 441 elements in the element. 442 443 It only replaces the first text node it sees. 444 445 If there are no text nodes, it calls appendText 446 447 So, given (ignore the spaces in the tags): 448 < div > < img > text here < /div > 449 450 it will keep the img, and replace the "text here". 451 */ 452 @property void directText(string text) { 453 foreach(e; children) { 454 if(e.nodeType == NodeType.Text) { 455 auto it = cast(TextNode) e; 456 it.contents = text; 457 return; 458 } 459 } 460 461 appendText(text); 462 } 463 } 464 465 /// finds comments that match the given txt. Case insensitive, strips whitespace. 466 Element[] findComments(Document document, string txt) { 467 return findComments(document.root, txt); 468 } 469 470 /// ditto 471 Element[] findComments(Element element, string txt) { 472 txt = txt.strip().toLower(); 473 Element[] ret; 474 475 foreach(comment; element.getElementsByTagName("#comment")) { 476 string t = comment.nodeValue().strip().toLower(); 477 if(t == txt) 478 ret ~= comment; 479 } 480 481 return ret; 482 } 483 484 // I'm just dicking around with this 485 struct ElementCollection { 486 this(Element e) { 487 elements = [e]; 488 } 489 490 this(Element e, string selector) { 491 elements = e.querySelectorAll(selector); 492 } 493 494 this(Element[] e) { 495 elements = e; 496 } 497 498 Element[] elements; 499 //alias elements this; // let it implicitly convert to the underlying array 500 501 ElementCollection opIndex(string selector) { 502 ElementCollection ec; 503 foreach(e; elements) 504 ec.elements ~= e.getElementsBySelector(selector); 505 return ec; 506 } 507 508 /// if you slice it, give the underlying array for easy forwarding of the 509 /// collection to range expecting algorithms or looping over. 510 Element[] opSlice() { 511 return elements; 512 } 513 514 /// And input range primitives so we can foreach over this 515 void popFront() { 516 elements = elements[1..$]; 517 } 518 519 /// ditto 520 Element front() { 521 return elements[0]; 522 } 523 524 /// ditto 525 bool empty() { 526 return !elements.length; 527 } 528 529 /// Forward method calls to each individual element of the collection 530 /// returns this so it can be chained. 531 ElementCollection opDispatch(string name, T...)(T t) { 532 foreach(e; elements) { 533 mixin("e." ~ name)(t); 534 } 535 return this; 536 } 537 538 ElementCollection opBinary(string op : "~")(ElementCollection rhs) { 539 return ElementCollection(this.elements ~ rhs.elements); 540 } 541 } 542 543 544 // this puts in operators and opDispatch to handle string indexes and properties, forwarding to get and set functions. 545 mixin template JavascriptStyleDispatch() { 546 string opDispatch(string name)(string v = null) if(name != "popFront") { // popFront will make this look like a range. Do not want. 547 if(v !is null) 548 return set(name, v); 549 return get(name); 550 } 551 552 string opIndex(string key) const { 553 return get(key); 554 } 555 556 string opIndexAssign(string value, string field) { 557 return set(field, value); 558 } 559 560 // FIXME: doesn't seem to work 561 string* opBinary(string op)(string key) if(op == "in") { 562 return key in fields; 563 } 564 } 565 566 /// A proxy object to do the Element class' dataset property. See Element.dataset for more info. 567 /// 568 /// Do not create this object directly. 569 struct DataSet { 570 this(Element e) { 571 this._element = e; 572 } 573 574 private Element _element; 575 string set(string name, string value) { 576 _element.setAttribute("data-" ~ unCamelCase(name), value); 577 return value; 578 } 579 580 string get(string name) const { 581 return _element.getAttribute("data-" ~ unCamelCase(name)); 582 } 583 584 mixin JavascriptStyleDispatch!(); 585 } 586 587 /// Proxy object for attributes which will replace the main opDispatch eventually 588 struct AttributeSet { 589 this(Element e) { 590 this._element = e; 591 } 592 593 private Element _element; 594 string set(string name, string value) { 595 _element.setAttribute(name, value); 596 return value; 597 } 598 599 string get(string name) const { 600 return _element.getAttribute(name); 601 } 602 603 mixin JavascriptStyleDispatch!(); 604 } 605 606 607 608 /// for style, i want to be able to set it with a string like a plain attribute, 609 /// but also be able to do properties Javascript style. 610 611 struct ElementStyle { 612 this(Element parent) { 613 _element = parent; 614 } 615 616 Element _element; 617 618 @property ref inout(string) _attribute() inout { 619 auto s = "style" in _element.attributes; 620 if(s is null) { 621 auto e = cast() _element; // const_cast 622 e.attributes["style"] = ""; // we need something to reference 623 s = cast(inout) ("style" in e.attributes); 624 } 625 626 assert(s !is null); 627 return *s; 628 } 629 630 alias _attribute this; // this is meant to allow element.style = element.style ~ " string "; to still work. 631 632 string set(string name, string value) { 633 if(name.length == 0) 634 return value; 635 if(name == "cssFloat") 636 name = "float"; 637 else 638 name = unCamelCase(name); 639 auto r = rules(); 640 r[name] = value; 641 642 _attribute = ""; 643 foreach(k, v; r) { 644 if(v is null || v.length == 0) /* css can't do empty rules anyway so we'll use that to remove */ 645 continue; 646 if(_attribute.length) 647 _attribute ~= " "; 648 _attribute ~= k ~ ": " ~ v ~ ";"; 649 } 650 651 _element.setAttribute("style", _attribute); // this is to trigger the observer call 652 653 return value; 654 } 655 string get(string name) const { 656 if(name == "cssFloat") 657 name = "float"; 658 else 659 name = unCamelCase(name); 660 auto r = rules(); 661 if(name in r) 662 return r[name]; 663 return null; 664 } 665 666 string[string] rules() const { 667 string[string] ret; 668 foreach(rule; _attribute.split(";")) { 669 rule = rule.strip(); 670 if(rule.length == 0) 671 continue; 672 auto idx = rule.indexOf(":"); 673 if(idx == -1) 674 ret[rule] = ""; 675 else { 676 auto name = rule[0 .. idx].strip(); 677 auto value = rule[idx + 1 .. $].strip(); 678 679 ret[name] = value; 680 } 681 } 682 683 return ret; 684 } 685 686 mixin JavascriptStyleDispatch!(); 687 } 688 689 /// Converts a camel cased propertyName to a css style dashed property-name 690 string unCamelCase(string a) { 691 string ret; 692 foreach(c; a) 693 if((c >= 'A' && c <= 'Z')) 694 ret ~= "-" ~ toLower("" ~ c)[0]; 695 else 696 ret ~= c; 697 return ret; 698 } 699 700 /// Translates a css style property-name to a camel cased propertyName 701 string camelCase(string a) { 702 string ret; 703 bool justSawDash = false; 704 foreach(c; a) 705 if(c == '-') { 706 justSawDash = true; 707 } else { 708 if(justSawDash) { 709 justSawDash = false; 710 ret ~= toUpper("" ~ c); 711 } else 712 ret ~= c; 713 } 714 return ret; 715 } 716 717 718 719 720 721 722 723 724 725 // domconvenience ends } 726 727 728 729 730 731 732 733 734 735 736 737 // @safe: 738 739 // NOTE: do *NOT* override toString on Element subclasses. It won't work. 740 // Instead, override writeToAppender(); 741 742 // FIXME: should I keep processing instructions like <?blah ?> and <!-- blah --> (comments too lol)? I *want* them stripped out of most my output, but I want to be able to parse and create them too. 743 744 // Stripping them is useful for reading php as html.... but adding them 745 // is good for building php. 746 747 // I need to maintain compatibility with the way it is now too. 748 749 import std..string; 750 import std.exception; 751 import std.uri; 752 import std.array; 753 import std.range; 754 755 //import std.stdio; 756 757 // tag soup works for most the crap I know now! If you have two bad closing tags back to back, it might erase one, but meh 758 // that's rarer than the flipped closing tags that hack fixes so I'm ok with it. (Odds are it should be erased anyway; it's 759 // most likely a typo so I say kill kill kill. 760 761 762 /// This might belong in another module, but it represents a file with a mime type and some data. 763 /// Document implements this interface with type = text/html (see Document.contentType for more info) 764 /// and data = document.toString, so you can return Documents anywhere web.d expects FileResources. 765 interface FileResource { 766 @property string contentType() const; /// the content-type of the file. e.g. "text/html; charset=utf-8" or "image/png" 767 immutable(ubyte)[] getData() const; /// the data 768 } 769 770 771 772 773 ///. 774 enum NodeType { Text = 3 } 775 776 777 /// You can use this to do an easy null check or a dynamic cast+null check on any element. 778 T require(T = Element, string file = __FILE__, int line = __LINE__)(Element e) if(is(T : Element)) 779 in {} 780 out(ret) { assert(ret !is null); } 781 do { 782 auto ret = cast(T) e; 783 if(ret is null) 784 throw new ElementNotFoundException(T.stringof, "passed value", file, line); 785 return ret; 786 } 787 788 /// This represents almost everything in the DOM. 789 class Element { 790 mixin DomConvenienceFunctions!(); 791 792 // do nothing, this is primarily a virtual hook 793 // for links and forms 794 void setValue(string field, string value) { } 795 796 797 // this is a thing so i can remove observer support if it gets slow 798 // I have not implemented all these yet 799 private void sendObserverEvent(DomMutationOperations operation, string s1 = null, string s2 = null, Element r = null, Element r2 = null) { 800 if(parentDocument is null) return; 801 DomMutationEvent me; 802 me.operation = operation; 803 me.target = this; 804 me.relatedString = s1; 805 me.relatedString2 = s2; 806 me.related = r; 807 me.related2 = r2; 808 parentDocument.dispatchMutationEvent(me); 809 } 810 811 // putting all the members up front 812 813 // this ought to be private. don't use it directly. 814 Element[] children; 815 816 /// The name of the tag. Remember, changing this doesn't change the dynamic type of the object. 817 string tagName; 818 819 /// This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead. 820 string[string] attributes; 821 822 /// In XML, it is valid to write <tag /> for all elements with no children, but that breaks HTML, so I don't do it here. 823 /// Instead, this flag tells if it should be. It is based on the source document's notation and a html element list. 824 private bool selfClosed; 825 826 /// Get the parent Document object that contains this element. 827 /// It may be null, so remember to check for that. 828 Document parentDocument; 829 830 ///. 831 Element parentNode; 832 833 // the next few methods are for implementing interactive kind of things 834 private CssStyle _computedStyle; 835 836 // these are here for event handlers. Don't forget that this library never fires events. 837 // (I'm thinking about putting this in a version statement so you don't have the baggage. The instance size of this class is 56 bytes right now.) 838 EventHandler[][string] bubblingEventHandlers; 839 EventHandler[][string] capturingEventHandlers; 840 EventHandler[string] defaultEventHandlers; 841 842 void addEventListener(string event, EventHandler handler, bool useCapture = false) { 843 if(event.length > 2 && event[0..2] == "on") 844 event = event[2 .. $]; 845 846 if(useCapture) 847 capturingEventHandlers[event] ~= handler; 848 else 849 bubblingEventHandlers[event] ~= handler; 850 } 851 852 853 // and now methods 854 855 /// Convenience function to try to do the right thing for HTML. This is the main 856 /// way I create elements. 857 static Element make(string tagName, string childInfo = null, string childInfo2 = null) { 858 bool selfClosed = tagName.isInArray(selfClosedElements); 859 860 Element e; 861 // want to create the right kind of object for the given tag... 862 switch(tagName) { 863 case "#text": 864 e = new TextNode(null, childInfo); 865 return e; 866 // break; 867 case "table": 868 e = new Table(null); 869 break; 870 case "a": 871 e = new Link(null); 872 break; 873 case "form": 874 e = new Form(null); 875 break; 876 case "tr": 877 e = new TableRow(null); 878 break; 879 case "td", "th": 880 e = new TableCell(null, tagName); 881 break; 882 default: 883 e = new Element(null, tagName, null, selfClosed); // parent document should be set elsewhere 884 } 885 886 // make sure all the stuff is constructed properly FIXME: should probably be in all the right constructors too 887 e.tagName = tagName; 888 e.selfClosed = selfClosed; 889 890 if(childInfo !is null) 891 switch(tagName) { 892 /* html5 convenience tags */ 893 case "audio": 894 if(childInfo.length) 895 e.addChild("source", childInfo); 896 if(childInfo2 !is null) 897 e.appendText(childInfo2); 898 break; 899 case "source": 900 e.src = childInfo; 901 if(childInfo2 !is null) 902 e.type = childInfo2; 903 break; 904 /* regular html 4 stuff */ 905 case "img": 906 e.src = childInfo; 907 if(childInfo2 !is null) 908 e.alt = childInfo2; 909 break; 910 case "link": 911 e.href = childInfo; 912 if(childInfo2 !is null) 913 e.rel = childInfo2; 914 break; 915 case "option": 916 e.innerText = childInfo; 917 if(childInfo2 !is null) 918 e.value = childInfo2; 919 break; 920 case "input": 921 e.type = "hidden"; 922 e.name = childInfo; 923 if(childInfo2 !is null) 924 e.value = childInfo2; 925 break; 926 case "button": 927 e.innerText = childInfo; 928 if(childInfo2 !is null) 929 e.type = childInfo2; 930 break; 931 case "a": 932 e.innerText = childInfo; 933 if(childInfo2 !is null) 934 e.href = childInfo2; 935 break; 936 case "script": 937 case "style": 938 e.innerRawSource = childInfo; 939 break; 940 case "meta": 941 e.name = childInfo; 942 if(childInfo2 !is null) 943 e.content = childInfo2; 944 break; 945 /* generically, assume we were passed text and perhaps class */ 946 default: 947 e.innerText = childInfo; 948 if(childInfo2.length) 949 e.className = childInfo2; 950 } 951 952 return e; 953 } 954 955 static Element make(string tagName, in Html innerHtml, string childInfo2 = null) { 956 // FIXME: childInfo2 is ignored when info1 is null 957 auto m = Element.make(tagName, cast(string) null, childInfo2); 958 m.innerHTML = innerHtml.source; 959 return m; 960 } 961 962 static Element make(string tagName, Element child, string childInfo2 = null) { 963 auto m = Element.make(tagName, cast(string) null, childInfo2); 964 m.appendChild(child); 965 return m; 966 } 967 968 969 /// Generally, you don't want to call this yourself - use Element.make or document.createElement instead. 970 this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) { 971 parentDocument = _parentDocument; 972 tagName = _tagName; 973 if(_attributes !is null) 974 attributes = _attributes; 975 selfClosed = _selfClosed; 976 977 version(dom_node_indexes) 978 this.dataset.nodeIndex = to!string(&(this.attributes)); 979 980 assert(_tagName.indexOf(" ") == -1);//, "<" ~ _tagName ~ "> is invalid"); 981 } 982 983 /// Convenience constructor when you don't care about the parentDocument. Note this might break things on the document. 984 /// Note also that without a parent document, elements are always in strict, case-sensitive mode. 985 this(string _tagName, string[string] _attributes = null) { 986 tagName = _tagName; 987 if(_attributes !is null) 988 attributes = _attributes; 989 selfClosed = tagName.isInArray(selfClosedElements); 990 991 // this is meant to reserve some memory. It makes a small, but consistent improvement. 992 //children.length = 8; 993 //children.length = 0; 994 995 version(dom_node_indexes) 996 this.dataset.nodeIndex = to!string(&(this.attributes)); 997 } 998 999 private this(Document _parentDocument) { 1000 parentDocument = _parentDocument; 1001 1002 version(dom_node_indexes) 1003 this.dataset.nodeIndex = to!string(&(this.attributes)); 1004 } 1005 1006 1007 /* ******************************* 1008 Navigating the DOM 1009 *********************************/ 1010 1011 /// Returns the first child of this element. If it has no children, returns null. 1012 /// Remember, text nodes are children too. 1013 @property Element firstChild() { 1014 return children.length ? children[0] : null; 1015 } 1016 1017 /// 1018 @property Element lastChild() { 1019 return children.length ? children[$ - 1] : null; 1020 } 1021 1022 1023 ///. 1024 @property Element previousSibling(string tagName = null) { 1025 if(this.parentNode is null) 1026 return null; 1027 Element ps = null; 1028 foreach(e; this.parentNode.childNodes) { 1029 if(e is this) 1030 break; 1031 if(tagName == "*" && e.nodeType != NodeType.Text) { 1032 ps = e; 1033 break; 1034 } 1035 if(tagName is null || e.tagName == tagName) 1036 ps = e; 1037 } 1038 1039 return ps; 1040 } 1041 1042 ///. 1043 @property Element nextSibling(string tagName = null) { 1044 if(this.parentNode is null) 1045 return null; 1046 Element ns = null; 1047 bool mightBe = false; 1048 foreach(e; this.parentNode.childNodes) { 1049 if(e is this) { 1050 mightBe = true; 1051 continue; 1052 } 1053 if(mightBe) { 1054 if(tagName == "*" && e.nodeType != NodeType.Text) { 1055 ns = e; 1056 break; 1057 } 1058 if(tagName is null || e.tagName == tagName) { 1059 ns = e; 1060 break; 1061 } 1062 } 1063 } 1064 1065 return ns; 1066 } 1067 1068 1069 /// Gets the nearest node, going up the chain, with the given tagName 1070 /// May return null or throw. 1071 T getParent(T = Element)(string tagName = null) if(is(T : Element)) { 1072 if(tagName is null) { 1073 static if(is(T == Form)) 1074 tagName = "form"; 1075 else static if(is(T == Table)) 1076 tagName = "table"; 1077 else static if(is(T == Link)) 1078 tagName == "a"; 1079 } 1080 1081 auto par = this.parentNode; 1082 while(par !is null) { 1083 if(tagName is null || par.tagName == tagName) 1084 break; 1085 par = par.parentNode; 1086 } 1087 1088 static if(!is(T == Element)) { 1089 auto t = cast(T) par; 1090 if(t is null) 1091 throw new ElementNotFoundException("", tagName ~ " parent not found"); 1092 } else 1093 auto t = par; 1094 1095 return t; 1096 } 1097 1098 ///. 1099 Element getElementById(string id) { 1100 // FIXME: I use this function a lot, and it's kinda slow 1101 // not terribly slow, but not great. 1102 foreach(e; tree) 1103 if(e.id == id) 1104 return e; 1105 return null; 1106 } 1107 1108 /// Note: you can give multiple selectors, separated by commas. 1109 /// It will return the first match it finds. 1110 Element querySelector(string selector) { 1111 // FIXME: inefficient; it gets all results just to discard most of them 1112 auto list = getElementsBySelector(selector); 1113 if(list.length == 0) 1114 return null; 1115 return list[0]; 1116 } 1117 1118 /// a more standards-compliant alias for getElementsBySelector 1119 Element[] querySelectorAll(string selector) { 1120 return getElementsBySelector(selector); 1121 } 1122 1123 /** 1124 Does a CSS selector 1125 1126 * -- all, default if nothing else is there 1127 1128 tag#id.class.class.class:pseudo[attrib=what][attrib=what] OP selector 1129 1130 It is all additive 1131 1132 OP 1133 1134 space = descendant 1135 > = direct descendant 1136 + = sibling (E+F Matches any F element immediately preceded by a sibling element E) 1137 1138 [foo] Foo is present as an attribute 1139 [foo="warning"] Matches any E element whose "foo" attribute value is exactly equal to "warning". 1140 E[foo~="warning"] Matches any E element whose "foo" attribute value is a list of space-separated values, one of which is exactly equal to "warning" 1141 E[lang|="en"] Matches any E element whose "lang" attribute has a hyphen-separated list of values beginning (from the left) with "en". 1142 1143 [item$=sdas] ends with 1144 [item^-sdsad] begins with 1145 1146 Quotes are optional here. 1147 1148 Pseudos: 1149 :first-child 1150 :last-child 1151 :link (same as a[href] for our purposes here) 1152 1153 1154 There can be commas separating the selector. A comma separated list result is OR'd onto the main. 1155 1156 1157 1158 This ONLY cares about elements. text, etc, are ignored 1159 1160 1161 There should be two functions: given element, does it match the selector? and given a selector, give me all the elements 1162 */ 1163 Element[] getElementsBySelector(string selector) { 1164 // FIXME: this function could probably use some performance attention 1165 // ... but only mildly so according to the profiler in the big scheme of things; probably negligible in a big app. 1166 1167 1168 bool caseSensitiveTags = true; 1169 if(parentDocument && parentDocument.loose) 1170 caseSensitiveTags = false; 1171 1172 Element[] ret; 1173 foreach(sel; parseSelectorString(selector, caseSensitiveTags)) 1174 ret ~= sel.getElements(this); 1175 return ret; 1176 } 1177 1178 /// . 1179 Element[] getElementsByClassName(string cn) { 1180 // is this correct? 1181 return getElementsBySelector("." ~ cn); 1182 } 1183 1184 ///. 1185 Element[] getElementsByTagName(string tag) { 1186 if(parentDocument && parentDocument.loose) 1187 tag = tag.toLower(); 1188 Element[] ret; 1189 foreach(e; tree) 1190 if(e.tagName == tag) 1191 ret ~= e; 1192 return ret; 1193 } 1194 1195 1196 /* ******************************* 1197 Attributes 1198 *********************************/ 1199 1200 /** 1201 Gets the given attribute value, or null if the 1202 attribute is not set. 1203 1204 Note that the returned string is decoded, so it no longer contains any xml entities. 1205 */ 1206 string getAttribute(string name) const { 1207 if(parentDocument && parentDocument.loose) 1208 name = name.toLower(); 1209 auto e = name in attributes; 1210 if(e) 1211 return *e; 1212 else 1213 return null; 1214 } 1215 1216 /** 1217 Sets an attribute. Returns this for easy chaining 1218 */ 1219 Element setAttribute(string name, string value) { 1220 if(parentDocument && parentDocument.loose) 1221 name = name.toLower(); 1222 1223 // I never use this shit legitimately and neither should you 1224 auto it = name.toLower(); 1225 if(it == "href" || it == "src") { 1226 auto v = value.strip().toLower(); 1227 if(v.startsWith("vbscript:")) 1228 value = value[9..$]; 1229 if(v.startsWith("javascript:")) 1230 value = value[11..$]; 1231 } 1232 1233 attributes[name] = value; 1234 1235 sendObserverEvent(DomMutationOperations.setAttribute, name, value); 1236 1237 return this; 1238 } 1239 1240 /** 1241 Returns if the attribute exists. 1242 */ 1243 bool hasAttribute(string name) { 1244 if(parentDocument && parentDocument.loose) 1245 name = name.toLower(); 1246 1247 if(name in attributes) 1248 return true; 1249 else 1250 return false; 1251 } 1252 1253 /** 1254 Removes the given attribute from the element. 1255 */ 1256 Element removeAttribute(string name) 1257 out(ret) { 1258 assert(ret is this); 1259 } 1260 do { 1261 if(parentDocument && parentDocument.loose) 1262 name = name.toLower(); 1263 if(name in attributes) 1264 attributes.remove(name); 1265 1266 sendObserverEvent(DomMutationOperations.removeAttribute, name); 1267 return this; 1268 } 1269 1270 /** 1271 Gets the class attribute's contents. Returns 1272 an empty string if it has no class. 1273 */ 1274 @property string className() const { 1275 auto c = getAttribute("class"); 1276 if(c is null) 1277 return ""; 1278 return c; 1279 } 1280 1281 ///. 1282 @property Element className(string c) { 1283 setAttribute("class", c); 1284 return this; 1285 } 1286 1287 /** 1288 Provides easy access to attributes, object style. 1289 1290 auto element = Element.make("a"); 1291 a.href = "cool.html"; // this is the same as a.setAttribute("href", "cool.html"); 1292 string where = a.href; // same as a.getAttribute("href"); 1293 1294 */ 1295 @property string opDispatch(string name)(string v = null) if(isConvenientAttribute(name)) { 1296 if(v !is null) 1297 setAttribute(name, v); 1298 return getAttribute(name); 1299 } 1300 1301 /** 1302 DEPRECATED: generally open opDispatch caused a lot of unforeseen trouble with compile time duck typing and UFCS extensions. 1303 so I want to remove it. A small whitelist of attributes is still allowed, but others are not. 1304 1305 Instead, use element.attrs.attribute, element.attrs["attribute"], 1306 or element.getAttribute("attribute")/element.setAttribute("attribute"). 1307 */ 1308 @property string opDispatch(string name)(string v = null) if(!isConvenientAttribute(name)) { 1309 static assert(0, "Don't use " ~ name ~ " direct on Element, instead use element.attrs.attributeName"); 1310 } 1311 1312 /* 1313 // this would be nice for convenience, but it broke the getter above. 1314 @property void opDispatch(string name)(bool boolean) if(name != "popFront") { 1315 if(boolean) 1316 setAttribute(name, name); 1317 else 1318 removeAttribute(name); 1319 } 1320 */ 1321 1322 /** 1323 Returns the element's children. 1324 */ 1325 @property const(Element[]) childNodes() const { 1326 return children; 1327 } 1328 1329 /// Mutable version of the same 1330 @property Element[] childNodes() { // FIXME: the above should be inout 1331 return children; 1332 } 1333 1334 /// HTML5's dataset property. It is an alternate view into attributes with the data- prefix. 1335 /// 1336 /// Given: <a data-my-property="cool" /> 1337 /// 1338 /// We get: assert(a.dataset.myProperty == "cool"); 1339 @property DataSet dataset() { 1340 return DataSet(this); 1341 } 1342 1343 /// Gives dot/opIndex access to attributes 1344 /// ele.attrs.largeSrc = "foo"; // same as ele.setAttribute("largeSrc", "foo") 1345 @property AttributeSet attrs() { 1346 return AttributeSet(this); 1347 } 1348 1349 /// Provides both string and object style (like in Javascript) access to the style attribute. 1350 @property ElementStyle style() { 1351 return ElementStyle(this); 1352 } 1353 1354 /// This sets the style attribute with a string. 1355 @property ElementStyle style(string s) { 1356 this.setAttribute("style", s); 1357 return this.style; 1358 } 1359 1360 private void parseAttributes(string[] whichOnes = null) { 1361 /+ 1362 if(whichOnes is null) 1363 whichOnes = attributes.keys; 1364 foreach(attr; whichOnes) { 1365 switch(attr) { 1366 case "id": 1367 1368 break; 1369 case "class": 1370 1371 break; 1372 case "style": 1373 1374 break; 1375 default: 1376 // we don't care about it 1377 } 1378 } 1379 +/ 1380 } 1381 1382 1383 // if you change something here, it won't apply... FIXME const? but changing it would be nice if it applies to the style attribute too though you should use style there. 1384 ///. 1385 @property CssStyle computedStyle() { 1386 if(_computedStyle is null) { 1387 auto style = this.getAttribute("style"); 1388 /* we'll treat shitty old html attributes as css here */ 1389 if(this.hasAttribute("width")) 1390 style ~= "; width: " ~ this.attrs.width; 1391 if(this.hasAttribute("height")) 1392 style ~= "; height: " ~ this.attrs.height; 1393 if(this.hasAttribute("bgcolor")) 1394 style ~= "; background-color: " ~ this.attrs.bgcolor; 1395 if(this.tagName == "body" && this.hasAttribute("text")) 1396 style ~= "; color: " ~ this.attrs.text; 1397 if(this.hasAttribute("color")) 1398 style ~= "; color: " ~ this.attrs.color; 1399 /* done */ 1400 1401 1402 _computedStyle = new CssStyle(null, style); // gives at least something to work with 1403 } 1404 return _computedStyle; 1405 } 1406 1407 /// These properties are useless in most cases, but if you write a layout engine on top of this lib, they may be good 1408 version(browser) { 1409 void* expansionHook; ///ditto 1410 int offsetWidth; ///ditto 1411 int offsetHeight; ///ditto 1412 int offsetLeft; ///ditto 1413 int offsetTop; ///ditto 1414 Element offsetParent; ///ditto 1415 bool hasLayout; ///ditto 1416 int zIndex; ///ditto 1417 1418 ///ditto 1419 int absoluteLeft() { 1420 int a = offsetLeft; 1421 auto p = offsetParent; 1422 while(p) { 1423 a += p.offsetLeft; 1424 p = p.offsetParent; 1425 } 1426 1427 return a; 1428 } 1429 1430 ///ditto 1431 int absoluteTop() { 1432 int a = offsetTop; 1433 auto p = offsetParent; 1434 while(p) { 1435 a += p.offsetTop; 1436 p = p.offsetParent; 1437 } 1438 1439 return a; 1440 } 1441 } 1442 1443 // Back to the regular dom functions 1444 1445 public: 1446 1447 1448 /* ******************************* 1449 DOM Mutation 1450 *********************************/ 1451 1452 /// Removes all inner content from the tag; all child text and elements are gone. 1453 void removeAllChildren() 1454 out { 1455 assert(this.children.length == 0); 1456 } 1457 do { 1458 children = null; 1459 } 1460 1461 1462 /// Appends the given element to this one. The given element must not have a parent already. 1463 Element appendChild(Element e) 1464 in { 1465 assert(e !is null); 1466 assert(e.parentNode is null); 1467 } 1468 out (ret) { 1469 assert(e.parentNode is this); 1470 assert(e.parentDocument is this.parentDocument); 1471 assert(e is ret); 1472 } 1473 do { 1474 selfClosed = false; 1475 e.parentNode = this; 1476 e.parentDocument = this.parentDocument; 1477 children ~= e; 1478 1479 sendObserverEvent(DomMutationOperations.appendChild, null, null, e); 1480 1481 return e; 1482 } 1483 1484 /// Inserts the second element to this node, right before the first param 1485 Element insertBefore(in Element where, Element what) 1486 in { 1487 assert(where !is null); 1488 assert(where.parentNode is this); 1489 assert(what !is null); 1490 assert(what.parentNode is null); 1491 } 1492 out (ret) { 1493 assert(where.parentNode is this); 1494 assert(what.parentNode is this); 1495 1496 assert(what.parentDocument is this.parentDocument); 1497 assert(ret is what); 1498 } 1499 do { 1500 foreach(i, e; children) { 1501 if(e is where) { 1502 children = children[0..i] ~ what ~ children[i..$]; 1503 what.parentDocument = this.parentDocument; 1504 what.parentNode = this; 1505 return what; 1506 } 1507 } 1508 1509 return what; 1510 1511 assert(0); 1512 } 1513 1514 ///. 1515 Element insertAfter(in Element where, Element what) 1516 in { 1517 assert(where !is null); 1518 assert(where.parentNode is this); 1519 assert(what !is null); 1520 assert(what.parentNode is null); 1521 } 1522 out (ret) { 1523 assert(where.parentNode is this); 1524 assert(what.parentNode is this); 1525 assert(what.parentDocument is this.parentDocument); 1526 assert(ret is what); 1527 } 1528 do { 1529 foreach(i, e; children) { 1530 if(e is where) { 1531 children = children[0 .. i + 1] ~ what ~ children[i + 1 .. $]; 1532 what.parentNode = this; 1533 what.parentDocument = this.parentDocument; 1534 return what; 1535 } 1536 } 1537 1538 return what; 1539 1540 assert(0); 1541 } 1542 1543 /// swaps one child for a new thing. Returns the old child which is now parentless. 1544 Element swapNode(Element child, Element replacement) 1545 in { 1546 assert(child !is null); 1547 assert(replacement !is null); 1548 assert(child.parentNode is this); 1549 } 1550 out(ret) { 1551 assert(ret is child); 1552 assert(ret.parentNode is null); 1553 assert(replacement.parentNode is this); 1554 assert(replacement.parentDocument is this.parentDocument); 1555 } 1556 do { 1557 foreach(ref c; this.children) 1558 if(c is child) { 1559 c.parentNode = null; 1560 c = replacement; 1561 c.parentNode = this; 1562 c.parentDocument = this.parentDocument; 1563 return child; 1564 } 1565 assert(0); 1566 } 1567 1568 1569 ///. 1570 Element appendText(string text) { 1571 Element e = new TextNode(parentDocument, text); 1572 appendChild(e); 1573 return this; 1574 } 1575 1576 ///. 1577 @property Element[] childElements() { 1578 Element[] ret; 1579 foreach(c; children) 1580 if(c.nodeType == 1) 1581 ret ~= c; 1582 return ret; 1583 } 1584 1585 /// Appends the given html to the element, returning the elements appended 1586 Element[] appendHtml(string html) { 1587 Document d = new Document("<root>" ~ html ~ "</root>"); 1588 return stealChildren(d.root); 1589 } 1590 1591 1592 ///. 1593 void insertChildAfter(Element child, Element where) 1594 in { 1595 assert(child !is null); 1596 assert(where !is null); 1597 assert(where.parentNode is this); 1598 assert(!selfClosed); 1599 //assert(isInArray(where, children)); 1600 } 1601 out { 1602 assert(child.parentNode is this); 1603 assert(where.parentNode is this); 1604 //assert(isInArray(where, children)); 1605 //assert(isInArray(child, children)); 1606 } 1607 do { 1608 foreach(ref i, c; children) { 1609 if(c is where) { 1610 i++; 1611 children = children[0..i] ~ child ~ children[i..$]; 1612 child.parentNode = this; 1613 child.parentDocument = this.parentDocument; 1614 break; 1615 } 1616 } 1617 } 1618 1619 ///. 1620 Element[] stealChildren(Element e, Element position = null) 1621 in { 1622 assert(!selfClosed); 1623 assert(e !is null); 1624 //if(position !is null) 1625 //assert(isInArray(position, children)); 1626 } 1627 out (ret) { 1628 assert(e.children.length == 0); 1629 debug foreach(child; ret) { 1630 assert(child.parentNode is this); 1631 assert(child.parentDocument is this.parentDocument); 1632 } 1633 } 1634 do { 1635 foreach(c; e.children) { 1636 c.parentNode = this; 1637 c.parentDocument = this.parentDocument; 1638 } 1639 if(position is null) 1640 children ~= e.children; 1641 else { 1642 foreach(i, child; children) { 1643 if(child is position) { 1644 children = children[0..i] ~ 1645 e.children ~ 1646 children[i..$]; 1647 break; 1648 } 1649 } 1650 } 1651 1652 auto ret = e.children.dup; 1653 e.children.length = 0; 1654 1655 return ret; 1656 } 1657 1658 /// Puts the current element first in our children list. The given element must not have a parent already. 1659 Element prependChild(Element e) 1660 in { 1661 assert(e.parentNode is null); 1662 assert(!selfClosed); 1663 } 1664 out { 1665 assert(e.parentNode is this); 1666 assert(e.parentDocument is this.parentDocument); 1667 assert(children[0] is e); 1668 } 1669 do { 1670 e.parentNode = this; 1671 e.parentDocument = this.parentDocument; 1672 children = e ~ children; 1673 return e; 1674 } 1675 1676 1677 /** 1678 Returns a string containing all child elements, formatted such that it could be pasted into 1679 an XML file. 1680 */ 1681 @property string innerHTML(Appender!string where = appender!string()) const { 1682 if(children is null) 1683 return ""; 1684 1685 auto start = where.data.length; 1686 1687 foreach(child; children) { 1688 assert(child !is null); 1689 1690 child.writeToAppender(where); 1691 } 1692 1693 return where.data[start .. $]; 1694 } 1695 1696 /** 1697 Takes some html and replaces the element's children with the tree made from the string. 1698 */ 1699 @property Element innerHTML(string html, bool strict = false) { 1700 if(html.length) 1701 selfClosed = false; 1702 1703 if(html.length == 0) { 1704 // I often say innerHTML = ""; as a shortcut to clear it out, 1705 // so let's optimize that slightly. 1706 removeAllChildren(); 1707 return this; 1708 } 1709 1710 auto doc = new Document(); 1711 doc.parseUtf8("<innerhtml>" ~ html ~ "</innerhtml>", strict, strict); // FIXME: this should preserve the strictness of the parent document 1712 1713 children = doc.root.children; 1714 foreach(c; children) { 1715 c.parentNode = this; 1716 c.parentDocument = this.parentDocument; 1717 } 1718 1719 reparentTreeDocuments(); 1720 1721 doc.root.children = null; 1722 1723 return this; 1724 } 1725 1726 /// ditto 1727 @property Element innerHTML(Html html) { 1728 return this.innerHTML = html.source; 1729 } 1730 1731 private void reparentTreeDocuments() { 1732 foreach(c; this.tree) 1733 c.parentDocument = this.parentDocument; 1734 } 1735 1736 /** 1737 Replaces this node with the given html string, which is parsed 1738 1739 Note: this invalidates the this reference, since it is removed 1740 from the tree. 1741 1742 Returns the new children that replace this. 1743 */ 1744 @property Element[] outerHTML(string html) { 1745 auto doc = new Document(); 1746 doc.parseUtf8("<innerhtml>" ~ html ~ "</innerhtml>"); // FIXME: needs to preserve the strictness 1747 1748 children = doc.root.children; 1749 foreach(c; children) { 1750 c.parentNode = this; 1751 c.parentDocument = this.parentDocument; 1752 } 1753 1754 1755 reparentTreeDocuments(); 1756 1757 1758 stripOut(); 1759 1760 return doc.root.children; 1761 } 1762 1763 /// Returns all the html for this element, including the tag itself. 1764 /// This is equivalent to calling toString(). 1765 @property string outerHTML() { 1766 return this.toString(); 1767 } 1768 1769 /// This sets the inner content of the element *without* trying to parse it. 1770 /// You can inject any code in there; this serves as an escape hatch from the dom. 1771 /// 1772 /// The only times you might actually need it are for < style > and < script > tags in html. 1773 /// Other than that, innerHTML and/or innerText should do the job. 1774 @property void innerRawSource(string rawSource) { 1775 children.length = 0; 1776 auto rs = new RawSource(parentDocument, rawSource); 1777 rs.parentNode = this; 1778 1779 children ~= rs; 1780 } 1781 1782 ///. 1783 Element replaceChild(Element find, Element replace) 1784 in { 1785 assert(find !is null); 1786 assert(replace !is null); 1787 assert(replace.parentNode is null); 1788 } 1789 out(ret) { 1790 assert(ret is replace); 1791 assert(replace.parentNode is this); 1792 assert(replace.parentDocument is this.parentDocument); 1793 assert(find.parentNode is null); 1794 } 1795 do { 1796 for(int i = 0; i < children.length; i++) { 1797 if(children[i] is find) { 1798 replace.parentNode = this; 1799 children[i].parentNode = null; 1800 children[i] = replace; 1801 replace.parentDocument = this.parentDocument; 1802 return replace; 1803 } 1804 } 1805 1806 throw new Exception("no such child"); 1807 } 1808 1809 /** 1810 Replaces the given element with a whole group. 1811 */ 1812 void replaceChild(Element find, Element[] replace) 1813 in { 1814 assert(find !is null); 1815 assert(replace !is null); 1816 assert(find.parentNode is this); 1817 debug foreach(r; replace) 1818 assert(r.parentNode is null); 1819 } 1820 out { 1821 assert(find.parentNode is null); 1822 assert(children.length >= replace.length); 1823 debug foreach(child; children) 1824 assert(child !is find); 1825 debug foreach(r; replace) 1826 assert(r.parentNode is this); 1827 } 1828 do { 1829 if(replace.length == 0) { 1830 removeChild(find); 1831 return; 1832 } 1833 assert(replace.length); 1834 for(int i = 0; i < children.length; i++) { 1835 if(children[i] is find) { 1836 children[i].parentNode = null; // this element should now be dead 1837 children[i] = replace[0]; 1838 foreach(e; replace) { 1839 e.parentNode = this; 1840 e.parentDocument = this.parentDocument; 1841 } 1842 1843 children = .insertAfter(children, i, replace[1..$]); 1844 1845 return; 1846 } 1847 } 1848 1849 throw new Exception("no such child"); 1850 } 1851 1852 1853 /** 1854 Removes the given child from this list. 1855 1856 Returns the removed element. 1857 */ 1858 Element removeChild(Element c) 1859 in { 1860 assert(c !is null); 1861 assert(c.parentNode is this); 1862 } 1863 out { 1864 debug foreach(child; children) 1865 assert(child !is c); 1866 assert(c.parentNode is null); 1867 } 1868 do { 1869 foreach(i, e; children) { 1870 if(e is c) { 1871 children = children[0..i] ~ children [i+1..$]; 1872 c.parentNode = null; 1873 return c; 1874 } 1875 } 1876 1877 throw new Exception("no such child"); 1878 } 1879 1880 /// This removes all the children from this element, returning the old list. 1881 Element[] removeChildren() 1882 out (ret) { 1883 assert(children.length == 0); 1884 debug foreach(r; ret) 1885 assert(r.parentNode is null); 1886 } 1887 do { 1888 Element[] oldChildren = children.dup; 1889 foreach(c; oldChildren) 1890 c.parentNode = null; 1891 1892 children.length = 0; 1893 1894 return oldChildren; 1895 } 1896 1897 /** 1898 Fetch the inside text, with all tags stripped out. 1899 1900 <p>cool <b>api</b> & code dude<p> 1901 innerText of that is "cool api & code dude". 1902 */ 1903 @property string innerText() const { 1904 string s; 1905 foreach(child; children) { 1906 if(child.nodeType != NodeType.Text) 1907 s ~= child.innerText; 1908 else 1909 s ~= child.nodeValue(); 1910 } 1911 return s; 1912 } 1913 1914 /** 1915 Sets the inside text, replacing all children. You don't 1916 have to worry about entity encoding. 1917 */ 1918 @property void innerText(string text) { 1919 selfClosed = false; 1920 Element e = new TextNode(parentDocument, text); 1921 e.parentNode = this; 1922 children = [e]; 1923 } 1924 1925 /** 1926 Strips this node out of the document, replacing it with the given text 1927 */ 1928 @property void outerText(string text) { 1929 parentNode.replaceChild(this, new TextNode(parentDocument, text)); 1930 } 1931 1932 /** 1933 Same result as innerText; the tag with all inner tags stripped out 1934 */ 1935 string outerText() const { 1936 return innerText; 1937 } 1938 1939 1940 /* ******************************* 1941 Miscellaneous 1942 *********************************/ 1943 1944 /// This is a full clone of the element 1945 @property Element cloned() 1946 /+ 1947 out(ret) { 1948 // FIXME: not sure why these fail... 1949 assert(ret.children.length == this.children.length, format("%d %d", ret.children.length, this.children.length)); 1950 assert(ret.tagName == this.tagName); 1951 } 1952 do { 1953 +/ 1954 { 1955 auto e = Element.make(this.tagName); 1956 e.parentDocument = this.parentDocument; 1957 e.attributes = this.attributes.aadup; 1958 e.selfClosed = this.selfClosed; 1959 foreach(child; children) { 1960 e.appendChild(child.cloned); 1961 } 1962 1963 return e; 1964 } 1965 1966 /// Clones the node. If deepClone is true, clone all inner tags too. If false, only do this tag (and its attributes), but it will have no contents. 1967 Element cloneNode(bool deepClone) { 1968 if(deepClone) 1969 return this.cloned; 1970 1971 // shallow clone 1972 auto e = Element.make(this.tagName); 1973 e.parentDocument = this.parentDocument; 1974 e.attributes = this.attributes.aadup; 1975 e.selfClosed = this.selfClosed; 1976 return e; 1977 } 1978 1979 ///. 1980 string nodeValue() const { 1981 return ""; 1982 } 1983 1984 // should return int 1985 ///. 1986 @property int nodeType() const { 1987 return 1; 1988 } 1989 1990 1991 invariant () { 1992 assert(tagName.indexOf(" ") == -1); 1993 1994 if(children !is null) 1995 debug foreach(child; children) { 1996 // assert(parentNode !is null); 1997 assert(child !is null); 1998 assert(child.parentNode is this, format("%s is not a parent of %s (it thought it was %s)", tagName, child.tagName, child.parentNode is null ? "null" : child.parentNode.tagName)); 1999 assert(child !is this); 2000 assert(child !is parentNode); 2001 } 2002 2003 /+ // only depend on parentNode's accuracy if you shuffle things around and use the top elements - where the contracts guarantee it on out 2004 if(parentNode !is null) { 2005 // if you have a parent, you should share the same parentDocument; this is appendChild()'s job 2006 auto lol = cast(TextNode) this; 2007 assert(parentDocument is parentNode.parentDocument, lol is null ? this.tagName : lol.contents); 2008 } 2009 +/ 2010 //assert(parentDocument !is null); // no more; if it is present, we use it, but it is not required 2011 // reason is so you can create these without needing a reference to the document 2012 } 2013 2014 /** 2015 Turns the whole element, including tag, attributes, and children, into a string which could be pasted into 2016 an XML file. 2017 */ 2018 override string toString() const { 2019 return writeToAppender(); 2020 } 2021 2022 /// This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time. 2023 /// Returns the string it creates. 2024 string writeToAppender(Appender!string where = appender!string()) const { 2025 assert(tagName !is null); 2026 2027 where.reserve((this.children.length + 1) * 512); 2028 2029 auto start = where.data.length; 2030 2031 where.put("<"); 2032 where.put(tagName); 2033 2034 foreach(n, v ; attributes) { 2035 assert(n !is null); 2036 //assert(v !is null); 2037 where.put(" "); 2038 where.put(n); 2039 where.put("=\""); 2040 htmlEntitiesEncode(v, where); 2041 where.put("\""); 2042 } 2043 2044 if(selfClosed){ 2045 where.put(" />"); 2046 return where.data[start .. $]; 2047 } 2048 2049 where.put('>'); 2050 2051 innerHTML(where); 2052 2053 where.put("</"); 2054 where.put(tagName); 2055 where.put('>'); 2056 2057 return where.data[start .. $]; 2058 } 2059 2060 /** 2061 Returns a lazy range of all its children, recursively. 2062 */ 2063 @property ElementStream tree() { 2064 return new ElementStream(this); 2065 } 2066 2067 // I moved these from Form because they are generally useful. 2068 // Ideally, I'd put them in arsd.html and use UFCS, but that doesn't work with the opDispatch here. 2069 /// Tags: HTML, HTML5 2070 // FIXME: add overloads for other label types... 2071 Element addField(string label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) { 2072 auto fs = this; 2073 auto i = fs.addChild("label"); 2074 2075 if(!(type == "checkbox" || type == "radio")) 2076 i.addChild("span", label); 2077 2078 Element input; 2079 if(type == "textarea") 2080 input = i.addChild("textarea"). 2081 setAttribute("name", name). 2082 setAttribute("rows", "6"); 2083 else 2084 input = i.addChild("input"). 2085 setAttribute("name", name). 2086 setAttribute("type", type); 2087 2088 if(type == "checkbox" || type == "radio") 2089 i.addChild("span", label); 2090 2091 // these are html 5 attributes; you'll have to implement fallbacks elsewhere. In Javascript or maybe I'll add a magic thing to html.d later. 2092 fieldOptions.applyToElement(input); 2093 return i; 2094 } 2095 2096 Element addField(Element label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) { 2097 auto fs = this; 2098 auto i = fs.addChild("label"); 2099 i.addChild(label); 2100 Element input; 2101 if(type == "textarea") 2102 input = i.addChild("textarea"). 2103 setAttribute("name", name). 2104 setAttribute("rows", "6"); 2105 else 2106 input = i.addChild("input"). 2107 setAttribute("name", name). 2108 setAttribute("type", type); 2109 2110 // these are html 5 attributes; you'll have to implement fallbacks elsewhere. In Javascript or maybe I'll add a magic thing to html.d later. 2111 fieldOptions.applyToElement(input); 2112 return i; 2113 } 2114 2115 Element addField(string label, string name, FormFieldOptions fieldOptions) { 2116 return addField(label, name, "text", fieldOptions); 2117 } 2118 2119 Element addField(string label, string name, string[string] options, FormFieldOptions fieldOptions = FormFieldOptions.none) { 2120 auto fs = this; 2121 auto i = fs.addChild("label"); 2122 i.addChild("span", label); 2123 auto sel = i.addChild("select").setAttribute("name", name); 2124 2125 foreach(k, opt; options) 2126 sel.addChild("option", opt, k); 2127 2128 // FIXME: implement requirements somehow 2129 2130 return i; 2131 } 2132 2133 Element addSubmitButton(string label = null) { 2134 auto t = this; 2135 auto holder = t.addChild("div"); 2136 holder.addClass("submit-holder"); 2137 auto i = holder.addChild("input"); 2138 i.type = "submit"; 2139 if(label.length) 2140 i.value = label; 2141 return holder; 2142 } 2143 2144 } 2145 2146 ///. 2147 class DocumentFragment : Element { 2148 ///. 2149 this(Document _parentDocument) { 2150 tagName = "#fragment"; 2151 super(_parentDocument); 2152 } 2153 2154 ///. 2155 override string writeToAppender(Appender!string where = appender!string()) const { 2156 return this.innerHTML(where); 2157 } 2158 } 2159 2160 /// Given text, encode all html entities on it - &, <, >, and ". This function also 2161 /// encodes all 8 bit characters as entities, thus ensuring the resultant text will work 2162 /// even if your charset isn't set right. 2163 /// 2164 /// The output parameter can be given to append to an existing buffer. You don't have to 2165 /// pass one; regardless, the return value will be usable for you, with just the data encoded. 2166 string htmlEntitiesEncode(string data, Appender!string output = appender!string()) { 2167 // if there's no entities, we can save a lot of time by not bothering with the 2168 // decoding loop. This check cuts the net toString time by better than half in my test. 2169 // let me know if it made your tests worse though, since if you use an entity in just about 2170 // every location, the check will add time... but I suspect the average experience is like mine 2171 // since the check gives up as soon as it can anyway. 2172 2173 bool shortcut = true; 2174 foreach(char c; data) { 2175 // non ascii chars are always higher than 127 in utf8; we'd better go to the full decoder if we see it. 2176 if(c == '<' || c == '>' || c == '"' || c == '&' || cast(uint) c > 127) { 2177 shortcut = false; // there's actual work to be done 2178 break; 2179 } 2180 } 2181 2182 if(shortcut) { 2183 output.put(data); 2184 return data; 2185 } 2186 2187 auto start = output.data.length; 2188 2189 output.reserve(data.length + 64); // grab some extra space for the encoded entities 2190 2191 foreach(dchar d; data) { 2192 if(d == '&') 2193 output.put("&"); 2194 else if (d == '<') 2195 output.put("<"); 2196 else if (d == '>') 2197 output.put(">"); 2198 else if (d == '\"') 2199 output.put("""); 2200 // else if (d == '\'') 2201 // output.put("'"); // if you are in an attribute, it might be important to encode for the same reason as double quotes 2202 // FIXME: should I encode apostrophes too? as '... I could also do space but if your html is so bad that it doesn't 2203 // quote attributes at all, maybe you deserve the xss. Encoding spaces will make everything really ugly so meh 2204 // idk about apostrophes though. Might be worth it, might not. 2205 else if (d < 128 && d > 0) 2206 output.put(d); 2207 else 2208 output.put("&#" ~ std.conv.to!string(cast(int) d) ~ ";"); 2209 } 2210 2211 //assert(output !is null); // this fails on empty attributes..... 2212 return output.data[start .. $]; 2213 2214 // data = data.replace("\u00a0", " "); 2215 } 2216 2217 /// An alias for htmlEntitiesEncode; it works for xml too 2218 string xmlEntitiesEncode(string data) { 2219 return htmlEntitiesEncode(data); 2220 } 2221 2222 /// This helper function is used for decoding html entities. It has a hard-coded list of entities and characters. 2223 dchar parseEntity(in dchar[] entity) { 2224 switch(entity[1..$-1]) { 2225 case "quot": 2226 return '"'; 2227 case "apos": 2228 return '\''; 2229 case "lt": 2230 return '<'; 2231 case "gt": 2232 return '>'; 2233 case "amp": 2234 return '&'; 2235 // the next are html rather than xml 2236 2237 case "Agrave": return '\u00C0'; 2238 case "Aacute": return '\u00C1'; 2239 case "Acirc": return '\u00C2'; 2240 case "Atilde": return '\u00C3'; 2241 case "Auml": return '\u00C4'; 2242 case "Aring": return '\u00C5'; 2243 case "AElig": return '\u00C6'; 2244 case "Ccedil": return '\u00C7'; 2245 case "Egrave": return '\u00C8'; 2246 case "Eacute": return '\u00C9'; 2247 case "Ecirc": return '\u00CA'; 2248 case "Euml": return '\u00CB'; 2249 case "Igrave": return '\u00CC'; 2250 case "Iacute": return '\u00CD'; 2251 case "Icirc": return '\u00CE'; 2252 case "Iuml": return '\u00CF'; 2253 case "ETH": return '\u00D0'; 2254 case "Ntilde": return '\u00D1'; 2255 case "Ograve": return '\u00D2'; 2256 case "Oacute": return '\u00D3'; 2257 case "Ocirc": return '\u00D4'; 2258 case "Otilde": return '\u00D5'; 2259 case "Ouml": return '\u00D6'; 2260 case "Oslash": return '\u00D8'; 2261 case "Ugrave": return '\u00D9'; 2262 case "Uacute": return '\u00DA'; 2263 case "Ucirc": return '\u00DB'; 2264 case "Uuml": return '\u00DC'; 2265 case "Yacute": return '\u00DD'; 2266 case "THORN": return '\u00DE'; 2267 case "szlig": return '\u00DF'; 2268 case "agrave": return '\u00E0'; 2269 case "aacute": return '\u00E1'; 2270 case "acirc": return '\u00E2'; 2271 case "atilde": return '\u00E3'; 2272 case "auml": return '\u00E4'; 2273 case "aring": return '\u00E5'; 2274 case "aelig": return '\u00E6'; 2275 case "ccedil": return '\u00E7'; 2276 case "egrave": return '\u00E8'; 2277 case "eacute": return '\u00E9'; 2278 case "ecirc": return '\u00EA'; 2279 case "euml": return '\u00EB'; 2280 case "igrave": return '\u00EC'; 2281 case "iacute": return '\u00ED'; 2282 case "icirc": return '\u00EE'; 2283 case "iuml": return '\u00EF'; 2284 case "eth": return '\u00F0'; 2285 case "ntilde": return '\u00F1'; 2286 case "ograve": return '\u00F2'; 2287 case "oacute": return '\u00F3'; 2288 case "ocirc": return '\u00F4'; 2289 case "otilde": return '\u00F5'; 2290 case "ouml": return '\u00F6'; 2291 case "oslash": return '\u00F8'; 2292 case "ugrave": return '\u00F9'; 2293 case "uacute": return '\u00FA'; 2294 case "ucirc": return '\u00FB'; 2295 case "uuml": return '\u00FC'; 2296 case "yacute": return '\u00FD'; 2297 case "thorn": return '\u00FE'; 2298 case "yuml": return '\u00FF'; 2299 case "nbsp": return '\u00A0'; 2300 case "iexcl": return '\u00A1'; 2301 case "cent": return '\u00A2'; 2302 case "pound": return '\u00A3'; 2303 case "curren": return '\u00A4'; 2304 case "yen": return '\u00A5'; 2305 case "brvbar": return '\u00A6'; 2306 case "sect": return '\u00A7'; 2307 case "uml": return '\u00A8'; 2308 case "copy": return '\u00A9'; 2309 case "ordf": return '\u00AA'; 2310 case "laquo": return '\u00AB'; 2311 case "not": return '\u00AC'; 2312 case "shy": return '\u00AD'; 2313 case "reg": return '\u00AE'; 2314 case "ldquo": return '\u201c'; 2315 case "rdquo": return '\u201d'; 2316 case "macr": return '\u00AF'; 2317 case "deg": return '\u00B0'; 2318 case "plusmn": return '\u00B1'; 2319 case "sup2": return '\u00B2'; 2320 case "sup3": return '\u00B3'; 2321 case "acute": return '\u00B4'; 2322 case "micro": return '\u00B5'; 2323 case "para": return '\u00B6'; 2324 case "middot": return '\u00B7'; 2325 case "cedil": return '\u00B8'; 2326 case "sup1": return '\u00B9'; 2327 case "ordm": return '\u00BA'; 2328 case "raquo": return '\u00BB'; 2329 case "frac14": return '\u00BC'; 2330 case "frac12": return '\u00BD'; 2331 case "frac34": return '\u00BE'; 2332 case "iquest": return '\u00BF'; 2333 case "times": return '\u00D7'; 2334 case "divide": return '\u00F7'; 2335 case "OElig": return '\u0152'; 2336 case "oelig": return '\u0153'; 2337 case "Scaron": return '\u0160'; 2338 case "scaron": return '\u0161'; 2339 case "Yuml": return '\u0178'; 2340 case "fnof": return '\u0192'; 2341 case "circ": return '\u02C6'; 2342 case "tilde": return '\u02DC'; 2343 case "trade": return '\u2122'; 2344 2345 case "hellip": return '\u2026'; 2346 case "ndash": return '\u2013'; 2347 case "mdash": return '\u2014'; 2348 case "lsquo": return '\u2018'; 2349 case "rsquo": return '\u2019'; 2350 2351 case "Omicron": return '\u039f'; 2352 case "omicron": return '\u03bf'; 2353 2354 // and handling numeric entities 2355 default: 2356 if(entity[1] == '#') { 2357 if(entity[2] == 'x' /*|| (!strict && entity[2] == 'X')*/) { 2358 auto hex = entity[3..$-1]; 2359 2360 auto p = intFromHex(to!string(hex).toLower()); 2361 return cast(dchar) p; 2362 } else { 2363 auto decimal = entity[2..$-1]; 2364 2365 // dealing with broken html entities 2366 while(decimal.length && (decimal[0] < '0' || decimal[0] > '9')) 2367 decimal = decimal[1 .. $]; 2368 2369 if(decimal.length == 0) 2370 return ' '; // this is really broken html 2371 // done with dealing with broken stuff 2372 2373 auto p = std.conv.to!int(decimal); 2374 return cast(dchar) p; 2375 } 2376 } else 2377 return '\ufffd'; // replacement character diamond thing 2378 } 2379 2380 assert(0); 2381 } 2382 2383 import std.utf; 2384 import std.stdio; 2385 2386 /// This takes a string of raw HTML and decodes the entities into a nice D utf-8 string. 2387 /// By default, it uses loose mode - it will try to return a useful string from garbage input too. 2388 /// Set the second parameter to true if you'd prefer it to strictly throw exceptions on garbage input. 2389 string htmlEntitiesDecode(string data, bool strict = false) { 2390 // this check makes a *big* difference; about a 50% improvement of parse speed on my test. 2391 if(data.indexOf("&") == -1) // all html entities begin with & 2392 return data; // if there are no entities in here, we can return the original slice and save some time 2393 2394 char[] a; // this seems to do a *better* job than appender! 2395 2396 char[4] buffer; 2397 2398 bool tryingEntity = false; 2399 dchar[] entityBeingTried; 2400 int entityAttemptIndex = 0; 2401 2402 foreach(dchar ch; data) { 2403 if(tryingEntity) { 2404 entityAttemptIndex++; 2405 entityBeingTried ~= ch; 2406 2407 // I saw some crappy html in the wild that looked like &0ї this tries to handle that. 2408 if(ch == '&') { 2409 if(strict) 2410 throw new Exception("unterminated entity; & inside another at " ~ to!string(entityBeingTried)); 2411 2412 // if not strict, let's try to parse both. 2413 2414 if(entityBeingTried == "&&") 2415 a ~= "&"; // double amp means keep the first one, still try to parse the next one 2416 else 2417 a ~= buffer[0.. std.utf.encode(buffer, parseEntity(entityBeingTried))]; 2418 2419 // tryingEntity is still true 2420 entityBeingTried = entityBeingTried[0 .. 1]; // keep the & 2421 entityAttemptIndex = 0; // restarting o this 2422 } else 2423 if(ch == ';') { 2424 tryingEntity = false; 2425 a ~= buffer[0.. std.utf.encode(buffer, parseEntity(entityBeingTried))]; 2426 } else if(ch == ' ') { 2427 // e.g. you & i 2428 if(strict) 2429 throw new Exception("unterminated entity at " ~ to!string(entityBeingTried)); 2430 else { 2431 tryingEntity = false; 2432 a ~= to!(char[])(entityBeingTried); 2433 } 2434 } else { 2435 if(entityAttemptIndex >= 9) { 2436 if(strict) 2437 throw new Exception("unterminated entity at " ~ to!string(entityBeingTried)); 2438 else { 2439 tryingEntity = false; 2440 a ~= to!(char[])(entityBeingTried); 2441 } 2442 } 2443 } 2444 } else { 2445 if(ch == '&') { 2446 tryingEntity = true; 2447 entityBeingTried = null; 2448 entityBeingTried ~= ch; 2449 entityAttemptIndex = 0; 2450 } else { 2451 a ~= buffer[0 .. std.utf.encode(buffer, ch)]; 2452 } 2453 } 2454 } 2455 2456 if(tryingEntity) { 2457 if(strict) 2458 throw new Exception("unterminated entity at " ~ to!string(entityBeingTried)); 2459 2460 // otherwise, let's try to recover, at least so we don't drop any data 2461 a ~= to!string(entityBeingTried); 2462 // FIXME: what if we have "cool &"? should we try to parse it? 2463 } 2464 2465 return cast(string) a; // assumeUnique is actually kinda slow, lol 2466 } 2467 2468 abstract class SpecialElement : Element { 2469 this(Document _parentDocument) { 2470 super(_parentDocument); 2471 } 2472 2473 ///. 2474 override Element appendChild(Element e) { 2475 assert(0, "Cannot append to a special node"); 2476 } 2477 2478 ///. 2479 @property override int nodeType() const { 2480 return 100; 2481 } 2482 } 2483 2484 ///. 2485 class RawSource : SpecialElement { 2486 ///. 2487 this(Document _parentDocument, string s) { 2488 super(_parentDocument); 2489 source = s; 2490 tagName = "#raw"; 2491 } 2492 2493 ///. 2494 override string nodeValue() const { 2495 return this.toString(); 2496 } 2497 2498 ///. 2499 override string writeToAppender(Appender!string where = appender!string()) const { 2500 where.put(source); 2501 return source; 2502 } 2503 2504 ///. 2505 string source; 2506 } 2507 2508 abstract class ServerSideCode : SpecialElement { 2509 this(Document _parentDocument, string type) { 2510 super(_parentDocument); 2511 tagName = "#" ~ type; 2512 } 2513 2514 ///. 2515 override string nodeValue() const { 2516 return this.source; 2517 } 2518 2519 ///. 2520 override string writeToAppender(Appender!string where = appender!string()) const { 2521 auto start = where.data.length; 2522 where.put("<"); 2523 where.put(source); 2524 where.put(">"); 2525 return where.data[start .. $]; 2526 } 2527 2528 ///. 2529 string source; 2530 } 2531 2532 ///. 2533 class PhpCode : ServerSideCode { 2534 ///. 2535 this(Document _parentDocument, string s) { 2536 super(_parentDocument, "php"); 2537 source = s; 2538 } 2539 } 2540 2541 ///. 2542 class AspCode : ServerSideCode { 2543 ///. 2544 this(Document _parentDocument, string s) { 2545 super(_parentDocument, "asp"); 2546 source = s; 2547 } 2548 } 2549 2550 ///. 2551 class BangInstruction : SpecialElement { 2552 ///. 2553 this(Document _parentDocument, string s) { 2554 super(_parentDocument); 2555 source = s; 2556 tagName = "#bpi"; 2557 } 2558 2559 ///. 2560 override string nodeValue() const { 2561 return this.source; 2562 } 2563 2564 ///. 2565 override string writeToAppender(Appender!string where = appender!string()) const { 2566 auto start = where.data.length; 2567 where.put("<!"); 2568 where.put(source); 2569 where.put(">"); 2570 return where.data[start .. $]; 2571 } 2572 2573 ///. 2574 string source; 2575 } 2576 2577 ///. 2578 class QuestionInstruction : SpecialElement { 2579 ///. 2580 this(Document _parentDocument, string s) { 2581 super(_parentDocument); 2582 source = s; 2583 tagName = "#qpi"; 2584 } 2585 2586 ///. 2587 override string nodeValue() const { 2588 return this.source; 2589 } 2590 2591 ///. 2592 override string writeToAppender(Appender!string where = appender!string()) const { 2593 auto start = where.data.length; 2594 where.put("<"); 2595 where.put(source); 2596 where.put(">"); 2597 return where.data[start .. $]; 2598 } 2599 2600 ///. 2601 string source; 2602 } 2603 2604 ///. 2605 class HtmlComment : SpecialElement { 2606 ///. 2607 this(Document _parentDocument, string s) { 2608 super(_parentDocument); 2609 source = s; 2610 tagName = "#comment"; 2611 } 2612 2613 ///. 2614 override string nodeValue() const { 2615 return this.source; 2616 } 2617 2618 ///. 2619 override string writeToAppender(Appender!string where = appender!string()) const { 2620 auto start = where.data.length; 2621 where.put("<!--"); 2622 where.put(source); 2623 where.put("-->"); 2624 return where.data[start .. $]; 2625 } 2626 2627 ///. 2628 string source; 2629 } 2630 2631 2632 2633 2634 ///. 2635 class TextNode : Element { 2636 public: 2637 ///. 2638 this(Document _parentDocument, string e) { 2639 super(_parentDocument); 2640 contents = e; 2641 tagName = "#text"; 2642 } 2643 2644 string opDispatch(string name)(string v = null) if(0) { return null; } // text nodes don't have attributes 2645 2646 ///. 2647 static TextNode fromUndecodedString(Document _parentDocument, string html) { 2648 auto e = new TextNode(_parentDocument, ""); 2649 e.contents = htmlEntitiesDecode(html, _parentDocument is null ? false : !_parentDocument.loose); 2650 return e; 2651 } 2652 2653 ///. 2654 override @property Element cloned() { 2655 auto n = new TextNode(parentDocument, contents); 2656 return n; 2657 } 2658 2659 ///. 2660 override string nodeValue() const { 2661 return this.contents; //toString(); 2662 } 2663 2664 ///. 2665 @property override int nodeType() const { 2666 return NodeType.Text; 2667 } 2668 2669 ///. 2670 override string writeToAppender(Appender!string where = appender!string()) const { 2671 string s; 2672 if(contents.length) 2673 s = htmlEntitiesEncode(contents, where); 2674 else 2675 s = ""; 2676 2677 assert(s !is null); 2678 return s; 2679 } 2680 2681 ///. 2682 override Element appendChild(Element e) { 2683 assert(0, "Cannot append to a text node"); 2684 } 2685 2686 ///. 2687 string contents; 2688 // alias contents content; // I just mistype this a lot, 2689 } 2690 2691 /** 2692 There are subclasses of Element offering improved helper 2693 functions for the element in HTML. 2694 */ 2695 2696 ///. 2697 class Link : Element { 2698 2699 ///. 2700 this(Document _parentDocument) { 2701 super(_parentDocument); 2702 this.tagName = "a"; 2703 } 2704 2705 2706 ///. 2707 this(string href, string text) { 2708 super("a"); 2709 setAttribute("href", href); 2710 innerText = text; 2711 } 2712 /+ 2713 /// Returns everything in the href EXCEPT the query string 2714 @property string targetSansQuery() { 2715 2716 } 2717 2718 ///. 2719 @property string domainName() { 2720 2721 } 2722 2723 ///. 2724 @property string path 2725 +/ 2726 /// This gets a variable from the URL's query string. 2727 string getValue(string name) { 2728 auto vars = variablesHash(); 2729 if(name in vars) 2730 return vars[name]; 2731 return null; 2732 } 2733 2734 private string[string] variablesHash() { 2735 string href = getAttribute("href"); 2736 if(href is null) 2737 return null; 2738 2739 auto ques = href.indexOf("?"); 2740 string str = ""; 2741 if(ques != -1) { 2742 str = href[ques+1..$]; 2743 2744 auto fragment = str.indexOf("#"); 2745 if(fragment != -1) 2746 str = str[0..fragment]; 2747 } 2748 2749 string[] variables = str.split("&"); 2750 2751 string[string] hash; 2752 2753 foreach(var; variables) { 2754 auto index = var.indexOf("="); 2755 if(index == -1) 2756 hash[var] = ""; 2757 else { 2758 hash[decodeComponent(var[0..index])] = decodeComponent(var[index + 1 .. $]); 2759 } 2760 } 2761 2762 return hash; 2763 } 2764 2765 ///. 2766 /*private*/ void updateQueryString(string[string] vars) { 2767 string href = getAttribute("href"); 2768 2769 auto question = href.indexOf("?"); 2770 if(question != -1) 2771 href = href[0..question]; 2772 2773 string frag = ""; 2774 auto fragment = href.indexOf("#"); 2775 if(fragment != -1) { 2776 frag = href[fragment..$]; 2777 href = href[0..fragment]; 2778 } 2779 2780 string query = "?"; 2781 bool first = true; 2782 foreach(name, value; vars) { 2783 if(!first) 2784 query ~= "&"; 2785 else 2786 first = false; 2787 2788 query ~= encodeComponent(name); 2789 if(value.length) 2790 query ~= "=" ~ encodeComponent(value); 2791 } 2792 2793 if(query != "?") 2794 href ~= query; 2795 2796 href ~= frag; 2797 2798 setAttribute("href", href); 2799 } 2800 2801 /// Sets or adds the variable with the given name to the given value 2802 /// It automatically URI encodes the values and takes care of the ? and &. 2803 override void setValue(string name, string variable) { 2804 auto vars = variablesHash(); 2805 vars[name] = variable; 2806 2807 updateQueryString(vars); 2808 } 2809 2810 /// Removes the given variable from the query string 2811 void removeValue(string name) { 2812 auto vars = variablesHash(); 2813 vars.remove(name); 2814 2815 updateQueryString(vars); 2816 } 2817 2818 /* 2819 ///. 2820 override string toString() { 2821 2822 } 2823 2824 ///. 2825 override string getAttribute(string name) { 2826 if(name == "href") { 2827 2828 } else 2829 return super.getAttribute(name); 2830 } 2831 */ 2832 } 2833 2834 ///. 2835 class Form : Element { 2836 2837 ///. 2838 this(Document _parentDocument) { 2839 super(_parentDocument); 2840 tagName = "form"; 2841 } 2842 2843 override Element addField(string label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) { 2844 auto t = this.querySelector("fieldset div"); 2845 if(t is null) 2846 return super.addField(label, name, type, fieldOptions); 2847 else 2848 return t.addField(label, name, type, fieldOptions); 2849 } 2850 2851 override Element addField(string label, string name, FormFieldOptions fieldOptions) { 2852 auto type = "text"; 2853 auto t = this.querySelector("fieldset div"); 2854 if(t is null) 2855 return super.addField(label, name, type, fieldOptions); 2856 else 2857 return t.addField(label, name, type, fieldOptions); 2858 } 2859 2860 override Element addField(string label, string name, string[string] options, FormFieldOptions fieldOptions = FormFieldOptions.none) { 2861 auto t = this.querySelector("fieldset div"); 2862 if(t is null) 2863 return super.addField(label, name, options, fieldOptions); 2864 else 2865 return t.addField(label, name, options, fieldOptions); 2866 } 2867 2868 override void setValue(string field, string value) { 2869 setValue(field, value, true); 2870 } 2871 2872 // FIXME: doesn't handle arrays; multiple fields can have the same name 2873 2874 /// Set's the form field's value. For input boxes, this sets the value attribute. For 2875 /// textareas, it sets the innerText. For radio boxes and select boxes, it removes 2876 /// the checked/selected attribute from all, and adds it to the one matching the value. 2877 /// For checkboxes, if the value is non-null and not empty, it checks the box. 2878 2879 /// If you set a value that doesn't exist, it throws an exception if makeNew is false. 2880 /// Otherwise, it makes a new input with type=hidden to keep the value. 2881 void setValue(string field, string value, bool makeNew) { 2882 auto eles = getField(field); 2883 if(eles.length == 0) { 2884 if(makeNew) { 2885 addInput(field, value); 2886 return; 2887 } else 2888 throw new Exception("form field does not exist"); 2889 } 2890 2891 if(eles.length == 1) { 2892 auto e = eles[0]; 2893 switch(e.tagName) { 2894 default: assert(0); 2895 case "textarea": 2896 e.innerText = value; 2897 break; 2898 case "input": 2899 string type = e.getAttribute("type"); 2900 if(type is null) { 2901 e.value = value; 2902 return; 2903 } 2904 switch(type) { 2905 case "checkbox": 2906 case "radio": 2907 if(value.length) 2908 e.setAttribute("checked", "checked"); 2909 else 2910 e.removeAttribute("checked"); 2911 break; 2912 default: 2913 e.value = value; 2914 return; 2915 } 2916 break; 2917 case "select": 2918 bool found = false; 2919 foreach(child; e.tree) { 2920 if(child.tagName != "option") 2921 continue; 2922 string val = child.getAttribute("value"); 2923 if(val is null) 2924 val = child.innerText; 2925 if(val == value) { 2926 child.setAttribute("selected", "selected"); 2927 found = true; 2928 } else 2929 child.removeAttribute("selected"); 2930 } 2931 2932 if(!found) { 2933 e.addChild("option", value) 2934 .setAttribute("selected", "selected"); 2935 } 2936 break; 2937 } 2938 } else { 2939 // assume radio boxes 2940 foreach(e; eles) { 2941 string val = e.getAttribute("value"); 2942 //if(val is null) 2943 // throw new Exception("don't know what to do with radio boxes with null value"); 2944 if(val == value) 2945 e.setAttribute("checked", "checked"); 2946 else 2947 e.removeAttribute("checked"); 2948 } 2949 } 2950 } 2951 2952 /// This takes an array of strings and adds hidden <input> elements for each one of them. Unlike setValue, 2953 /// it makes no attempt to find and modify existing elements in the form to the new values. 2954 void addValueArray(string key, string[] arrayOfValues) { 2955 foreach(arr; arrayOfValues) 2956 addChild("input", key, arr); 2957 } 2958 2959 /// Gets the value of the field; what would be given if it submitted right now. (so 2960 /// it handles select boxes and radio buttons too). For checkboxes, if a value isn't 2961 /// given, but it is checked, it returns "checked", since null and "" are indistinguishable 2962 string getValue(string field) { 2963 auto eles = getField(field); 2964 if(eles.length == 0) 2965 return ""; 2966 if(eles.length == 1) { 2967 auto e = eles[0]; 2968 switch(e.tagName) { 2969 default: assert(0); 2970 case "input": 2971 if(e.type == "checkbox") { 2972 if(e.checked) 2973 return e.value.length ? e.value : "checked"; 2974 return ""; 2975 } else 2976 return e.value; 2977 case "textarea": 2978 return e.innerText; 2979 case "select": 2980 foreach(child; e.tree) { 2981 if(child.tagName != "option") 2982 continue; 2983 if(child.selected) 2984 return child.value; 2985 } 2986 break; 2987 } 2988 } else { 2989 // assuming radio 2990 foreach(e; eles) { 2991 if(e.checked) 2992 return e.value; 2993 } 2994 } 2995 2996 return ""; 2997 } 2998 2999 // FIXME: doesn't handle multiple elements with the same name (except radio buttons) 3000 ///. 3001 string getPostableData() { 3002 bool[string] namesDone; 3003 3004 string ret; 3005 bool outputted = false; 3006 3007 foreach(e; getElementsBySelector("[name]")) { 3008 if(e.name in namesDone) 3009 continue; 3010 3011 if(outputted) 3012 ret ~= "&"; 3013 else 3014 outputted = true; 3015 3016 ret ~= std.uri.encodeComponent(e.name) ~ "=" ~ std.uri.encodeComponent(getValue(e.name)); 3017 3018 namesDone[e.name] = true; 3019 } 3020 3021 return ret; 3022 } 3023 3024 /// Gets the actual elements with the given name 3025 Element[] getField(string name) { 3026 Element[] ret; 3027 foreach(e; tree) { 3028 if(e.name == name) 3029 ret ~= e; 3030 } 3031 return ret; 3032 } 3033 3034 /// Grabs the <label> with the given for tag, if there is one. 3035 Element getLabel(string forId) { 3036 foreach(e; tree) 3037 if(e.tagName == "label" && e.getAttribute("for") == forId) 3038 return e; 3039 return null; 3040 } 3041 3042 /// Adds a new INPUT field to the end of the form with the given attributes. 3043 Element addInput(string name, string value, string type = "hidden") { 3044 auto e = new Element(parentDocument, "input", null, true); 3045 e.name = name; 3046 e.value = value; 3047 e.type = type; 3048 3049 appendChild(e); 3050 3051 return e; 3052 } 3053 3054 /// Removes the given field from the form. It finds the element and knocks it right out. 3055 void removeField(string name) { 3056 foreach(e; getField(name)) 3057 e.parentNode.removeChild(e); 3058 } 3059 3060 /+ 3061 /// Returns all form members. 3062 @property Element[] elements() { 3063 3064 } 3065 3066 ///. 3067 string opDispatch(string name)(string v = null) 3068 // filter things that should actually be attributes on the form 3069 if( name != "method" && name != "action" && name != "enctype" 3070 && name != "style" && name != "name" && name != "id" && name != "class") 3071 { 3072 3073 } 3074 +/ 3075 /+ 3076 void submit() { 3077 // take its elements and submit them through http 3078 } 3079 +/ 3080 } 3081 3082 import std.conv; 3083 3084 ///. 3085 class Table : Element { 3086 3087 ///. 3088 this(Document _parentDocument) { 3089 super(_parentDocument); 3090 tagName = "table"; 3091 } 3092 3093 ///. 3094 Element th(T)(T t) { 3095 Element e; 3096 if(parentDocument !is null) 3097 e = parentDocument.createElement("th"); 3098 else 3099 e = Element.make("th"); 3100 static if(is(T == Html)) 3101 e.innerHTML = t; 3102 else 3103 e.innerText = to!string(t); 3104 return e; 3105 } 3106 3107 ///. 3108 Element td(T)(T t) { 3109 Element e; 3110 if(parentDocument !is null) 3111 e = parentDocument.createElement("td"); 3112 else 3113 e = Element.make("td"); 3114 static if(is(T == Html)) 3115 e.innerHTML = t; 3116 else 3117 e.innerText = to!string(t); 3118 return e; 3119 } 3120 3121 /// . 3122 Element appendHeaderRow(T...)(T t) { 3123 return appendRowInternal("th", "thead", t); 3124 } 3125 3126 /// . 3127 Element appendFooterRow(T...)(T t) { 3128 return appendRowInternal("td", "tfoot", t); 3129 } 3130 3131 /// . 3132 Element appendRow(T...)(T t) { 3133 return appendRowInternal("td", "tbody", t); 3134 } 3135 3136 void addColumnClasses(string[] classes...) { 3137 auto grid = getGrid(); 3138 foreach(row; grid) 3139 foreach(i, cl; classes) { 3140 if(cl.length) 3141 if(i < row.length) 3142 row[i].addClass(cl); 3143 } 3144 } 3145 3146 private Element appendRowInternal(T...)(string innerType, string findType, T t) { 3147 Element row = Element.make("tr"); 3148 3149 foreach(e; t) { 3150 static if(is(typeof(e) : Element)) { 3151 if(e.tagName == "td" || e.tagName == "th") 3152 row.appendChild(e); 3153 else { 3154 Element a = Element.make(innerType); 3155 3156 a.appendChild(e); 3157 3158 row.appendChild(a); 3159 } 3160 } else static if(is(typeof(e) == Html)) { 3161 Element a = Element.make(innerType); 3162 a.innerHTML = e.source; 3163 row.appendChild(a); 3164 } else static if(is(typeof(e) == Element[])) { 3165 Element a = Element.make(innerType); 3166 foreach(ele; e) 3167 a.appendChild(ele); 3168 row.appendChild(a); 3169 } else { 3170 Element a = Element.make(innerType); 3171 a.innerText = to!string(e); 3172 row.appendChild(a); 3173 } 3174 } 3175 3176 foreach(e; children) { 3177 if(e.tagName == findType) { 3178 e.appendChild(row); 3179 return row; 3180 } 3181 } 3182 3183 // the type was not found if we are here... let's add it so it is well-formed 3184 auto lol = this.addChild(findType); 3185 lol.appendChild(row); 3186 3187 return row; 3188 } 3189 3190 ///. 3191 Element captionElement() { 3192 Element cap; 3193 foreach(c; children) { 3194 if(c.tagName == "caption") { 3195 cap = c; 3196 break; 3197 } 3198 } 3199 3200 if(cap is null) { 3201 cap = Element.make("caption"); 3202 appendChild(cap); 3203 } 3204 3205 return cap; 3206 } 3207 3208 ///. 3209 @property string caption() { 3210 return captionElement().innerText; 3211 } 3212 3213 ///. 3214 @property void caption(string text) { 3215 captionElement().innerText = text; 3216 } 3217 3218 /// Gets the logical layout of the table as a rectangular grid of 3219 /// cells. It considers rowspan and colspan. A cell with a large 3220 /// span is represented in the grid by being referenced several times. 3221 /// The tablePortition parameter can get just a <thead>, <tbody>, or 3222 /// <tfoot> portion if you pass one. 3223 /// 3224 /// Note: the rectangular grid might include null cells. 3225 /// 3226 /// This is kinda expensive so you should call once when you want the grid, 3227 /// then do lookups on the returned array. 3228 TableCell[][] getGrid(Element tablePortition = null) 3229 in { 3230 if(tablePortition is null) 3231 assert(tablePortition is null); 3232 else { 3233 assert(tablePortition !is null); 3234 assert(tablePortition.parentNode is this); 3235 assert( 3236 tablePortition.tagName == "tbody" 3237 || 3238 tablePortition.tagName == "tfoot" 3239 || 3240 tablePortition.tagName == "thead" 3241 ); 3242 } 3243 } 3244 do { 3245 if(tablePortition is null) 3246 tablePortition = this; 3247 3248 TableCell[][] ret; 3249 3250 // FIXME: will also return rows of sub tables! 3251 auto rows = tablePortition.getElementsByTagName("tr"); 3252 ret.length = rows.length; 3253 3254 int maxLength = 0; 3255 3256 int insertCell(int row, int position, TableCell cell) { 3257 if(row >= ret.length) 3258 return position; // not supposed to happen - a rowspan is prolly too big. 3259 3260 if(position == -1) { 3261 position++; 3262 foreach(item; ret[row]) { 3263 if(item is null) 3264 break; 3265 position++; 3266 } 3267 } 3268 3269 if(position < ret[row].length) 3270 ret[row][position] = cell; 3271 else 3272 foreach(i; ret[row].length .. position + 1) { 3273 if(i == position) 3274 ret[row] ~= cell; 3275 else 3276 ret[row] ~= null; 3277 } 3278 return position; 3279 } 3280 3281 foreach(size_t i, rowElement; rows) { 3282 auto row = cast(TableRow) rowElement; 3283 assert(row !is null); 3284 assert(i < ret.length); 3285 3286 int position = 0; 3287 foreach(cellElement; rowElement.childNodes) { 3288 auto cell = cast(TableCell) cellElement; 3289 if(cell is null) 3290 continue; 3291 3292 // FIXME: colspan == 0 or rowspan == 0 3293 // is supposed to mean fill in the rest of 3294 // the table, not skip it 3295 foreach(int j; 0 .. cell.colspan) { 3296 foreach(int k; 0 .. cell.rowspan) 3297 // if the first row, always append. 3298 insertCell(cast(int)(k + i), k == 0 ? -1 : position, cell); 3299 position++; 3300 } 3301 } 3302 3303 if(ret[i].length > maxLength) 3304 maxLength = cast(int) ret[i].length; 3305 } 3306 3307 // want to ensure it's rectangular 3308 foreach(ref r; ret) { 3309 foreach(i; r.length .. maxLength) 3310 r ~= null; 3311 } 3312 3313 return ret; 3314 } 3315 } 3316 3317 /// Represents a table row element - a <tr> 3318 class TableRow : Element { 3319 ///. 3320 this(Document _parentDocument) { 3321 super(_parentDocument); 3322 tagName = "tr"; 3323 } 3324 3325 // FIXME: the standard says there should be a lot more in here, 3326 // but meh, I never use it and it's a pain to implement. 3327 } 3328 3329 /// Represents anything that can be a table cell - <td> or <th> html. 3330 class TableCell : Element { 3331 ///. 3332 this(Document _parentDocument, string _tagName) { 3333 super(_parentDocument, _tagName); 3334 } 3335 3336 @property int rowspan() const { 3337 int ret = 1; 3338 auto it = getAttribute("rowspan"); 3339 if(it.length) 3340 ret = to!int(it); 3341 return ret; 3342 } 3343 3344 @property int colspan() const { 3345 int ret = 1; 3346 auto it = getAttribute("colspan"); 3347 if(it.length) 3348 ret = to!int(it); 3349 return ret; 3350 } 3351 3352 @property int rowspan(int i) { 3353 setAttribute("rowspan", to!string(i)); 3354 return i; 3355 } 3356 3357 @property int colspan(int i) { 3358 setAttribute("colspan", to!string(i)); 3359 return i; 3360 } 3361 3362 } 3363 3364 3365 ///. 3366 class MarkupException : Exception { 3367 3368 ///. 3369 this(string message, string file = __FILE__, size_t line = __LINE__) { 3370 super(message, file, line); 3371 } 3372 } 3373 3374 /// This is used when you are using one of the require variants of navigation, and no matching element can be found in the tree. 3375 class ElementNotFoundException : Exception { 3376 3377 /// type == kind of element you were looking for and search == a selector describing the search. 3378 this(string type, string search, string file = __FILE__, size_t line = __LINE__) { 3379 super("Element of type '"~type~"' matching {"~search~"} not found.", file, line); 3380 } 3381 } 3382 3383 /// The html struct is used to differentiate between regular text nodes and html in certain functions 3384 /// 3385 /// Easiest way to construct it is like this: auto html = Html("<p>hello</p>"); 3386 struct Html { 3387 /// This string holds the actual html. Use it to retrieve the contents. 3388 string source; 3389 } 3390 3391 /// The main document interface, including a html parser. 3392 class Document : FileResource { 3393 ///. 3394 this(string data, bool caseSensitive = false, bool strict = false) { 3395 parseUtf8(data, caseSensitive, strict); 3396 } 3397 3398 /** 3399 Creates an empty document. It has *nothing* in it at all. 3400 */ 3401 this() { 3402 3403 } 3404 3405 /// This is just something I'm toying with. Right now, you use opIndex to put in css selectors. 3406 /// It returns a struct that forwards calls to all elements it holds, and returns itself so you 3407 /// can chain it. 3408 /// 3409 /// Example: document["p"].innerText("hello").addClass("modified"); 3410 /// 3411 /// Equivalent to: foreach(e; document.getElementsBySelector("p")) { e.innerText("hello"); e.addClas("modified"); } 3412 /// 3413 /// Note: always use function calls (not property syntax) and don't use toString in there for best results. 3414 /// 3415 /// You can also do things like: document["p"]["b"] though tbh I'm not sure why since the selector string can do all that anyway. Maybe 3416 /// you could put in some kind of custom filter function tho. 3417 ElementCollection opIndex(string selector) { 3418 auto e = ElementCollection(this.root); 3419 return e[selector]; 3420 } 3421 3422 string _contentType = "text/html; charset=utf-8"; 3423 3424 /// If you're using this for some other kind of XML, you can 3425 /// set the content type here. 3426 /// 3427 /// Note: this has no impact on the function of this class. 3428 /// It is only used if the document is sent via a protocol like HTTP. 3429 /// 3430 /// This may be called by parse() if it recognizes the data. Otherwise, 3431 /// if you don't set it, it assumes text/html; charset=utf-8. 3432 @property string contentType(string mimeType) { 3433 _contentType = mimeType; 3434 return _contentType; 3435 } 3436 3437 /// implementing the FileResource interface, useful for sending via 3438 /// http automatically. 3439 override @property string contentType() const { 3440 return _contentType; 3441 } 3442 3443 /// implementing the FileResource interface; it calls toString. 3444 override immutable(ubyte)[] getData() const { 3445 return cast(immutable(ubyte)[]) this.toString(); 3446 } 3447 3448 3449 /// Concatenates any consecutive text nodes 3450 /* 3451 void normalize() { 3452 3453 } 3454 */ 3455 3456 /// This will set delegates for parseSaw* (note: this overwrites anything else you set, and you setting subsequently will overwrite this) that add those things to the dom tree when it sees them. 3457 /// Call this before calling parse(). 3458 3459 /// Note this will also preserve the prolog and doctype from the original file, if there was one. 3460 void enableAddingSpecialTagsToDom() { 3461 parseSawComment = (string) => true; 3462 parseSawAspCode = (string) => true; 3463 parseSawPhpCode = (string) => true; 3464 parseSawQuestionInstruction = (string) => true; 3465 parseSawBangInstruction = (string) => true; 3466 } 3467 3468 /// If the parser sees a html comment, it will call this callback 3469 /// <!-- comment --> will call parseSawComment(" comment ") 3470 /// Return true if you want the node appended to the document. 3471 bool delegate(string) parseSawComment; 3472 3473 /// If the parser sees <% asp code... %>, it will call this callback. 3474 /// It will be passed "% asp code... %" or "%= asp code .. %" 3475 /// Return true if you want the node appended to the document. 3476 bool delegate(string) parseSawAspCode; 3477 3478 /// If the parser sees <?php php code... ?>, it will call this callback. 3479 /// It will be passed "?php php code... ?" or "?= asp code .. ?" 3480 /// Note: dom.d cannot identify the other php <? code ?> short format. 3481 /// Return true if you want the node appended to the document. 3482 bool delegate(string) parseSawPhpCode; 3483 3484 /// if it sees a <?xxx> that is not php or asp 3485 /// it calls this function with the contents. 3486 /// <?SOMETHING foo> calls parseSawQuestionInstruction("?SOMETHING foo") 3487 /// Unlike the php/asp ones, this ends on the first > it sees, without requiring ?>. 3488 /// Return true if you want the node appended to the document. 3489 bool delegate(string) parseSawQuestionInstruction; 3490 3491 /// if it sees a <! that is not CDATA or comment (CDATA is handled automatically and comments call parseSawComment), 3492 /// it calls this function with the contents. 3493 /// <!SOMETHING foo> calls parseSawBangInstruction("SOMETHING foo") 3494 /// Return true if you want the node appended to the document. 3495 bool delegate(string) parseSawBangInstruction; 3496 3497 /// Given the kind of garbage you find on the Internet, try to make sense of it. 3498 /// Equivalent to document.parse(data, false, false, null); 3499 /// (Case-insensitive, non-strict, determine character encoding from the data.) 3500 3501 /// NOTE: this makes no attempt at added security. 3502 /// 3503 /// It is a template so it lazily imports characterencodings. 3504 void parseGarbage()(string data) { 3505 parse(data, false, false, null); 3506 } 3507 3508 /// Parses well-formed UTF-8, case-sensitive, XML or XHTML 3509 /// Will throw exceptions on things like unclosed tags. 3510 void parseStrict(string data) { 3511 parseStream(toUtf8Stream(data), true, true); 3512 } 3513 3514 /// Parses well-formed UTF-8 in loose mode (by default). Tries to correct 3515 /// tag soup, but does NOT try to correct bad character encodings. 3516 /// 3517 /// They will still throw an exception. 3518 void parseUtf8(string data, bool caseSensitive = false, bool strict = false) { 3519 parseStream(toUtf8Stream(data), caseSensitive, strict); 3520 } 3521 3522 // this is a template so we get lazy import behavior 3523 Utf8Stream handleDataEncoding()(in string rawdata, string dataEncoding, bool strict) { 3524 import arsd.characterencodings; 3525 // gotta determine the data encoding. If you know it, pass it in above to skip all this. 3526 if(dataEncoding is null) { 3527 dataEncoding = tryToDetermineEncoding(cast(const(ubyte[])) rawdata); 3528 // it can't tell... probably a random 8 bit encoding. Let's check the document itself. 3529 // Now, XML and HTML can both list encoding in the document, but we can't really parse 3530 // it here without changing a lot of code until we know the encoding. So I'm going to 3531 // do some hackish string checking. 3532 if(dataEncoding is null) { 3533 auto dataAsBytes = cast(immutable(ubyte)[]) rawdata; 3534 // first, look for an XML prolog 3535 auto idx = indexOfBytes(dataAsBytes, cast(immutable ubyte[]) "encoding=\""); 3536 if(idx != -1) { 3537 idx += "encoding=\"".length; 3538 // we're probably past the prolog if it's this far in; we might be looking at 3539 // content. Forget about it. 3540 if(idx > 100) 3541 idx = -1; 3542 } 3543 // if that fails, we're looking for Content-Type http-equiv or a meta charset (see html5).. 3544 if(idx == -1) { 3545 idx = indexOfBytes(dataAsBytes, cast(immutable ubyte[]) "charset="); 3546 if(idx != -1) { 3547 idx += "charset=".length; 3548 if(dataAsBytes[idx] == '"') 3549 idx++; 3550 } 3551 } 3552 3553 // found something in either branch... 3554 if(idx != -1) { 3555 // read till a quote or about 12 chars, whichever comes first... 3556 auto end = idx; 3557 while(end < dataAsBytes.length && dataAsBytes[end] != '"' && end - idx < 12) 3558 end++; 3559 3560 dataEncoding = cast(string) dataAsBytes[idx .. end]; 3561 } 3562 // otherwise, we just don't know. 3563 } 3564 } 3565 3566 if(dataEncoding is null) { 3567 if(strict) 3568 throw new MarkupException("I couldn't figure out the encoding of this document."); 3569 else 3570 // if we really don't know by here, it means we already tried UTF-8, 3571 // looked for utf 16 and 32 byte order marks, and looked for xml or meta 3572 // tags... let's assume it's Windows-1252, since that's probably the most 3573 // common aside from utf that wouldn't be labeled. 3574 3575 dataEncoding = "Windows 1252"; 3576 } 3577 3578 // and now, go ahead and convert it. 3579 3580 string data; 3581 3582 if(!strict) { 3583 // if we're in non-strict mode, we need to check 3584 // the document for mislabeling too; sometimes 3585 // web documents will say they are utf-8, but aren't 3586 // actually properly encoded. If it fails to validate, 3587 // we'll assume it's actually Windows encoding - the most 3588 // likely candidate for mislabeled garbage. 3589 dataEncoding = dataEncoding.toLower(); 3590 dataEncoding = dataEncoding.replace(" ", ""); 3591 dataEncoding = dataEncoding.replace("-", ""); 3592 dataEncoding = dataEncoding.replace("_", ""); 3593 if(dataEncoding == "utf8") { 3594 try { 3595 validate(rawdata); 3596 } catch(UTFException e) { 3597 dataEncoding = "Windows 1252"; 3598 } 3599 } 3600 } 3601 3602 if(dataEncoding != "UTF-8") { 3603 if(strict) 3604 data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, dataEncoding); 3605 else { 3606 try { 3607 data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, dataEncoding); 3608 } catch(Exception e) { 3609 data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, "Windows 1252"); 3610 } 3611 } 3612 } else 3613 data = rawdata; 3614 3615 return toUtf8Stream(data); 3616 } 3617 3618 private 3619 Utf8Stream toUtf8Stream(in string rawdata) { 3620 string data = rawdata; 3621 static if(is(Utf8Stream == string)) 3622 return data; 3623 else 3624 return new Utf8Stream(data); 3625 } 3626 3627 /** 3628 Take XMLish data and try to make the DOM tree out of it. 3629 3630 The goal isn't to be perfect, but to just be good enough to 3631 approximate Javascript's behavior. 3632 3633 If strict, it throws on something that doesn't make sense. 3634 (Examples: mismatched tags. It doesn't validate!) 3635 If not strict, it tries to recover anyway, and only throws 3636 when something is REALLY unworkable. 3637 3638 If strict is false, it uses a magic list of tags that needn't 3639 be closed. If you are writing a document specifically for this, 3640 try to avoid such - use self closed tags at least. Easier to parse. 3641 3642 The dataEncoding argument can be used to pass a specific 3643 charset encoding for automatic conversion. If null (which is NOT 3644 the default!), it tries to determine from the data itself, 3645 using the xml prolog or meta tags, and assumes UTF-8 if unsure. 3646 3647 If this assumption is wrong, it can throw on non-ascii 3648 characters! 3649 3650 3651 Note that it previously assumed the data was encoded as UTF-8, which 3652 is why the dataEncoding argument defaults to that. 3653 3654 So it shouldn't break backward compatibility. 3655 3656 But, if you want the best behavior on wild data - figuring it out from the document 3657 instead of assuming - you'll probably want to change that argument to null. 3658 3659 This is a template so it lazily imports arsd.characterencodings, which is required 3660 to fix up data encodings. 3661 3662 If you are sure the encoding is good, try parseUtf8 or parseStrict to avoid the 3663 dependency. If it is data from the Internet though, a random website, the encoding 3664 is often a lie. This function, if dataEncoding == null, can correct for that, or 3665 you can try parseGarbage. In those cases, arsd.characterencodings is required to 3666 compile. 3667 */ 3668 void parse()(in string rawdata, bool caseSensitive = false, bool strict = false, string dataEncoding = "UTF-8") { 3669 auto data = handleDataEncoding(rawdata, dataEncoding, strict); 3670 parseStream(data, caseSensitive, strict); 3671 } 3672 3673 // note: this work best in strict mode, unless data is just a simple string wrapper 3674 void parseStream(Utf8Stream data, bool caseSensitive = false, bool strict = false) { 3675 // FIXME: this parser could be faster; it's in the top ten biggest tree times according to the profiler 3676 // of my big app. 3677 3678 assert(data !is null); 3679 3680 // go through character by character. 3681 // if you see a <, consider it a tag. 3682 // name goes until the first non tagname character 3683 // then see if it self closes or has an attribute 3684 3685 // if not in a tag, anything not a tag is a big text 3686 // node child. It ends as soon as it sees a < 3687 3688 // Whitespace in text or attributes is preserved, but not between attributes 3689 3690 // & and friends are converted when I know them, left the same otherwise 3691 3692 3693 // this it should already be done correctly.. so I'm leaving it off to net a ~10% speed boost on my typical test file (really) 3694 //validate(data); // it *must* be UTF-8 for this to work correctly 3695 3696 sizediff_t pos = 0; 3697 3698 clear(); 3699 3700 loose = !caseSensitive; 3701 3702 bool sawImproperNesting = false; 3703 bool paragraphHackfixRequired = false; 3704 3705 int getLineNumber(sizediff_t p) { 3706 int line = 1; 3707 foreach(c; data[0..p]) 3708 if(c == '\n') 3709 line++; 3710 return line; 3711 } 3712 3713 void parseError(string message) { 3714 throw new MarkupException(format("char %d (line %d): %s", pos, getLineNumber(pos), message)); 3715 } 3716 3717 void eatWhitespace() { 3718 while(pos < data.length && (data[pos] == ' ' || data[pos] == '\n' || data[pos] == '\t')) 3719 pos++; 3720 } 3721 3722 string readTagName() { 3723 // remember to include : for namespaces 3724 // basically just keep going until >, /, or whitespace 3725 auto start = pos; 3726 while( data[pos] != '>' && data[pos] != '/' && 3727 data[pos] != ' ' && data[pos] != '\n' && data[pos] != '\t') 3728 { 3729 pos++; 3730 if(pos == data.length) { 3731 if(strict) 3732 throw new Exception("tag name incomplete when file ended"); 3733 else 3734 break; 3735 } 3736 } 3737 3738 if(!caseSensitive) 3739 return toLower(data[start..pos]); 3740 else 3741 return data[start..pos]; 3742 } 3743 3744 string readAttributeName() { 3745 // remember to include : for namespaces 3746 // basically just keep going until >, /, or whitespace 3747 auto start = pos; 3748 while( data[pos] != '>' && data[pos] != '/' && data[pos] != '=' && 3749 data[pos] != ' ' && data[pos] != '\n' && data[pos] != '\t') 3750 { 3751 if(data[pos] == '<') { 3752 if(strict) 3753 throw new MarkupException("The character < can never appear in an attribute name. Line " ~ to!string(getLineNumber(pos))); 3754 else 3755 break; // e.g. <a href="something" <img src="poo" /></a>. The > should have been after the href, but some shitty files don't do that right and the browser handles it, so we will too, by pretending the > was indeed there 3756 } 3757 pos++; 3758 if(pos == data.length) { 3759 if(strict) 3760 throw new Exception("unterminated attribute name"); 3761 else 3762 break; 3763 } 3764 } 3765 3766 if(!caseSensitive) 3767 return toLower(data[start..pos]); 3768 else 3769 return data[start..pos]; 3770 } 3771 3772 string readAttributeValue() { 3773 if(pos >= data.length) { 3774 if(strict) 3775 throw new Exception("no attribute value before end of file"); 3776 else 3777 return null; 3778 } 3779 switch(data[pos]) { 3780 case '\'': 3781 case '"': 3782 auto started = pos; 3783 char end = data[pos]; 3784 pos++; 3785 auto start = pos; 3786 while(pos < data.length && data[pos] != end) 3787 pos++; 3788 if(strict && pos == data.length) 3789 throw new MarkupException("Unclosed attribute value, started on char " ~ to!string(started)); 3790 string v = htmlEntitiesDecode(data[start..pos], strict); 3791 pos++; // skip over the end 3792 return v; 3793 default: 3794 if(strict) 3795 parseError("Attributes must be quoted"); 3796 // read until whitespace or terminator (/ or >) 3797 auto start = pos; 3798 while( 3799 pos < data.length && 3800 data[pos] != '>' && 3801 // unquoted attributes might be urls, so gotta be careful with them and self-closed elements 3802 !(data[pos] == '/' && pos + 1 < data.length && data[pos+1] == '>') && 3803 data[pos] != ' ' && data[pos] != '\n' && data[pos] != '\t') 3804 pos++; 3805 3806 string v = htmlEntitiesDecode(data[start..pos], strict); 3807 // don't skip the end - we'll need it later 3808 return v; 3809 } 3810 } 3811 3812 TextNode readTextNode() { 3813 auto start = pos; 3814 while(pos < data.length && data[pos] != '<') { 3815 pos++; 3816 } 3817 3818 return TextNode.fromUndecodedString(this, data[start..pos]); 3819 } 3820 3821 // this is obsolete! 3822 RawSource readCDataNode() { 3823 auto start = pos; 3824 while(pos < data.length && data[pos] != '<') { 3825 pos++; 3826 } 3827 3828 return new RawSource(this, data[start..pos]); 3829 } 3830 3831 3832 struct Ele { 3833 int type; // element or closing tag or nothing 3834 /* 3835 type == 0 means regular node, self-closed (element is valid) 3836 type == 1 means closing tag (payload is the tag name, element may be valid) 3837 type == 2 means you should ignore it completely 3838 type == 3 means it is a special element that should be appended, if possible, e.g. a <!DOCTYPE> that was chosen to be kept, php code, or comment. It will be appended at the current element if inside the root, and to a special document area if not 3839 type == 4 means the document was totally empty 3840 */ 3841 Element element; // for type == 0 or type == 3 3842 string payload; // for type == 1 3843 } 3844 // recursively read a tag 3845 Ele readElement(string[] parentChain = null) { 3846 // FIXME: this is the slowest function in this module, by far, even in strict mode. 3847 // Loose mode should perform decently, but strict mode is the important one. 3848 if(!strict && parentChain is null) 3849 parentChain = []; 3850 3851 static string[] recentAutoClosedTags; 3852 3853 if(pos >= data.length) 3854 { 3855 if(strict) { 3856 throw new MarkupException("Gone over the input (is there no root element or did it never close?), chain: " ~ to!string(parentChain)); 3857 } else { 3858 if(parentChain.length) 3859 return Ele(1, null, parentChain[0]); // in loose mode, we just assume the document has ended 3860 else 3861 return Ele(4); // signal emptiness upstream 3862 } 3863 } 3864 3865 if(data[pos] != '<') { 3866 return Ele(0, readTextNode(), null); 3867 } 3868 3869 enforce(data[pos] == '<'); 3870 pos++; 3871 if(pos == data.length) { 3872 if(strict) 3873 throw new MarkupException("Found trailing < at end of file"); 3874 // if not strict, we'll just skip the switch 3875 } else 3876 switch(data[pos]) { 3877 // I don't care about these, so I just want to skip them 3878 case '!': // might be a comment, a doctype, or a special instruction 3879 pos++; 3880 3881 // FIXME: we should store these in the tree too 3882 // though I like having it stripped out tbh. 3883 3884 if(pos == data.length) { 3885 if(strict) 3886 throw new MarkupException("<! opened at end of file"); 3887 } else if(data[pos] == '-' && (pos + 1 < data.length) && data[pos+1] == '-') { 3888 // comment 3889 pos += 2; 3890 3891 // FIXME: technically, a comment is anything 3892 // between -- and -- inside a <!> block. 3893 // so in <!-- test -- lol> , the " lol" is NOT a comment 3894 // and should probably be handled differently in here, but for now 3895 // I'll just keep running until --> since that's the common way 3896 3897 auto commentStart = pos; 3898 while(pos+3 < data.length && data[pos..pos+3] != "-->") 3899 pos++; 3900 3901 auto end = commentStart; 3902 3903 if(pos + 3 >= data.length) { 3904 if(strict) 3905 throw new MarkupException("unclosed comment"); 3906 end = data.length; 3907 pos = data.length; 3908 } else { 3909 end = pos; 3910 assert(data[pos] == '-'); 3911 pos++; 3912 assert(data[pos] == '-'); 3913 pos++; 3914 assert(data[pos] == '>'); 3915 pos++; 3916 } 3917 3918 if(parseSawComment !is null) 3919 if(parseSawComment(data[commentStart .. end])) { 3920 return Ele(3, new HtmlComment(this, data[commentStart .. end]), null); 3921 } 3922 } else if(pos + 7 <= data.length && data[pos..pos + 7] == "[CDATA[") { 3923 pos += 7; 3924 3925 auto cdataStart = pos; 3926 3927 ptrdiff_t end = -1; 3928 typeof(end) cdataEnd; 3929 3930 if(pos < data.length) { 3931 // cdata isn't allowed to nest, so this should be generally ok, as long as it is found 3932 end = data[pos .. $].indexOf("]]>"); 3933 } 3934 3935 if(end == -1) { 3936 if(strict) 3937 throw new MarkupException("Unclosed CDATA section"); 3938 end = pos; 3939 cdataEnd = pos; 3940 } else { 3941 cdataEnd = pos + end; 3942 pos = cdataEnd + 3; 3943 } 3944 3945 return Ele(0, new TextNode(this, data[cdataStart .. cdataEnd]), null); 3946 } else { 3947 auto start = pos; 3948 while(pos < data.length && data[pos] != '>') 3949 pos++; 3950 3951 auto bangEnds = pos; 3952 if(pos == data.length) { 3953 if(strict) 3954 throw new MarkupException("unclosed processing instruction (<!xxx>)"); 3955 } else pos++; // skipping the > 3956 3957 if(parseSawBangInstruction !is null) 3958 if(parseSawBangInstruction(data[start .. bangEnds])) { 3959 // FIXME: these should be able to modify the parser state, 3960 // doing things like adding entities, somehow. 3961 3962 return Ele(3, new BangInstruction(this, data[start .. bangEnds]), null); 3963 } 3964 } 3965 3966 /* 3967 if(pos < data.length && data[pos] == '>') 3968 pos++; // skip the > 3969 else 3970 assert(!strict); 3971 */ 3972 break; 3973 case '%': 3974 case '?': 3975 /* 3976 Here's what we want to support: 3977 3978 <% asp code %> 3979 <%= asp code %> 3980 <?php php code ?> 3981 <?= php code ?> 3982 3983 The contents don't really matter, just if it opens with 3984 one of the above for, it ends on the two char terminator. 3985 3986 <?something> 3987 this is NOT php code 3988 because I've seen this in the wild: <?EM-dummyText> 3989 3990 This could be php with shorttags which would be cut off 3991 prematurely because if(a >) - that > counts as the close 3992 of the tag, but since dom.d can't tell the difference 3993 between that and the <?EM> real world example, it will 3994 not try to look for the ?> ending. 3995 3996 The difference between this and the asp/php stuff is that it 3997 ends on >, not ?>. ONLY <?php or <?= ends on ?>. The rest end 3998 on >. 3999 */ 4000 4001 char end = data[pos]; 4002 auto started = pos; 4003 bool isAsp = end == '%'; 4004 int currentIndex = 0; 4005 bool isPhp = false; 4006 bool isEqualTag = false; 4007 int phpCount = 0; 4008 4009 more: 4010 pos++; // skip the start 4011 if(pos == data.length) { 4012 if(strict) 4013 throw new MarkupException("Unclosed <"~end~" by end of file"); 4014 } else { 4015 currentIndex++; 4016 if(currentIndex == 1 && data[pos] == '=') { 4017 if(!isAsp) 4018 isPhp = true; 4019 isEqualTag = true; 4020 goto more; 4021 } 4022 if(currentIndex == 1 && data[pos] == 'p') 4023 phpCount++; 4024 if(currentIndex == 2 && data[pos] == 'h') 4025 phpCount++; 4026 if(currentIndex == 3 && data[pos] == 'p' && phpCount == 2) 4027 isPhp = true; 4028 4029 if(data[pos] == '>') { 4030 if((isAsp || isPhp) && data[pos - 1] != end) 4031 goto more; 4032 // otherwise we're done 4033 } else 4034 goto more; 4035 } 4036 4037 //writefln("%s: %s", isAsp ? "ASP" : isPhp ? "PHP" : "<? ", data[started .. pos]); 4038 auto code = data[started .. pos]; 4039 4040 4041 assert((pos < data.length && data[pos] == '>') || (!strict && pos == data.length)); 4042 if(pos < data.length) 4043 pos++; // get past the > 4044 4045 if(isAsp && parseSawAspCode !is null) { 4046 if(parseSawAspCode(code)) { 4047 return Ele(3, new AspCode(this, code), null); 4048 } 4049 } else if(isPhp && parseSawPhpCode !is null) { 4050 if(parseSawPhpCode(code)) { 4051 return Ele(3, new PhpCode(this, code), null); 4052 } 4053 } else if(!isAsp && !isPhp && parseSawQuestionInstruction !is null) { 4054 if(parseSawQuestionInstruction(code)) { 4055 return Ele(3, new QuestionInstruction(this, code), null); 4056 } 4057 } 4058 break; 4059 case '/': // closing an element 4060 pos++; // skip the start 4061 auto p = pos; 4062 while(pos < data.length && data[pos] != '>') 4063 pos++; 4064 //writefln("</%s>", data[p..pos]); 4065 if(pos == data.length && data[pos-1] != '>') { 4066 if(strict) 4067 throw new MarkupException("File ended before closing tag had a required >"); 4068 else 4069 data ~= ">"; // just hack it in 4070 } 4071 pos++; // skip the '>' 4072 4073 string tname = data[p..pos-1]; 4074 if(!caseSensitive) 4075 tname = tname.toLower(); 4076 4077 return Ele(1, null, tname); // closing tag reports itself here 4078 case ' ': // assume it isn't a real element... 4079 if(strict) 4080 parseError("bad markup - improperly placed <"); 4081 else 4082 return Ele(0, TextNode.fromUndecodedString(this, "<"), null); 4083 break; 4084 default: 4085 4086 if(!strict) { 4087 // what about something that kinda looks like a tag, but isn't? 4088 auto nextTag = data[pos .. $].indexOf("<"); 4089 auto closeTag = data[pos .. $].indexOf(">"); 4090 if(closeTag != -1 && nextTag != -1) 4091 if(nextTag < closeTag) { 4092 // since attribute names cannot possibly have a < in them, we'll look for an equal since it might be an attribute value... and even in garbage mode, it'd have to be a quoted one realistically 4093 4094 auto equal = data[pos .. $].indexOf("=\""); 4095 if(equal != -1 && equal < closeTag) { 4096 // this MIGHT be ok, soldier on 4097 } else { 4098 // definitely no good, this must be a (horribly distorted) text node 4099 pos++; // skip the < we're on - don't want text node to end prematurely 4100 auto node = readTextNode(); 4101 node.contents = "<" ~ node.contents; // put this back 4102 return Ele(0, node, null); 4103 } 4104 } 4105 } 4106 4107 string tagName = readTagName(); 4108 string[string] attributes; 4109 4110 Ele addTag(bool selfClosed) { 4111 if(selfClosed) 4112 pos++; 4113 else { 4114 if(!strict) 4115 if(tagName.isInArray(selfClosedElements)) 4116 // these are de-facto self closed 4117 selfClosed = true; 4118 } 4119 4120 if(strict) 4121 enforce(data[pos] == '>');//, format("got %s when expecting >\nContext:\n%s", data[pos], data[pos - 100 .. pos + 100])); 4122 else { 4123 // if we got here, it's probably because a slash was in an 4124 // unquoted attribute - don't trust the selfClosed value 4125 if(!selfClosed) 4126 selfClosed = tagName.isInArray(selfClosedElements); 4127 4128 while(pos < data.length && data[pos] != '>') 4129 pos++; 4130 } 4131 4132 auto whereThisTagStarted = pos; // for better error messages 4133 4134 pos++; 4135 4136 auto e = createElement(tagName); 4137 e.attributes = attributes; 4138 version(dom_node_indexes) { 4139 if(e.dataset.nodeIndex.length == 0) 4140 e.dataset.nodeIndex = to!string(&(e.attributes)); 4141 } 4142 e.selfClosed = selfClosed; 4143 e.parseAttributes(); 4144 4145 4146 // HACK to handle script and style as a raw data section as it is in HTML browsers 4147 if(tagName == "script" || tagName == "style") { 4148 if(!selfClosed) { 4149 string closer = "</" ~ tagName ~ ">"; 4150 ptrdiff_t ending; 4151 if(pos >= data.length) 4152 ending = -1; 4153 else 4154 ending = indexOf(data[pos..$], closer); 4155 4156 ending = indexOf(data[pos..$], closer, 0, (loose ? CaseSensitive.no : CaseSensitive.yes)); 4157 /* 4158 if(loose && ending == -1 && pos < data.length) 4159 ending = indexOf(data[pos..$], closer.toUpper()); 4160 */ 4161 if(ending == -1) { 4162 if(strict) 4163 throw new Exception("tag " ~ tagName ~ " never closed"); 4164 else { 4165 // let's call it totally empty and do the rest of the file as text. doing it as html could still result in some weird stuff like if(a<4) being read as <4 being a tag so it comes out if(a<4></4> and other weirdness) It is either a closed script tag or the rest of the file is forfeit. 4166 if(pos < data.length) { 4167 e = new TextNode(this, data[pos .. $]); 4168 pos = data.length; 4169 } 4170 } 4171 } else { 4172 ending += pos; 4173 e.innerRawSource = data[pos..ending]; 4174 pos = ending + closer.length; 4175 } 4176 } 4177 return Ele(0, e, null); 4178 } 4179 4180 bool closed = selfClosed; 4181 4182 void considerHtmlParagraphHack(Element n) { 4183 assert(!strict); 4184 if(e.tagName == "p" && e.tagName == n.tagName) { 4185 // html lets you write <p> para 1 <p> para 1 4186 // but in the dom tree, they should be siblings, not children. 4187 paragraphHackfixRequired = true; 4188 } 4189 } 4190 4191 //writef("<%s>", tagName); 4192 while(!closed) { 4193 Ele n; 4194 if(strict) 4195 n = readElement(); 4196 else 4197 n = readElement(parentChain ~ tagName); 4198 4199 if(n.type == 4) return n; // the document is empty 4200 4201 if(n.type == 3 && n.element !is null) { 4202 // special node, append if possible 4203 if(e !is null) 4204 e.appendChild(n.element); 4205 else 4206 piecesBeforeRoot ~= n.element; 4207 } else if(n.type == 0) { 4208 if(!strict) 4209 considerHtmlParagraphHack(n.element); 4210 e.appendChild(n.element); 4211 } else if(n.type == 1) { 4212 bool found = false; 4213 if(n.payload != tagName) { 4214 if(strict) 4215 parseError(format("mismatched tag: </%s> != <%s> (opened on line %d)", n.payload, tagName, getLineNumber(whereThisTagStarted))); 4216 else { 4217 sawImproperNesting = true; 4218 // this is so we don't drop several levels of awful markup 4219 if(n.element) { 4220 if(!strict) 4221 considerHtmlParagraphHack(n.element); 4222 e.appendChild(n.element); 4223 n.element = null; 4224 } 4225 4226 // is the element open somewhere up the chain? 4227 foreach(i, parent; parentChain) 4228 if(parent == n.payload) { 4229 recentAutoClosedTags ~= tagName; 4230 // just rotating it so we don't inadvertently break stuff with vile crap 4231 if(recentAutoClosedTags.length > 4) 4232 recentAutoClosedTags = recentAutoClosedTags[1 .. $]; 4233 4234 n.element = e; 4235 return n; 4236 } 4237 4238 // if not, this is a text node; we can't fix it up... 4239 4240 // If it's already in the tree somewhere, assume it is closed by algorithm 4241 // and we shouldn't output it - odds are the user just flipped a couple tags 4242 foreach(ele; e.tree) { 4243 if(ele.tagName == n.payload) { 4244 found = true; 4245 break; 4246 } 4247 } 4248 4249 foreach(ele; recentAutoClosedTags) { 4250 if(ele == n.payload) { 4251 found = true; 4252 break; 4253 } 4254 } 4255 4256 if(!found) // if not found in the tree though, it's probably just text 4257 e.appendChild(TextNode.fromUndecodedString(this, "</"~n.payload~">")); 4258 } 4259 } else { 4260 if(n.element) { 4261 if(!strict) 4262 considerHtmlParagraphHack(n.element); 4263 e.appendChild(n.element); 4264 } 4265 } 4266 4267 if(n.payload == tagName) // in strict mode, this is always true 4268 closed = true; 4269 } else { /*throw new Exception("wtf " ~ tagName);*/ } 4270 } 4271 //writef("</%s>\n", tagName); 4272 return Ele(0, e, null); 4273 } 4274 4275 // if a tag was opened but not closed by end of file, we can arrive here 4276 if(!strict && pos >= data.length) 4277 return addTag(false); 4278 //else if(strict) assert(0); // should be caught before 4279 4280 switch(data[pos]) { 4281 default: assert(0); 4282 case '/': // self closing tag 4283 return addTag(true); 4284 case '>': 4285 return addTag(false); 4286 case ' ': 4287 case '\t': 4288 case '\n': 4289 // there might be attributes... 4290 moreAttributes: 4291 eatWhitespace(); 4292 4293 // same deal as above the switch.... 4294 if(!strict && pos >= data.length) 4295 return addTag(false); 4296 4297 if(strict && pos >= data.length) 4298 throw new MarkupException("tag open, didn't find > before end of file"); 4299 4300 switch(data[pos]) { 4301 case '/': // self closing tag 4302 return addTag(true); 4303 case '>': // closed tag; open -- we now read the contents 4304 return addTag(false); 4305 default: // it is an attribute 4306 string attrName = readAttributeName(); 4307 string attrValue = attrName; 4308 if(pos >= data.length) { 4309 if(strict) 4310 assert(0, "this should have thrown in readAttributeName"); 4311 else { 4312 data ~= ">"; 4313 goto blankValue; 4314 } 4315 } 4316 if(data[pos] == '=') { 4317 pos++; 4318 attrValue = readAttributeValue(); 4319 } 4320 4321 blankValue: 4322 4323 if(strict && attrName in attributes) 4324 throw new MarkupException("Repeated attribute: " ~ attrName); 4325 4326 if(attrName.strip().length) 4327 attributes[attrName] = attrValue; 4328 else if(strict) throw new MarkupException("wtf, zero length attribute name"); 4329 4330 if(!strict && pos < data.length && data[pos] == '<') { 4331 // this is the broken tag that doesn't have a > at the end 4332 data = data[0 .. pos] ~ ">" ~ data[pos.. $]; 4333 // let's insert one as a hack 4334 goto case '>'; 4335 } 4336 4337 goto moreAttributes; 4338 } 4339 } 4340 } 4341 4342 return Ele(2, null, null); // this is a <! or <? thing that got ignored prolly. 4343 //assert(0); 4344 } 4345 4346 eatWhitespace(); 4347 Ele r; 4348 do { 4349 r = readElement(); // there SHOULD only be one element... 4350 4351 if(r.type == 3 && r.element !is null) 4352 piecesBeforeRoot ~= r.element; 4353 4354 if(r.type == 4) 4355 break; // the document is completely empty... 4356 } while (r.type != 0 || r.element.nodeType != 1); // we look past the xml prologue and doctype; root only begins on a regular node 4357 4358 root = r.element; 4359 4360 if(!strict) // in strict mode, we'll just ignore stuff after the xml 4361 while(r.type != 4) { 4362 r = readElement(); 4363 if(r.type != 4 && r.type != 2) { // if not empty and not ignored 4364 if(r.element !is null) 4365 piecesAfterRoot ~= r.element; 4366 } 4367 } 4368 4369 if(root is null) 4370 { 4371 if(strict) 4372 assert(0, "empty document should be impossible in strict mode"); 4373 else 4374 parseUtf8(`<html><head></head><body></body></html>`); // fill in a dummy document in loose mode since that's what browsers do 4375 } 4376 4377 if(paragraphHackfixRequired) { 4378 assert(!strict); // this should never happen in strict mode; it ought to never set the hack flag... 4379 4380 // in loose mode, we can see some "bad" nesting (it's valid html, but poorly formed xml). 4381 // It's hard to handle above though because my code sucks. So, we'll fix it here. 4382 4383 auto iterator = root.tree; 4384 foreach(ele; iterator) { 4385 if(ele.parentNode is null) 4386 continue; 4387 4388 if(ele.tagName == "p" && ele.parentNode.tagName == ele.tagName) { 4389 auto shouldBePreviousSibling = ele.parentNode; 4390 auto holder = shouldBePreviousSibling.parentNode; // this is the two element's mutual holder... 4391 holder.insertAfter(shouldBePreviousSibling, ele.removeFromTree()); 4392 iterator.currentKilled(); // the current branch can be skipped; we'll hit it soon anyway since it's now next up. 4393 } 4394 } 4395 } 4396 } 4397 4398 /* end massive parse function */ 4399 4400 /// Gets the <title> element's innerText, if one exists 4401 @property string title() { 4402 bool doesItMatch(Element e) { 4403 return (e.tagName == "title"); 4404 } 4405 4406 auto e = findFirst(&doesItMatch); 4407 if(e) 4408 return e.innerText(); 4409 return ""; 4410 } 4411 4412 /// Sets the title of the page, creating a <title> element if needed. 4413 @property void title(string t) { 4414 bool doesItMatch(Element e) { 4415 return (e.tagName == "title"); 4416 } 4417 4418 auto e = findFirst(&doesItMatch); 4419 4420 if(!e) { 4421 e = createElement("title"); 4422 auto heads = getElementsByTagName("head"); 4423 if(heads.length) 4424 heads[0].appendChild(e); 4425 } 4426 4427 if(e) 4428 e.innerText = t; 4429 } 4430 4431 // FIXME: would it work to alias root this; ???? might be a good idea 4432 /// These functions all forward to the root element. See the documentation in the Element class. 4433 Element getElementById(string id) { 4434 return root.getElementById(id); 4435 } 4436 4437 /// ditto 4438 final SomeElementType requireElementById(SomeElementType = Element)(string id, string file = __FILE__, size_t line = __LINE__) 4439 if( is(SomeElementType : Element)) 4440 out(ret) { assert(ret !is null); } 4441 do { 4442 return root.requireElementById!(SomeElementType)(id, file, line); 4443 } 4444 4445 /// ditto 4446 final SomeElementType requireSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__) 4447 if( is(SomeElementType : Element)) 4448 out(ret) { assert(ret !is null); } 4449 do { 4450 return root.requireSelector!(SomeElementType)(selector, file, line); 4451 } 4452 4453 4454 /// ditto 4455 Element querySelector(string selector) { 4456 return root.querySelector(selector); 4457 } 4458 4459 /// ditto 4460 Element[] querySelectorAll(string selector) { 4461 return root.querySelectorAll(selector); 4462 } 4463 4464 /// ditto 4465 Element[] getElementsBySelector(string selector) { 4466 return root.getElementsBySelector(selector); 4467 } 4468 4469 /// ditto 4470 Element[] getElementsByTagName(string tag) { 4471 return root.getElementsByTagName(tag); 4472 } 4473 4474 /** FIXME: btw, this could just be a lazy range...... */ 4475 Element getFirstElementByTagName(string tag) { 4476 if(loose) 4477 tag = tag.toLower(); 4478 bool doesItMatch(Element e) { 4479 return e.tagName == tag; 4480 } 4481 return findFirst(&doesItMatch); 4482 } 4483 4484 /// This returns the <body> element, if there is one. (It different than Javascript, where it is called 'body', because body is a keyword in D.) 4485 Element mainBody() { 4486 return getFirstElementByTagName("body"); 4487 } 4488 4489 /// this uses a weird thing... it's [name=] if no colon and 4490 /// [property=] if colon 4491 string getMeta(string name) { 4492 string thing = name.indexOf(":") == -1 ? "name" : "property"; 4493 auto e = querySelector("head meta["~thing~"="~name~"]"); 4494 if(e is null) 4495 return null; 4496 return e.content; 4497 } 4498 4499 /// Sets a meta tag in the document header. It is kinda hacky to work easily for both Facebook open graph and traditional html meta tags/ 4500 void setMeta(string name, string value) { 4501 string thing = name.indexOf(":") == -1 ? "name" : "property"; 4502 auto e = querySelector("head meta["~thing~"="~name~"]"); 4503 if(e is null) { 4504 e = requireSelector("head").addChild("meta"); 4505 e.setAttribute(thing, name); 4506 } 4507 4508 e.content = value; 4509 } 4510 4511 ///. 4512 Form[] forms() { 4513 return cast(Form[]) getElementsByTagName("form"); 4514 } 4515 4516 ///. 4517 Form createForm() 4518 out(ret) { 4519 assert(ret !is null); 4520 } 4521 do { 4522 return cast(Form) createElement("form"); 4523 } 4524 4525 ///. 4526 Element createElement(string name) { 4527 if(loose) 4528 name = name.toLower(); 4529 4530 auto e = Element.make(name); 4531 e.parentDocument = this; 4532 4533 return e; 4534 4535 // return new Element(this, name, null, selfClosed); 4536 } 4537 4538 ///. 4539 Element createFragment() { 4540 return new DocumentFragment(this); 4541 } 4542 4543 ///. 4544 Element createTextNode(string content) { 4545 return new TextNode(this, content); 4546 } 4547 4548 4549 ///. 4550 Element findFirst(bool delegate(Element) doesItMatch) { 4551 Element result; 4552 4553 bool goThroughElement(Element e) { 4554 if(doesItMatch(e)) { 4555 result = e; 4556 return true; 4557 } 4558 4559 foreach(child; e.children) { 4560 if(goThroughElement(child)) 4561 return true; 4562 } 4563 4564 return false; 4565 } 4566 4567 goThroughElement(root); 4568 4569 return result; 4570 } 4571 4572 ///. 4573 void clear() { 4574 root = null; 4575 loose = false; 4576 } 4577 4578 ///. 4579 void setProlog(string d) { 4580 _prolog = d; 4581 prologWasSet = true; 4582 } 4583 4584 ///. 4585 private string _prolog = "<!DOCTYPE html>\n"; 4586 private bool prologWasSet = false; // set to true if the user changed it 4587 4588 @property string prolog() const { 4589 // if the user explicitly changed it, do what they want 4590 // or if we didn't keep/find stuff from the document itself, 4591 // we'll use the builtin one as a default. 4592 if(prologWasSet || piecesBeforeRoot.length == 0) 4593 return _prolog; 4594 4595 string p; 4596 foreach(e; piecesBeforeRoot) 4597 p ~= e.toString() ~ "\n"; 4598 return p; 4599 } 4600 4601 ///. 4602 override string toString() const { 4603 return prolog ~ root.toString(); 4604 } 4605 4606 ///. 4607 Element root; 4608 4609 /// if these were kept, this is stuff that appeared before the root element, such as <?xml version ?> decls and <!DOCTYPE>s 4610 Element[] piecesBeforeRoot; 4611 4612 /// stuff after the root, only stored in non-strict mode and not used in toString, but available in case you want it 4613 Element[] piecesAfterRoot; 4614 4615 ///. 4616 bool loose; 4617 4618 4619 4620 // what follows are for mutation events that you can observe 4621 void delegate(DomMutationEvent)[] eventObservers; 4622 4623 void dispatchMutationEvent(DomMutationEvent e) { 4624 foreach(o; eventObservers) 4625 o(e); 4626 } 4627 } 4628 4629 4630 // FIXME: since Document loosens the input requirements, it should probably be the sub class... 4631 /// Specializes Document for handling generic XML. (always uses strict mode, uses xml mime type and file header) 4632 class XmlDocument : Document { 4633 this(string data) { 4634 contentType = "text/xml; charset=utf-8"; 4635 _prolog = `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n"; 4636 4637 parseStrict(data); 4638 } 4639 } 4640 4641 4642 4643 // for the observers 4644 enum DomMutationOperations { 4645 setAttribute, 4646 removeAttribute, 4647 appendChild, // tagname, attributes[], innerHTML 4648 insertBefore, 4649 truncateChildren, 4650 removeChild, 4651 appendHtml, 4652 replaceHtml, 4653 appendText, 4654 replaceText, 4655 replaceTextOnly 4656 } 4657 4658 // and for observers too 4659 struct DomMutationEvent { 4660 DomMutationOperations operation; 4661 Element target; 4662 Element related; // what this means differs with the operation 4663 Element related2; 4664 string relatedString; 4665 string relatedString2; 4666 } 4667 4668 4669 private enum static string[] selfClosedElements = [ 4670 // html 4 4671 "img", "hr", "input", "br", "col", "link", "meta", 4672 // html 5 4673 "source" ]; 4674 4675 static import std.conv; 4676 4677 ///. 4678 int intFromHex(string hex) { 4679 int place = 1; 4680 int value = 0; 4681 for(sizediff_t a = hex.length - 1; a >= 0; a--) { 4682 int v; 4683 char q = hex[a]; 4684 if( q >= '0' && q <= '9') 4685 v = q - '0'; 4686 else if (q >= 'a' && q <= 'f') 4687 v = q - 'a' + 10; 4688 else throw new Exception("Illegal hex character: " ~ q); 4689 4690 value += v * place; 4691 4692 place *= 16; 4693 } 4694 4695 return value; 4696 } 4697 4698 4699 // CSS selector handling 4700 4701 // EXTENSIONS 4702 // dd - dt means get the dt directly before that dd (opposite of +) NOT IMPLEMENTED 4703 // dd -- dt means rewind siblings until you hit a dt, go as far as you need to NOT IMPLEMENTED 4704 // dt < dl means get the parent of that dt iff it is a dl (usable for "get a dt that are direct children of dl") 4705 // dt << dl means go as far up as needed to find a dl (you have an element and want its containers) NOT IMPLEMENTED 4706 // :first means to stop at the first hit, don't do more (so p + p == p ~ p:first 4707 4708 4709 4710 // CSS4 draft currently says you can change the subject (the element actually returned) by putting a ! at the end of it. 4711 // That might be useful to implement, though I do have parent selectors too. 4712 4713 ///. 4714 static immutable string[] selectorTokens = [ 4715 // It is important that the 2 character possibilities go first here for accurate lexing 4716 "~=", "*=", "|=", "^=", "$=", "!=", // "::" should be there too for full standard 4717 "<<", // my any-parent extension (reciprocal of whitespace) 4718 " - ", // previous-sibling extension (whitespace required to disambiguate tag-names) 4719 ".", ">", "+", "*", ":", "[", "]", "=", "\"", "#", ",", " ", "~", "<" 4720 ]; // other is white space or a name. 4721 4722 ///. 4723 sizediff_t idToken(string str, sizediff_t position) { 4724 sizediff_t tid = -1; 4725 char c = str[position]; 4726 foreach(a, token; selectorTokens) 4727 4728 if(c == token[0]) { 4729 if(token.length > 1) { 4730 if(position + 1 >= str.length || str[position+1] != token[1]) 4731 continue; // not this token 4732 } 4733 tid = a; 4734 break; 4735 } 4736 return tid; 4737 } 4738 4739 ///. 4740 // look, ma, no phobos! 4741 // new lexer by ketmar 4742 string[] lexSelector (string selstr) { 4743 4744 static sizediff_t idToken (string str, size_t stpos) { 4745 char c = str[stpos]; 4746 foreach (sizediff_t tidx, immutable token; selectorTokens) { 4747 if (c == token[0]) { 4748 if (token.length > 1) { 4749 assert(token.length == 2); // we don't have 3-char tokens yet 4750 if (str.length-stpos < 2 || str[stpos+1] != token[1]) continue; 4751 } 4752 return tidx; 4753 } 4754 } 4755 return -1; 4756 } 4757 4758 // skip spaces and comments 4759 static string removeLeadingBlanks (string str) { 4760 size_t curpos = 0; 4761 while (curpos < str.length) { 4762 immutable char ch = str[curpos]; 4763 // this can overflow on 4GB strings on 32-bit; 'cmon, don't be silly, nobody cares! 4764 if (ch == '/' && str.length-curpos > 1 && str[curpos+1] == '*') { 4765 // comment 4766 curpos += 2; 4767 while (curpos < str.length) { 4768 if (str[curpos] == '*' && str.length-curpos > 1 && str[curpos+1] == '/') { 4769 curpos += 2; 4770 break; 4771 } 4772 ++curpos; 4773 } 4774 } else if (ch <= 32) { 4775 // we should consider unicode spaces too, but... unicode sux anyway. 4776 ++curpos; 4777 } else { 4778 break; 4779 } 4780 } 4781 return str[curpos..$]; 4782 } 4783 4784 static bool isBlankAt() (string str, size_t pos) { 4785 // we should consider unicode spaces too, but... unicode sux anyway. 4786 return 4787 (pos < str.length && // in string 4788 (str[pos] <= 32 || // space 4789 (str.length-pos > 1 && str[pos] == '/' && str[pos+1] == '*'))); // comment 4790 } 4791 4792 string[] tokens; 4793 // lexx it! 4794 while ((selstr = removeLeadingBlanks(selstr)).length > 0) { 4795 if(selstr[0] == '\"') { 4796 auto pos = 1; 4797 bool escaping; 4798 while(pos < selstr.length && !escaping && selstr[pos] != '"') { 4799 if(escaping) 4800 escaping = false; 4801 else if(selstr[pos] == '\\') 4802 escaping = true; 4803 pos++; 4804 } 4805 4806 // FIXME: do better unescaping 4807 tokens ~= selstr[1 .. pos].replace(`\"`, `"`); 4808 selstr = selstr[pos + 1.. $]; 4809 continue; 4810 } 4811 4812 4813 // no tokens starts with escape 4814 immutable tid = idToken(selstr, 0); 4815 if (tid >= 0) { 4816 // special token 4817 tokens ~= selectorTokens[tid]; // it's funnier this way 4818 selstr = selstr[selectorTokens[tid].length..$]; 4819 continue; 4820 } 4821 // from start to space or special token 4822 size_t escapePos = size_t.max; 4823 size_t curpos = 0; // i can has chizburger^w escape at the start 4824 while (curpos < selstr.length) { 4825 if (selstr[curpos] == '\\') { 4826 // this is escape, just skip it and next char 4827 if (escapePos == size_t.max) escapePos = curpos; 4828 curpos = (selstr.length-curpos >= 2 ? curpos+2 : selstr.length); 4829 } else { 4830 if (isBlankAt(selstr, curpos) || idToken(selstr, curpos) >= 0) break; 4831 ++curpos; 4832 } 4833 } 4834 // identifier 4835 if (escapePos != size_t.max) { 4836 // i hate it when it happens 4837 string id = selstr[0..escapePos]; 4838 while (escapePos < curpos) { 4839 if (curpos-escapePos < 2) break; 4840 id ~= selstr[escapePos+1]; // escaped char 4841 escapePos += 2; 4842 immutable stp = escapePos; 4843 while (escapePos < curpos && selstr[escapePos] != '\\') ++escapePos; 4844 if (escapePos > stp) id ~= selstr[stp..escapePos]; 4845 } 4846 if (id.length > 0) tokens ~= id; 4847 } else { 4848 tokens ~= selstr[0..curpos]; 4849 } 4850 selstr = selstr[curpos..$]; 4851 } 4852 return tokens; 4853 } 4854 version(unittest_domd_lexer) unittest { 4855 assert(lexSelector(r" test\=me /*d*/") == [r"test=me"]); 4856 assert(lexSelector(r"div/**/. id") == ["div", ".", "id"]); 4857 assert(lexSelector(r" < <") == ["<", "<"]); 4858 assert(lexSelector(r" <<") == ["<<"]); 4859 assert(lexSelector(r" <</") == ["<<", "/"]); 4860 assert(lexSelector(r" <</*") == ["<<"]); 4861 assert(lexSelector(r" <\</*") == ["<", "<"]); 4862 assert(lexSelector(r"heh\") == ["heh"]); 4863 assert(lexSelector(r"alice \") == ["alice"]); 4864 assert(lexSelector(r"alice,is#best") == ["alice", ",", "is", "#", "best"]); 4865 } 4866 4867 ///. 4868 struct SelectorPart { 4869 string tagNameFilter; ///. 4870 string[] attributesPresent; /// [attr] 4871 string[2][] attributesEqual; /// [attr=value] 4872 string[2][] attributesStartsWith; /// [attr^=value] 4873 string[2][] attributesEndsWith; /// [attr$=value] 4874 // split it on space, then match to these 4875 string[2][] attributesIncludesSeparatedBySpaces; /// [attr~=value] 4876 // split it on dash, then match to these 4877 string[2][] attributesIncludesSeparatedByDashes; /// [attr|=value] 4878 string[2][] attributesInclude; /// [attr*=value] 4879 string[2][] attributesNotEqual; /// [attr!=value] -- extension by me 4880 4881 bool firstChild; ///. 4882 bool lastChild; ///. 4883 4884 bool emptyElement; ///. 4885 bool oddChild; ///. 4886 bool evenChild; ///. 4887 4888 bool rootElement; ///. 4889 4890 int separation = -1; /// -1 == only itself; the null selector, 0 == tree, 1 == childNodes, 2 == childAfter, 3 == youngerSibling, 4 == parentOf 4891 4892 ///. 4893 string toString() { 4894 string ret; 4895 switch(separation) { 4896 default: assert(0); 4897 case -1: break; 4898 case 0: ret ~= " "; break; 4899 case 1: ret ~= ">"; break; 4900 case 2: ret ~= "+"; break; 4901 case 3: ret ~= "~"; break; 4902 case 4: ret ~= "<"; break; 4903 } 4904 ret ~= tagNameFilter; 4905 foreach(a; attributesPresent) ret ~= "[" ~ a ~ "]"; 4906 foreach(a; attributesEqual) ret ~= "[" ~ a[0] ~ "=\"" ~ a[1] ~ "\"]"; 4907 foreach(a; attributesEndsWith) ret ~= "[" ~ a[0] ~ "$=\"" ~ a[1] ~ "\"]"; 4908 foreach(a; attributesStartsWith) ret ~= "[" ~ a[0] ~ "^=\"" ~ a[1] ~ "\"]"; 4909 foreach(a; attributesNotEqual) ret ~= "[" ~ a[0] ~ "!=\"" ~ a[1] ~ "\"]"; 4910 foreach(a; attributesInclude) ret ~= "[" ~ a[0] ~ "*=\"" ~ a[1] ~ "\"]"; 4911 foreach(a; attributesIncludesSeparatedByDashes) ret ~= "[" ~ a[0] ~ "|=\"" ~ a[1] ~ "\"]"; 4912 foreach(a; attributesIncludesSeparatedBySpaces) ret ~= "[" ~ a[0] ~ "~=\"" ~ a[1] ~ "\"]"; 4913 4914 if(firstChild) ret ~= ":first-child"; 4915 if(lastChild) ret ~= ":last-child"; 4916 if(emptyElement) ret ~= ":empty"; 4917 if(oddChild) ret ~= ":odd-child"; 4918 if(evenChild) ret ~= ":even-child"; 4919 if(rootElement) ret ~= ":root"; 4920 4921 return ret; 4922 } 4923 4924 // USEFUL 4925 ///. 4926 bool matchElement(Element e) { 4927 // FIXME: this can be called a lot of times, and really add up in times according to the profiler. 4928 // Each individual call is reasonably fast already, but it adds up. 4929 if(e is null) return false; 4930 if(e.nodeType != 1) return false; 4931 4932 if(tagNameFilter != "" && tagNameFilter != "*") 4933 if(e.tagName != tagNameFilter) 4934 return false; 4935 if(firstChild) { 4936 if(e.parentNode is null) 4937 return false; 4938 if(e.parentNode.childElements[0] !is e) 4939 return false; 4940 } 4941 if(lastChild) { 4942 if(e.parentNode is null) 4943 return false; 4944 auto ce = e.parentNode.childElements; 4945 if(ce[$-1] !is e) 4946 return false; 4947 } 4948 if(emptyElement) { 4949 if(e.children.length) 4950 return false; 4951 } 4952 if(rootElement) { 4953 if(e.parentNode !is null) 4954 return false; 4955 } 4956 if(oddChild || evenChild) { 4957 if(e.parentNode is null) 4958 return false; 4959 foreach(i, child; e.parentNode.childElements) { 4960 if(child is e) { 4961 if(oddChild && !(i&1)) 4962 return false; 4963 if(evenChild && (i&1)) 4964 return false; 4965 break; 4966 } 4967 } 4968 } 4969 4970 bool matchWithSeparator(string attr, string value, string separator) { 4971 foreach(s; attr.split(separator)) 4972 if(s == value) 4973 return true; 4974 return false; 4975 } 4976 4977 foreach(a; attributesPresent) 4978 if(a !in e.attributes) 4979 return false; 4980 foreach(a; attributesEqual) 4981 if(a[0] !in e.attributes || e.attributes[a[0]] != a[1]) 4982 return false; 4983 foreach(a; attributesNotEqual) 4984 // FIXME: maybe it should say null counts... this just bit me. 4985 // I did [attr][attr!=value] to work around. 4986 // 4987 // if it's null, it's not equal, right? 4988 //if(a[0] !in e.attributes || e.attributes[a[0]] == a[1]) 4989 if(e.getAttribute(a[0]) == a[1]) 4990 return false; 4991 foreach(a; attributesInclude) 4992 if(a[0] !in e.attributes || (e.attributes[a[0]].indexOf(a[1]) == -1)) 4993 return false; 4994 foreach(a; attributesStartsWith) 4995 if(a[0] !in e.attributes || !e.attributes[a[0]].startsWith(a[1])) 4996 return false; 4997 foreach(a; attributesEndsWith) 4998 if(a[0] !in e.attributes || !e.attributes[a[0]].endsWith(a[1])) 4999 return false; 5000 foreach(a; attributesIncludesSeparatedBySpaces) 5001 if(a[0] !in e.attributes || !matchWithSeparator(e.attributes[a[0]], a[1], " ")) 5002 return false; 5003 foreach(a; attributesIncludesSeparatedByDashes) 5004 if(a[0] !in e.attributes || !matchWithSeparator(e.attributes[a[0]], a[1], "-")) 5005 return false; 5006 5007 return true; 5008 } 5009 } 5010 5011 // USEFUL 5012 ///. 5013 Element[] getElementsBySelectorParts(Element start, SelectorPart[] parts) { 5014 Element[] ret; 5015 if(!parts.length) { 5016 return [start]; // the null selector only matches the start point; it 5017 // is what terminates the recursion 5018 } 5019 5020 auto part = parts[0]; 5021 switch(part.separation) { 5022 default: assert(0); 5023 case -1: 5024 case 0: // tree 5025 foreach(e; start.tree) { 5026 if(part.separation == 0 && start is e) 5027 continue; // space doesn't match itself! 5028 if(part.matchElement(e)) { 5029 ret ~= getElementsBySelectorParts(e, parts[1..$]); 5030 } 5031 } 5032 break; 5033 case 1: // children 5034 foreach(e; start.childNodes) { 5035 if(part.matchElement(e)) { 5036 ret ~= getElementsBySelectorParts(e, parts[1..$]); 5037 } 5038 } 5039 break; 5040 case 2: // next-sibling 5041 auto tmp = start.parentNode; 5042 if(tmp !is null) { 5043 sizediff_t pos = -1; 5044 auto children = tmp.childElements; 5045 foreach(i, child; children) { 5046 if(child is start) { 5047 pos = i; 5048 break; 5049 } 5050 } 5051 assert(pos != -1); 5052 if(pos + 1 < children.length) { 5053 auto e = children[pos+1]; 5054 if(part.matchElement(e)) 5055 ret ~= getElementsBySelectorParts(e, parts[1..$]); 5056 } 5057 } 5058 break; 5059 case 3: // younger sibling 5060 auto tmp = start.parentNode; 5061 if(tmp !is null) { 5062 sizediff_t pos = -1; 5063 auto children = tmp.childElements; 5064 foreach(i, child; children) { 5065 if(child is start) { 5066 pos = i; 5067 break; 5068 } 5069 } 5070 assert(pos != -1); 5071 foreach(e; children[pos+1..$]) { 5072 if(part.matchElement(e)) 5073 ret ~= getElementsBySelectorParts(e, parts[1..$]); 5074 } 5075 } 5076 break; 5077 case 4: // immediate parent node, an extension of mine to walk back up the tree 5078 auto e = start.parentNode; 5079 if(part.matchElement(e)) { 5080 ret ~= getElementsBySelectorParts(e, parts[1..$]); 5081 } 5082 /* 5083 Example of usefulness: 5084 5085 Consider you have an HTML table. If you want to get all rows that have a th, you can do: 5086 5087 table th < tr 5088 5089 Get all th descendants of the table, then walk back up the tree to fetch their parent tr nodes 5090 */ 5091 break; 5092 case 5: // any parent note, another extension of mine to go up the tree (backward of the whitespace operator) 5093 /* 5094 Like with the < operator, this is best used to find some parent of a particular known element. 5095 5096 Say you have an anchor inside a 5097 */ 5098 } 5099 5100 return ret; 5101 } 5102 5103 ///. 5104 struct Selector { 5105 ///. 5106 SelectorPart[] parts; 5107 5108 ///. 5109 string toString() { 5110 string ret; 5111 foreach(part; parts) 5112 ret ~= part.toString(); 5113 return ret; 5114 } 5115 5116 // USEFUL 5117 ///. 5118 Element[] getElements(Element start) { 5119 return removeDuplicates(getElementsBySelectorParts(start, parts)); 5120 } 5121 5122 // USEFUL (but not implemented) 5123 /// If relativeTo == null, it assumes the root of the parent document. 5124 bool matchElement(Element e, Element relativeTo = null) { 5125 // FIXME 5126 /+ 5127 Element where = e; 5128 foreach(part; retro(parts)) { 5129 if(where is relativeTo) 5130 return false; // at end of line, if we aren't done by now, the match fails 5131 if(!part.matchElement(where)) 5132 return false; // didn't match 5133 5134 if(part.selection == 1) // the > operator 5135 where = where.parentNode; 5136 else if(part.selection == 0) { // generic parent 5137 // need to go up the whole chain 5138 } 5139 } 5140 +/ 5141 return true; // if we got here, it is a success 5142 } 5143 5144 // the string should NOT have commas. Use parseSelectorString for that instead 5145 ///. 5146 static Selector fromString(string selector) { 5147 return parseSelector(lexSelector(selector)); 5148 } 5149 } 5150 5151 ///. 5152 Selector[] parseSelectorString(string selector, bool caseSensitiveTags = true) { 5153 Selector[] ret; 5154 auto tokens = lexSelector(selector); // this will parse commas too 5155 // and now do comma-separated slices (i haz phobosophobia!) 5156 while (tokens.length > 0) { 5157 size_t end = 0; 5158 while (end < tokens.length && tokens[end] != ",") ++end; 5159 if (end > 0) ret ~= parseSelector(tokens[0..end], caseSensitiveTags); 5160 if (tokens.length-end < 2) break; 5161 tokens = tokens[end+1..$]; 5162 } 5163 return ret; 5164 } 5165 5166 ///. 5167 Selector parseSelector(string[] tokens, bool caseSensitiveTags = true) { 5168 Selector s; 5169 5170 SelectorPart current; 5171 void commit() { 5172 // might as well skip null items 5173 if(current != current.init) { 5174 s.parts ~= current; 5175 5176 current = current.init; // start right over 5177 } 5178 } 5179 enum State { 5180 Starting, 5181 ReadingClass, 5182 ReadingId, 5183 ReadingAttributeSelector, 5184 ReadingAttributeComparison, 5185 ExpectingAttributeCloser, 5186 ReadingPseudoClass, 5187 ReadingAttributeValue 5188 } 5189 State state = State.Starting; 5190 string attributeName, attributeValue, attributeComparison; 5191 foreach(token; tokens) { 5192 sizediff_t tid = -1; 5193 foreach(i, item; selectorTokens) 5194 if(token == item) { 5195 tid = i; 5196 break; 5197 } 5198 final switch(state) { 5199 case State.Starting: // fresh, might be reading an operator or a tagname 5200 if(tid == -1) { 5201 if(!caseSensitiveTags) 5202 token = token.toLower(); 5203 if(current.tagNameFilter) { 5204 // if it was already set, we must see two thingies 5205 // separated by whitespace... 5206 commit(); 5207 current.separation = 0; // tree 5208 } 5209 current.tagNameFilter = token; 5210 } else { 5211 // Selector operators 5212 switch(token) { 5213 case "*": 5214 current.tagNameFilter = "*"; 5215 break; 5216 case " ": 5217 commit(); 5218 current.separation = 0; // tree 5219 break; 5220 case ">": 5221 commit(); 5222 current.separation = 1; // child 5223 break; 5224 case "+": 5225 commit(); 5226 current.separation = 2; // sibling directly after 5227 break; 5228 case "~": 5229 commit(); 5230 current.separation = 3; // any sibling after 5231 break; 5232 case "<": 5233 commit(); 5234 current.separation = 4; // immediate parent of 5235 break; 5236 case "[": 5237 state = State.ReadingAttributeSelector; 5238 break; 5239 case ".": 5240 state = State.ReadingClass; 5241 break; 5242 case "#": 5243 state = State.ReadingId; 5244 break; 5245 case ":": 5246 state = State.ReadingPseudoClass; 5247 break; 5248 5249 default: 5250 assert(0, token); 5251 } 5252 } 5253 break; 5254 case State.ReadingClass: 5255 current.attributesIncludesSeparatedBySpaces ~= ["class", token]; 5256 state = State.Starting; 5257 break; 5258 case State.ReadingId: 5259 current.attributesEqual ~= ["id", token]; 5260 state = State.Starting; 5261 break; 5262 case State.ReadingPseudoClass: 5263 switch(token) { 5264 case "first-child": 5265 current.firstChild = true; 5266 break; 5267 case "last-child": 5268 current.lastChild = true; 5269 break; 5270 case "only-child": 5271 current.firstChild = true; 5272 current.lastChild = true; 5273 break; 5274 case "empty": 5275 // one with no children 5276 current.emptyElement = true; 5277 break; 5278 case "link": 5279 current.attributesPresent ~= "href"; 5280 break; 5281 case "root": 5282 current.rootElement = true; 5283 break; 5284 // FIXME: add :not() 5285 // My extensions 5286 case "odd-child": 5287 current.oddChild = true; 5288 break; 5289 case "even-child": 5290 current.evenChild = true; 5291 break; 5292 5293 case "visited", "active", "hover", "target", "focus", "checked", "selected": 5294 current.attributesPresent ~= "nothing"; 5295 // FIXME 5296 /* 5297 // defined in the standard, but I don't implement it 5298 case "not": 5299 */ 5300 /+ 5301 // extensions not implemented 5302 //case "text": // takes the text in the element and wraps it in an element, returning it 5303 +/ 5304 goto case; 5305 case "before", "after": 5306 current.attributesPresent ~= "FIXME"; 5307 5308 break; 5309 default: 5310 //if(token.indexOf("lang") == -1) 5311 //assert(0, token); 5312 break; 5313 } 5314 state = State.Starting; 5315 break; 5316 case State.ReadingAttributeSelector: 5317 attributeName = token; 5318 attributeComparison = null; 5319 attributeValue = null; 5320 state = State.ReadingAttributeComparison; 5321 break; 5322 case State.ReadingAttributeComparison: 5323 // FIXME: these things really should be quotable in the proper lexer... 5324 if(token != "]") { 5325 if(token.indexOf("=") == -1) { 5326 // not a comparison; consider it 5327 // part of the attribute 5328 attributeValue ~= token; 5329 } else { 5330 attributeComparison = token; 5331 state = State.ReadingAttributeValue; 5332 } 5333 break; 5334 } 5335 goto case; 5336 case State.ExpectingAttributeCloser: 5337 if(token != "]") { 5338 // not the closer; consider it part of comparison 5339 if(attributeComparison == "") 5340 attributeName ~= token; 5341 else 5342 attributeValue ~= token; 5343 break; 5344 } 5345 5346 // Selector operators 5347 switch(attributeComparison) { 5348 default: assert(0); 5349 case "": 5350 current.attributesPresent ~= attributeName; 5351 break; 5352 case "=": 5353 current.attributesEqual ~= [attributeName, attributeValue]; 5354 break; 5355 case "|=": 5356 current.attributesIncludesSeparatedByDashes ~= [attributeName, attributeValue]; 5357 break; 5358 case "~=": 5359 current.attributesIncludesSeparatedBySpaces ~= [attributeName, attributeValue]; 5360 break; 5361 case "$=": 5362 current.attributesEndsWith ~= [attributeName, attributeValue]; 5363 break; 5364 case "^=": 5365 current.attributesStartsWith ~= [attributeName, attributeValue]; 5366 break; 5367 case "*=": 5368 current.attributesInclude ~= [attributeName, attributeValue]; 5369 break; 5370 case "!=": 5371 current.attributesNotEqual ~= [attributeName, attributeValue]; 5372 break; 5373 } 5374 5375 state = State.Starting; 5376 break; 5377 case State.ReadingAttributeValue: 5378 attributeValue = token; 5379 state = State.ExpectingAttributeCloser; 5380 break; 5381 } 5382 } 5383 5384 commit(); 5385 5386 return s; 5387 } 5388 5389 ///. 5390 Element[] removeDuplicates(Element[] input) { 5391 Element[] ret; 5392 5393 bool[Element] already; 5394 foreach(e; input) { 5395 if(e in already) continue; 5396 already[e] = true; 5397 ret ~= e; 5398 } 5399 5400 return ret; 5401 } 5402 5403 // done with CSS selector handling 5404 5405 5406 // FIXME: use the better parser from html.d 5407 /// This is probably not useful to you unless you're writing a browser or something like that. 5408 /// It represents a *computed* style, like what the browser gives you after applying stylesheets, inline styles, and html attributes. 5409 /// From here, you can start to make a layout engine for the box model and have a css aware browser. 5410 class CssStyle { 5411 ///. 5412 this(string rule, string content) { 5413 rule = rule.strip(); 5414 content = content.strip(); 5415 5416 if(content.length == 0) 5417 return; 5418 5419 originatingRule = rule; 5420 originatingSpecificity = getSpecificityOfRule(rule); // FIXME: if there's commas, this won't actually work! 5421 5422 foreach(part; content.split(";")) { 5423 part = part.strip(); 5424 if(part.length == 0) 5425 continue; 5426 auto idx = part.indexOf(":"); 5427 if(idx == -1) 5428 continue; 5429 //throw new Exception("Bad css rule (no colon): " ~ part); 5430 5431 Property p; 5432 5433 p.name = part[0 .. idx].strip(); 5434 p.value = part[idx + 1 .. $].replace("! important", "!important").replace("!important", "").strip(); // FIXME don't drop important 5435 p.givenExplicitly = true; 5436 p.specificity = originatingSpecificity; 5437 5438 properties ~= p; 5439 } 5440 5441 foreach(property; properties) 5442 expandShortForm(property, originatingSpecificity); 5443 } 5444 5445 ///. 5446 Specificity getSpecificityOfRule(string rule) { 5447 Specificity s; 5448 if(rule.length == 0) { // inline 5449 // s.important = 2; 5450 } else { 5451 // FIXME 5452 } 5453 5454 return s; 5455 } 5456 5457 string originatingRule; ///. 5458 Specificity originatingSpecificity; ///. 5459 5460 ///. 5461 union Specificity { 5462 uint score; ///. 5463 // version(little_endian) 5464 ///. 5465 struct { 5466 ubyte tags; ///. 5467 ubyte classes; ///. 5468 ubyte ids; ///. 5469 ubyte important; /// 0 = none, 1 = stylesheet author, 2 = inline style, 3 = user important 5470 } 5471 } 5472 5473 ///. 5474 struct Property { 5475 bool givenExplicitly; /// this is false if for example the user said "padding" and this is "padding-left" 5476 string name; ///. 5477 string value; ///. 5478 Specificity specificity; ///. 5479 // do we care about the original source rule? 5480 } 5481 5482 ///. 5483 Property[] properties; 5484 5485 ///. 5486 string opDispatch(string nameGiven)(string value = null) if(nameGiven != "popFront") { 5487 string name = unCamelCase(nameGiven); 5488 if(value is null) 5489 return getValue(name); 5490 else 5491 return setValue(name, value, 0x02000000 /* inline specificity */); 5492 } 5493 5494 /// takes dash style name 5495 string getValue(string name) { 5496 foreach(property; properties) 5497 if(property.name == name) 5498 return property.value; 5499 return null; 5500 } 5501 5502 /// takes dash style name 5503 string setValue(string name, string value, Specificity newSpecificity, bool explicit = true) { 5504 value = value.replace("! important", "!important"); 5505 if(value.indexOf("!important") != -1) { 5506 newSpecificity.important = 1; // FIXME 5507 value = value.replace("!important", "").strip(); 5508 } 5509 5510 foreach(ref property; properties) 5511 if(property.name == name) { 5512 if(newSpecificity.score >= property.specificity.score) { 5513 property.givenExplicitly = explicit; 5514 expandShortForm(property, newSpecificity); 5515 return (property.value = value); 5516 } else { 5517 if(name == "display") 5518 {}//writeln("Not setting ", name, " to ", value, " because ", newSpecificity.score, " < ", property.specificity.score); 5519 return value; // do nothing - the specificity is too low 5520 } 5521 } 5522 5523 // it's not here... 5524 5525 Property p; 5526 p.givenExplicitly = true; 5527 p.name = name; 5528 p.value = value; 5529 p.specificity = originatingSpecificity; 5530 5531 properties ~= p; 5532 expandShortForm(p, originatingSpecificity); 5533 5534 return value; 5535 } 5536 5537 private void expandQuadShort(string name, string value, Specificity specificity) { 5538 auto parts = value.split(" "); 5539 switch(parts.length) { 5540 case 1: 5541 setValue(name ~"-left", parts[0], specificity, false); 5542 setValue(name ~"-right", parts[0], specificity, false); 5543 setValue(name ~"-top", parts[0], specificity, false); 5544 setValue(name ~"-bottom", parts[0], specificity, false); 5545 break; 5546 case 2: 5547 setValue(name ~"-left", parts[1], specificity, false); 5548 setValue(name ~"-right", parts[1], specificity, false); 5549 setValue(name ~"-top", parts[0], specificity, false); 5550 setValue(name ~"-bottom", parts[0], specificity, false); 5551 break; 5552 case 3: 5553 setValue(name ~"-top", parts[0], specificity, false); 5554 setValue(name ~"-right", parts[1], specificity, false); 5555 setValue(name ~"-bottom", parts[2], specificity, false); 5556 setValue(name ~"-left", parts[2], specificity, false); 5557 5558 break; 5559 case 4: 5560 setValue(name ~"-top", parts[0], specificity, false); 5561 setValue(name ~"-right", parts[1], specificity, false); 5562 setValue(name ~"-bottom", parts[2], specificity, false); 5563 setValue(name ~"-left", parts[3], specificity, false); 5564 break; 5565 default: 5566 assert(0, value); 5567 } 5568 } 5569 5570 ///. 5571 void expandShortForm(Property p, Specificity specificity) { 5572 switch(p.name) { 5573 case "margin": 5574 case "padding": 5575 expandQuadShort(p.name, p.value, specificity); 5576 break; 5577 case "border": 5578 case "outline": 5579 setValue(p.name ~ "-left", p.value, specificity, false); 5580 setValue(p.name ~ "-right", p.value, specificity, false); 5581 setValue(p.name ~ "-top", p.value, specificity, false); 5582 setValue(p.name ~ "-bottom", p.value, specificity, false); 5583 break; 5584 5585 case "border-top": 5586 case "border-bottom": 5587 case "border-left": 5588 case "border-right": 5589 case "outline-top": 5590 case "outline-bottom": 5591 case "outline-left": 5592 case "outline-right": 5593 5594 default: {} 5595 } 5596 } 5597 5598 ///. 5599 override string toString() { 5600 string ret; 5601 if(originatingRule.length) 5602 ret = originatingRule ~ " {"; 5603 5604 foreach(property; properties) { 5605 if(!property.givenExplicitly) 5606 continue; // skip the inferred shit 5607 5608 if(originatingRule.length) 5609 ret ~= "\n\t"; 5610 else 5611 ret ~= " "; 5612 5613 ret ~= property.name ~ ": " ~ property.value ~ ";"; 5614 } 5615 5616 if(originatingRule.length) 5617 ret ~= "\n}\n"; 5618 5619 return ret; 5620 } 5621 } 5622 5623 string cssUrl(string url) { 5624 return "url(\"" ~ url ~ "\")"; 5625 } 5626 5627 /// This probably isn't useful, unless you're writing a browser or something like that. 5628 /// You might want to look at arsd.html for css macro, nesting, etc., or just use standard css 5629 /// as text. 5630 /// 5631 /// The idea, however, is to represent a kind of CSS object model, complete with specificity, 5632 /// that you can apply to your documents to build the complete computedStyle object. 5633 class StyleSheet { 5634 ///. 5635 CssStyle[] rules; 5636 5637 ///. 5638 this(string source) { 5639 // FIXME: handle @ rules and probably could improve lexer 5640 // add nesting? 5641 int state; 5642 string currentRule; 5643 string currentValue; 5644 5645 string* currentThing = ¤tRule; 5646 foreach(c; source) { 5647 handle: switch(state) { 5648 default: assert(0); 5649 case 0: // starting - we assume we're reading a rule 5650 switch(c) { 5651 case '@': 5652 state = 4; 5653 break; 5654 case '/': 5655 state = 1; 5656 break; 5657 case '{': 5658 currentThing = ¤tValue; 5659 break; 5660 case '}': 5661 if(currentThing is ¤tValue) { 5662 rules ~= new CssStyle(currentRule, currentValue); 5663 5664 currentRule = ""; 5665 currentValue = ""; 5666 5667 currentThing = ¤tRule; 5668 } else { 5669 // idk what is going on here. 5670 // check sveit.com to reproduce 5671 currentRule = ""; 5672 currentValue = ""; 5673 } 5674 break; 5675 default: 5676 (*currentThing) ~= c; 5677 } 5678 break; 5679 case 1: // expecting * 5680 if(c == '*') 5681 state = 2; 5682 else { 5683 state = 0; 5684 (*currentThing) ~= "/" ~ c; 5685 } 5686 break; 5687 case 2: // inside comment 5688 if(c == '*') 5689 state = 3; 5690 break; 5691 case 3: // expecting / to end comment 5692 if(c == '/') 5693 state = 0; 5694 else 5695 state = 2; // it's just a comment so no need to append 5696 break; 5697 case 4: 5698 if(c == '{') 5699 state = 5; 5700 if(c == ';') 5701 state = 0; // just skipping import 5702 break; 5703 case 5: 5704 if(c == '}') 5705 state = 0; // skipping font face probably 5706 } 5707 } 5708 } 5709 5710 /// Run through the document and apply this stylesheet to it. The computedStyle member will be accurate after this call 5711 void apply(Document document) { 5712 foreach(rule; rules) { 5713 if(rule.originatingRule.length == 0) 5714 continue; // this shouldn't happen here in a stylesheet 5715 foreach(element; document.querySelectorAll(rule.originatingRule)) { 5716 // note: this should be a different object than the inline style 5717 // since givenExplicitly is likely destroyed here 5718 auto current = element.computedStyle; 5719 5720 foreach(item; rule.properties) 5721 current.setValue(item.name, item.value, item.specificity); 5722 } 5723 } 5724 } 5725 } 5726 5727 5728 /// This is kinda private; just a little utility container for use by the ElementStream class. 5729 final class Stack(T) { 5730 this() { 5731 internalLength = 0; 5732 arr = initialBuffer[]; 5733 } 5734 5735 ///. 5736 void push(T t) { 5737 if(internalLength >= arr.length) { 5738 auto oldarr = arr; 5739 if(arr.length < 4096) 5740 arr = new T[arr.length * 2]; 5741 else 5742 arr = new T[arr.length + 4096]; 5743 arr[0 .. oldarr.length] = oldarr[]; 5744 } 5745 5746 arr[internalLength] = t; 5747 internalLength++; 5748 } 5749 5750 ///. 5751 T pop() { 5752 assert(internalLength); 5753 internalLength--; 5754 return arr[internalLength]; 5755 } 5756 5757 ///. 5758 T peek() { 5759 assert(internalLength); 5760 return arr[internalLength - 1]; 5761 } 5762 5763 ///. 5764 @property bool empty() { 5765 return internalLength ? false : true; 5766 } 5767 5768 ///. 5769 private T[] arr; 5770 private size_t internalLength; 5771 private T[64] initialBuffer; 5772 // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), 5773 // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() 5774 // function thanks to this, and push() was actually one of the slowest individual functions in the code! 5775 } 5776 5777 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. 5778 final class ElementStream { 5779 5780 ///. 5781 @property Element front() { 5782 return current.element; 5783 } 5784 5785 /// Use Element.tree instead. 5786 this(Element start) { 5787 current.element = start; 5788 current.childPosition = -1; 5789 isEmpty = false; 5790 stack = new Stack!(Current); 5791 } 5792 5793 /* 5794 Handle it 5795 handle its children 5796 5797 */ 5798 5799 ///. 5800 void popFront() { 5801 more: 5802 if(isEmpty) return; 5803 5804 // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) 5805 5806 current.childPosition++; 5807 if(current.childPosition >= current.element.children.length) { 5808 if(stack.empty()) 5809 isEmpty = true; 5810 else { 5811 current = stack.pop(); 5812 goto more; 5813 } 5814 } else { 5815 stack.push(current); 5816 current.element = current.element.children[current.childPosition]; 5817 current.childPosition = -1; 5818 } 5819 } 5820 5821 /// You should call this when you remove an element from the tree. It then doesn't recurse into that node and adjusts the current position, keeping the range stable. 5822 void currentKilled() { 5823 if(stack.empty) // should never happen 5824 isEmpty = true; 5825 else { 5826 current = stack.pop(); 5827 current.childPosition--; // when it is killed, the parent is brought back a lil so when we popFront, this is then right 5828 } 5829 } 5830 5831 ///. 5832 @property bool empty() { 5833 return isEmpty; 5834 } 5835 5836 private: 5837 5838 struct Current { 5839 Element element; 5840 int childPosition; 5841 } 5842 5843 Current current; 5844 5845 Stack!(Current) stack; 5846 5847 bool isEmpty; 5848 } 5849 5850 5851 5852 // unbelievable. 5853 // Don't use any of these in your own code. Instead, try to use phobos or roll your own, as I might kill these at any time. 5854 sizediff_t indexOfBytes(immutable(ubyte)[] haystack, immutable(ubyte)[] needle) { 5855 static import std.algorithm; 5856 auto found = std.algorithm.find(haystack, needle); 5857 if(found.length == 0) 5858 return -1; 5859 return haystack.length - found.length; 5860 } 5861 5862 private T[] insertAfter(T)(T[] arr, int position, T[] what) { 5863 assert(position < arr.length); 5864 T[] ret; 5865 ret.length = arr.length + what.length; 5866 int a = 0; 5867 foreach(i; arr[0..position+1]) 5868 ret[a++] = i; 5869 5870 foreach(i; what) 5871 ret[a++] = i; 5872 5873 foreach(i; arr[position+1..$]) 5874 ret[a++] = i; 5875 5876 return ret; 5877 } 5878 5879 package bool isInArray(T)(T item, T[] arr) { 5880 foreach(i; arr) 5881 if(item == i) 5882 return true; 5883 return false; 5884 } 5885 5886 private string[string] aadup(in string[string] arr) { 5887 string[string] ret; 5888 foreach(k, v; arr) 5889 ret[k] = v; 5890 return ret; 5891 } 5892 5893 // dom event support, if you want to use it 5894 5895 /// used for DOM events 5896 alias void delegate(Element handlerAttachedTo, Event event) EventHandler; 5897 5898 /// This is a DOM event, like in javascript. Note that this library never fires events - it is only here for you to use if you want it. 5899 class Event { 5900 this(string eventName, Element target) { 5901 this.eventName = eventName; 5902 this.srcElement = target; 5903 } 5904 5905 /// Prevents the default event handler (if there is one) from being called 5906 void preventDefault() { 5907 defaultPrevented = true; 5908 } 5909 5910 /// Stops the event propagation immediately. 5911 void stopPropagation() { 5912 propagationStopped = true; 5913 } 5914 5915 bool defaultPrevented; 5916 bool propagationStopped; 5917 string eventName; 5918 5919 Element srcElement; 5920 alias srcElement target; 5921 5922 Element relatedTarget; 5923 5924 int clientX; 5925 int clientY; 5926 5927 int button; 5928 5929 bool isBubbling; 5930 5931 /// this sends it only to the target. If you want propagation, use dispatch() instead. 5932 void send() { 5933 if(srcElement is null) 5934 return; 5935 5936 auto e = srcElement; 5937 5938 if(eventName in e.bubblingEventHandlers) 5939 foreach(handler; e.bubblingEventHandlers[eventName]) 5940 handler(e, this); 5941 5942 if(!defaultPrevented) 5943 if(eventName in e.defaultEventHandlers) 5944 e.defaultEventHandlers[eventName](e, this); 5945 } 5946 5947 /// this dispatches the element using the capture -> target -> bubble process 5948 void dispatch() { 5949 if(srcElement is null) 5950 return; 5951 5952 // first capture, then bubble 5953 5954 Element[] chain; 5955 Element curr = srcElement; 5956 while(curr) { 5957 auto l = curr; 5958 chain ~= l; 5959 curr = curr.parentNode; 5960 5961 } 5962 5963 isBubbling = false; 5964 5965 foreach(e; chain.retro()) { 5966 if(eventName in e.capturingEventHandlers) 5967 foreach(handler; e.capturingEventHandlers[eventName]) 5968 handler(e, this); 5969 5970 // the default on capture should really be to always do nothing 5971 5972 //if(!defaultPrevented) 5973 // if(eventName in e.defaultEventHandlers) 5974 // e.defaultEventHandlers[eventName](e.element, this); 5975 5976 if(propagationStopped) 5977 break; 5978 } 5979 5980 isBubbling = true; 5981 if(!propagationStopped) 5982 foreach(e; chain) { 5983 if(eventName in e.bubblingEventHandlers) 5984 foreach(handler; e.bubblingEventHandlers[eventName]) 5985 handler(e, this); 5986 5987 if(propagationStopped) 5988 break; 5989 } 5990 5991 if(!defaultPrevented) 5992 foreach(e; chain) { 5993 if(eventName in e.defaultEventHandlers) 5994 e.defaultEventHandlers[eventName](e, this); 5995 } 5996 } 5997 } 5998 5999 struct FormFieldOptions { 6000 // usable for any 6001 6002 /// this is a regex pattern used to validate the field 6003 string pattern; 6004 /// must the field be filled in? Even with a regex, it can be submitted blank if this is false. 6005 bool isRequired; 6006 /// this is displayed as an example to the user 6007 string placeholder; 6008 6009 // usable for numeric ones 6010 6011 6012 // convenience methods to quickly get some options 6013 @property static FormFieldOptions none() { 6014 FormFieldOptions f; 6015 return f; 6016 } 6017 6018 static FormFieldOptions required() { 6019 FormFieldOptions f; 6020 f.isRequired = true; 6021 return f; 6022 } 6023 6024 static FormFieldOptions regex(string pattern, bool required = false) { 6025 FormFieldOptions f; 6026 f.pattern = pattern; 6027 f.isRequired = required; 6028 return f; 6029 } 6030 6031 static FormFieldOptions fromElement(Element e) { 6032 FormFieldOptions f; 6033 if(e.hasAttribute("required")) 6034 f.isRequired = true; 6035 if(e.hasAttribute("pattern")) 6036 f.pattern = e.pattern; 6037 if(e.hasAttribute("placeholder")) 6038 f.placeholder = e.placeholder; 6039 return f; 6040 } 6041 6042 Element applyToElement(Element e) { 6043 if(this.isRequired) 6044 e.required = "required"; 6045 if(this.pattern.length) 6046 e.pattern = this.pattern; 6047 if(this.placeholder.length) 6048 e.placeholder = this.placeholder; 6049 return e; 6050 } 6051 } 6052 6053 // this needs to look just like a string, but can expand as needed 6054 version(no_dom_stream) 6055 alias string Utf8Stream; 6056 else 6057 class Utf8Stream { 6058 protected: 6059 // these two should be overridden in subclasses to actually do the stream magic 6060 string getMore() { 6061 if(getMoreHelper !is null) 6062 return getMoreHelper(); 6063 return null; 6064 } 6065 6066 bool hasMore() { 6067 if(hasMoreHelper !is null) 6068 return hasMoreHelper(); 6069 return false; 6070 } 6071 // the rest should be ok 6072 6073 public: 6074 this(string d) { 6075 this.data = d; 6076 } 6077 6078 this(string delegate() getMoreHelper, bool delegate() hasMoreHelper) { 6079 this.getMoreHelper = getMoreHelper; 6080 this.hasMoreHelper = hasMoreHelper; 6081 6082 if(hasMore()) 6083 this.data ~= getMore(); 6084 6085 stdout.flush(); 6086 } 6087 6088 @property final size_t length() { 6089 // the parser checks length primarily directly before accessing the next character 6090 // so this is the place we'll hook to append more if possible and needed. 6091 if(lastIdx + 1 >= data.length && hasMore()) { 6092 data ~= getMore(); 6093 } 6094 return data.length; 6095 } 6096 6097 final char opIndex(size_t idx) { 6098 if(idx > lastIdx) 6099 lastIdx = idx; 6100 return data[idx]; 6101 } 6102 6103 final string opSlice(size_t start, size_t end) { 6104 if(end > lastIdx) 6105 lastIdx = end; 6106 return data[start .. end]; 6107 } 6108 6109 final size_t opDollar() { 6110 return length(); 6111 } 6112 6113 final Utf8Stream opBinary(string op : "~")(string s) { 6114 this.data ~= s; 6115 return this; 6116 } 6117 6118 final Utf8Stream opOpAssign(string op : "~")(string s) { 6119 this.data ~= s; 6120 return this; 6121 } 6122 6123 final Utf8Stream opAssign(string rhs) { 6124 this.data = rhs; 6125 return this; 6126 } 6127 private: 6128 string data; 6129 6130 size_t lastIdx; 6131 6132 bool delegate() hasMoreHelper; 6133 string delegate() getMoreHelper; 6134 6135 6136 /+ 6137 // used to maybe clear some old stuff 6138 // you might have to remove elements parsed with it too since they can hold slices into the 6139 // old stuff, preventing gc 6140 void dropFront(int bytes) { 6141 posAdjustment += bytes; 6142 data = data[bytes .. $]; 6143 } 6144 6145 int posAdjustment; 6146 +/ 6147 } 6148 6149 void fillForm(T)(Form form, T obj, string name) { 6150 import arsd.database; 6151 fillData((k, v) => form.setValue(k, v), obj, name); 6152 } 6153 6154 /* 6155 Copyright: Adam D. Ruppe, 2010 - 2013 6156 License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>. 6157 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky and Trass3r among others 6158 6159 Copyright Adam D. Ruppe 2010-2013. 6160 Distributed under the Boost Software License, Version 1.0. 6161 (See accompanying file LICENSE_1_0.txt or copy at 6162 http://www.boost.org/LICENSE_1_0.txt) 6163 */ 6164