1 /** 2 This module includes functions to work with HTML and CSS in a more specialized manner than [arsd.dom]. Most of this is obsolete from my really old D web stuff, but there's still some useful stuff. View source before you decide to use it, as the implementations may suck more than you want to use. 3 4 It publically imports the DOM module to get started. 5 Then it adds a number of functions to enhance html 6 DOM documents and make other changes, like scripts 7 and stylesheets. 8 */ 9 module arsd.html; 10 11 public import arsd.dom; 12 import arsd.color; 13 14 static import std.uri; 15 import std.array; 16 import std.string; 17 import std.variant; 18 import core.vararg; 19 import std.exception; 20 21 22 /// This is a list of features you can allow when using the sanitizedHtml function. 23 enum HtmlFeatures : uint { 24 images = 1, /// The <img> tag 25 links = 2, /// <a href=""> tags 26 css = 4, /// Inline CSS 27 cssLinkedResources = 8, // FIXME: implement this 28 video = 16, /// The html5 <video> tag. autoplay is always stripped out. 29 audio = 32, /// The html5 <audio> tag. autoplay is always stripped out. 30 objects = 64, /// The <object> tag, which can link to many things, including Flash. 31 iframes = 128, /// The <iframe> tag. sandbox and restrict attributes are always added. 32 classes = 256, /// The class="" attribute 33 forms = 512, /// HTML forms 34 } 35 36 /// The things to allow in links, images, css, and aother urls. 37 /// FIXME: implement this for better flexibility 38 enum UriFeatures : uint { 39 http, /// http:// protocol absolute links 40 https, /// https:// protocol absolute links 41 data, /// data: url links to embed content. On some browsers (old Firefoxes) this was a security concern. 42 ftp, /// ftp:// protocol links 43 relative, /// relative links to the current location. You might want to rebase them. 44 anchors /// #anchor links 45 } 46 47 string[] htmlTagWhitelist = [ 48 "span", "div", 49 "p", "br", 50 "b", "i", "u", "s", "big", "small", "sub", "sup", "strong", "em", "tt", "blockquote", "cite", "ins", "del", "strike", 51 "ol", "ul", "li", "dl", "dt", "dd", 52 "q", 53 "table", "caption", "tr", "td", "th", "col", "thead", "tbody", "tfoot", 54 "hr", 55 "h1", "h2", "h3", "h4", "h5", "h6", 56 "abbr", 57 58 "img", "object", "audio", "video", "a", "source", // note that these usually *are* stripped out - see HtmlFeatures- but this lets them get into stage 2 59 60 "form", "input", "textarea", "legend", "fieldset", "label", // ditto, but with HtmlFeatures.forms 61 // style isn't here 62 ]; 63 64 string[] htmlAttributeWhitelist = [ 65 // style isn't here 66 /* 67 if style, expression must be killed 68 all urls must be checked for javascript and/or vbscript 69 imports must be killed 70 */ 71 "style", 72 73 "colspan", "rowspan", 74 "title", "alt", "class", 75 76 "href", "src", "type", "name", 77 "id", 78 "method", "enctype", "value", "type", // for forms only FIXME 79 80 "align", "valign", "width", "height", 81 ]; 82 83 /// This returns an element wrapping sanitized content, using a whitelist for html tags and attributes, 84 /// and a blacklist for css. Javascript is never allowed. 85 /// 86 /// It scans all URLs it allows and rejects 87 /// 88 /// You can tweak the allowed features with the HtmlFeatures enum. 89 /// 90 /// Note: you might want to use innerText for most user content. This is meant if you want to 91 /// give them a big section of rich text. 92 /// 93 /// userContent should just be a basic div, holding the user's actual content. 94 /// 95 /// FIXME: finish writing this 96 Element sanitizedHtml(/*in*/ Element userContent, string idPrefix = null, HtmlFeatures allow = HtmlFeatures.links | HtmlFeatures.images | HtmlFeatures.css) { 97 auto div = Element.make("div"); 98 div.addClass("sanitized user-content"); 99 100 auto content = div.appendChild(userContent.cloned); 101 startOver: 102 foreach(e; content.tree) { 103 if(e.nodeType == NodeType.Text) 104 continue; // text nodes are always fine. 105 106 e.tagName = e.tagName.toLower(); // normalize tag names... 107 108 if(!(e.tagName.isInArray(htmlTagWhitelist))) { 109 e.stripOut; 110 goto startOver; 111 } 112 113 if((!(allow & HtmlFeatures.links) && e.tagName == "a")) { 114 e.stripOut; 115 goto startOver; 116 } 117 118 if((!(allow & HtmlFeatures.video) && e.tagName == "video") 119 ||(!(allow & HtmlFeatures.audio) && e.tagName == "audio") 120 ||(!(allow & HtmlFeatures.objects) && e.tagName == "object") 121 ||(!(allow & HtmlFeatures.iframes) && e.tagName == "iframe") 122 ||(!(allow & HtmlFeatures.forms) && ( 123 e.tagName == "form" || 124 e.tagName == "input" || 125 e.tagName == "textarea" || 126 e.tagName == "label" || 127 e.tagName == "fieldset" || 128 e.tagName == "legend" 129 )) 130 ) { 131 e.innerText = e.innerText; // strips out non-text children 132 e.stripOut; 133 goto startOver; 134 } 135 136 if(e.tagName == "source" && (e.parentNode is null || e.parentNode.tagName != "video" || e.parentNode.tagName != "audio")) { 137 // source is only allowed in the HTML5 media elements 138 e.stripOut; 139 goto startOver; 140 } 141 142 if(!(allow & HtmlFeatures.images) && e.tagName == "img") { 143 e.replaceWith(new TextNode(null, e.alt)); 144 continue; // images not allowed are replaced with their alt text 145 } 146 147 foreach(k, v; e.attributes) { 148 e.removeAttribute(k); 149 k = k.toLower(); 150 if(!(k.isInArray(htmlAttributeWhitelist))) { 151 // not allowed, don't put it back 152 // this space is intentionally left blank 153 } else { 154 // it's allowed but let's make sure it's completely valid 155 if(k == "class" && (allow & HtmlFeatures.classes)) { 156 e.setAttribute("class", v); 157 } else if(k == "id") { 158 if(idPrefix !is null) 159 e.setAttribute(k, idPrefix ~ v); 160 // otherwise, don't allow user IDs 161 } else if(k == "style") { 162 if(allow & HtmlFeatures.css) { 163 e.setAttribute(k, sanitizeCss(v)); 164 } 165 } else if(k == "href" || k == "src") { 166 e.setAttribute(k, sanitizeUrl(v)); 167 } else 168 e.setAttribute(k, v); // allowed attribute 169 } 170 } 171 172 if(e.tagName == "iframe") { 173 // some additional restrictions for supported browsers 174 e.attrs.security = "restricted"; 175 e.attrs.sandbox = ""; 176 } 177 } 178 return div; 179 } 180 181 /// 182 Element sanitizedHtml(in Html userContent, string idPrefix = null, HtmlFeatures allow = HtmlFeatures.links | HtmlFeatures.images | HtmlFeatures.css) { 183 auto div = Element.make("div"); 184 div.innerHTML = userContent.source; 185 return sanitizedHtml(div, idPrefix, allow); 186 } 187 188 string sanitizeCss(string css) { 189 // FIXME: do a proper whitelist here; I should probably bring in the parser from html.d 190 // FIXME: sanitize urls inside too 191 return css.replace("expression", ""); 192 } 193 194 /// 195 string sanitizeUrl(string url) { 196 // FIXME: support other options; this is more restrictive than it has to be 197 if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) 198 return url; 199 return null; 200 } 201 202 /// This is some basic CSS I suggest you copy/paste into your stylesheet 203 /// if you use the sanitizedHtml function. 204 string recommendedBasicCssForUserContent = ` 205 .sanitized.user-content { 206 position: relative; 207 overflow: hidden; 208 } 209 210 .sanitized.user-content * { 211 max-width: 100%; 212 max-height: 100%; 213 } 214 `; 215 216 /++ 217 Given arbitrary user input, find links and add `<a href>` wrappers, otherwise just escaping the rest of it for HTML display. 218 +/ 219 Html linkify(string text) { 220 auto div = Element.make("div"); 221 222 while(text.length) { 223 auto idx = text.indexOf("http"); 224 if(idx == -1) { 225 idx = text.length; 226 } 227 228 div.appendText(text[0 .. idx]); 229 text = text[idx .. $]; 230 231 if(text.length) { 232 // where does it end? whitespace I guess 233 auto idxSpace = text.indexOf(" "); 234 if(idxSpace == -1) idxSpace = text.length; 235 auto idxLine = text.indexOf("\n"); 236 if(idxLine == -1) idxLine = text.length; 237 238 239 auto idxEnd = idxSpace < idxLine ? idxSpace : idxLine; 240 241 auto link = text[0 .. idxEnd]; 242 text = text[idxEnd .. $]; 243 244 div.addChild("a", link, link); 245 } 246 } 247 248 return Html(div.innerHTML); 249 } 250 251 /// Given existing encoded HTML, turns \n\n into `<p>`. 252 Html paragraphsToP(Html html) { 253 auto text = html.source; 254 string total; 255 foreach(p; text.split("\n\n")) { 256 total ~= "<p>"; 257 auto lines = p.splitLines; 258 foreach(idx, line; lines) 259 if(line.strip.length) { 260 total ~= line; 261 if(idx != lines.length - 1) 262 total ~= "<br />"; 263 } 264 total ~= "</p>"; 265 } 266 return Html(total); 267 } 268 269 /// Given user text, converts newlines to `<br>` and encodes the rest. 270 Html nl2br(string text) { 271 auto div = Element.make("div"); 272 273 bool first = true; 274 foreach(line; splitLines(text)) { 275 if(!first) 276 div.addChild("br"); 277 else 278 first = false; 279 div.appendText(line); 280 } 281 282 return Html(div.innerHTML); 283 } 284 285 /// Returns true of the string appears to be html/xml - if it matches the pattern 286 /// for tags or entities. 287 bool appearsToBeHtml(string src) { 288 import std.regex; 289 return cast(bool) match(src, `.*\<[A-Za-z]+>.*`); 290 } 291 292 /// Get the favicon out of a document, or return the default a browser would attempt if it isn't there. 293 string favicon(Document document) { 294 auto item = document.querySelector("link[rel~=icon]"); 295 if(item !is null) 296 return item.href; 297 return "/favicon.ico"; // it pisses me off that the fucking browsers do this.... but they do, so I will too. 298 } 299 300 /// 301 Element checkbox(string name, string value, string label, bool checked = false) { 302 auto lbl = Element.make("label"); 303 auto input = lbl.addChild("input"); 304 input.type = "checkbox"; 305 input.name = name; 306 input.value = value; 307 if(checked) 308 input.checked = "checked"; 309 310 lbl.appendText(" "); 311 lbl.addChild("span", label); 312 313 return lbl; 314 } 315 316 /++ Convenience function to create a small <form> to POST, but the creation function is more like a link 317 than a DOM form. 318 319 The idea is if you have a link to a page which needs to be changed since it is now taking an action, 320 this should provide an easy way to do it. 321 322 You might want to style these with css. The form these functions create has no class - use regular 323 dom functions to add one. When styling, hit the form itself and form > [type=submit]. (That will 324 cover both input[type=submit] and button[type=submit] - the two possibilities the functions may create.) 325 326 Param: 327 href: the link. Query params (if present) are converted into hidden form inputs and the rest is used as the form action 328 innerText: the text to show on the submit button 329 params: additional parameters for the form 330 +/ 331 Form makePostLink(string href, string innerText, string[string] params = null) { 332 auto submit = Element.make("input"); 333 submit.type = "submit"; 334 submit.value = innerText; 335 336 return makePostLink_impl(href, params, submit); 337 } 338 339 /// Similar to the above, but lets you pass HTML rather than just text. It puts the html inside a <button type="submit"> element. 340 /// 341 /// Using html strings imo generally sucks. I recommend you use plain text or structured Elements instead most the time. 342 Form makePostLink(string href, Html innerHtml, string[string] params = null) { 343 auto submit = Element.make("button"); 344 submit.type = "submit"; 345 submit.innerHTML = innerHtml; 346 347 return makePostLink_impl(href, params, submit); 348 } 349 350 /// Like the Html overload, this uses a <button> tag to get fancier with the submit button. The element you pass is appended to the submit button. 351 Form makePostLink(string href, Element submitButtonContents, string[string] params = null) { 352 auto submit = Element.make("button"); 353 submit.type = "submit"; 354 submit.appendChild(submitButtonContents); 355 356 return makePostLink_impl(href, params, submit); 357 } 358 359 import arsd.cgi; 360 import std.range; 361 362 Form makePostLink_impl(string href, string[string] params, Element submitButton) { 363 auto form = require!Form(Element.make("form")); 364 form.method = "POST"; 365 366 auto idx = href.indexOf("?"); 367 if(idx == -1) { 368 form.action = href; 369 } else { 370 form.action = href[0 .. idx]; 371 foreach(k, arr; decodeVariables(href[idx + 1 .. $])) 372 form.addValueArray(k, arr); 373 } 374 375 foreach(k, v; params) 376 form.setValue(k, v); 377 378 form.appendChild(submitButton); 379 380 return form; 381 } 382 383 /++ Given an existing link, create a POST item from it. 384 You can use this to do something like: 385 386 auto e = document.requireSelector("a.should-be-post"); // get my link from the dom 387 e.replaceWith(makePostLink(e)); // replace the link with a nice POST form that otherwise does the same thing 388 389 It passes all attributes of the link on to the form, though I could be convinced to put some on the submit button instead. 390 ++/ 391 Form makePostLink(Element link) { 392 Form form; 393 if(link.childNodes.length == 1) { 394 auto fc = link.firstChild; 395 if(fc.nodeType == NodeType.Text) 396 form = makePostLink(link.href, fc.nodeValue); 397 else 398 form = makePostLink(link.href, fc); 399 } else { 400 form = makePostLink(link.href, Html(link.innerHTML)); 401 } 402 403 assert(form !is null); 404 405 // auto submitButton = form.requireSelector("[type=submit]"); 406 407 foreach(k, v; link.attributes) { 408 if(k == "href" || k == "action" || k == "method") 409 continue; 410 411 form.setAttribute(k, v); // carries on class, events, etc. to the form. 412 } 413 414 return form; 415 } 416 417 /// Translates validate="" tags to inline javascript. "this" is the thing 418 /// being checked. 419 void translateValidation(Document document) { 420 int count; 421 foreach(f; document.getElementsByTagName("form")) { 422 count++; 423 string formValidation = ""; 424 string fid = f.getAttribute("id"); 425 if(fid is null) { 426 fid = "automatic-form-" ~ to!string(count); 427 f.setAttribute("id", "automatic-form-" ~ to!string(count)); 428 } 429 foreach(i; f.tree) { 430 if(i.tagName != "input" && i.tagName != "select") 431 continue; 432 if(i.getAttribute("id") is null) 433 i.id = "form-input-" ~ i.name; 434 auto validate = i.getAttribute("validate"); 435 if(validate is null) 436 continue; 437 438 auto valmsg = i.getAttribute("validate-message"); 439 if(valmsg !is null) { 440 i.removeAttribute("validate-message"); 441 valmsg ~= `\n`; 442 } 443 444 string valThis = ` 445 var currentField = elements['`~i.name~`']; 446 if(!(`~validate.replace("this", "currentField")~`)) { 447 currentField.style.backgroundColor = '#ffcccc'; 448 if(typeof failedMessage != 'undefined') 449 failedMessage += '`~valmsg~`'; 450 if(failed == null) { 451 failed = currentField; 452 } 453 if('`~valmsg~`' != '') { 454 var msgId = '`~i.name~`-valmsg'; 455 var msgHolder = document.getElementById(msgId); 456 if(!msgHolder) { 457 msgHolder = document.createElement('div'); 458 msgHolder.className = 'validation-message'; 459 msgHolder.id = msgId; 460 461 msgHolder.innerHTML = '<br />'; 462 msgHolder.appendChild(document.createTextNode('`~valmsg~`')); 463 464 var ele = currentField; 465 ele.parentNode.appendChild(msgHolder); 466 } 467 } 468 } else { 469 currentField.style.backgroundColor = '#ffffff'; 470 var msgId = '`~i.name~`-valmsg'; 471 var msgHolder = document.getElementById(msgId); 472 if(msgHolder) 473 msgHolder.innerHTML = ''; 474 }`; 475 476 formValidation ~= valThis; 477 478 string oldOnBlur = i.getAttribute("onblur"); 479 i.setAttribute("onblur", ` 480 var form = document.getElementById('`~fid~`'); 481 var failed = null; 482 with(form) { `~valThis~` } 483 ` ~ oldOnBlur); 484 485 i.removeAttribute("validate"); 486 } 487 488 if(formValidation != "") { 489 auto os = f.getAttribute("onsubmit"); 490 f.attrs.onsubmit = `var failed = null; var failedMessage = ''; with(this) { ` ~ formValidation ~ '\n' ~ ` if(failed != null) { alert('Please complete all required fields.\n' + failedMessage); failed.focus(); return false; } `~os~` return true; }`; 491 } 492 } 493 } 494 495 /// makes input[type=date] to call displayDatePicker with a button 496 void translateDateInputs(Document document) { 497 foreach(e; document.getElementsByTagName("input")) { 498 auto type = e.getAttribute("type"); 499 if(type is null) continue; 500 if(type == "date") { 501 auto name = e.getAttribute("name"); 502 assert(name !is null); 503 auto button = document.createElement("button"); 504 button.type = "button"; 505 button.attrs.onclick = "displayDatePicker('"~name~"');"; 506 button.innerText = "Choose..."; 507 e.parentNode.insertChildAfter(button, e); 508 509 e.type = "text"; 510 e.setAttribute("class", "date"); 511 } 512 } 513 } 514 515 /// finds class="striped" and adds class="odd"/class="even" to the relevant 516 /// children 517 void translateStriping(Document document) { 518 foreach(item; document.querySelectorAll(".striped")) { 519 bool odd = false; 520 string selector; 521 switch(item.tagName) { 522 case "ul": 523 case "ol": 524 selector = "> li"; 525 break; 526 case "table": 527 selector = "> tbody > tr"; 528 break; 529 case "tbody": 530 selector = "> tr"; 531 break; 532 default: 533 selector = "> *"; 534 } 535 foreach(e; item.getElementsBySelector(selector)) { 536 if(odd) 537 e.addClass("odd"); 538 else 539 e.addClass("even"); 540 541 odd = !odd; 542 } 543 } 544 } 545 546 /// tries to make an input to filter a list. it kinda sucks. 547 void translateFiltering(Document document) { 548 foreach(e; document.querySelectorAll("input[filter_what]")) { 549 auto filterWhat = e.attrs.filter_what; 550 if(filterWhat[0] == '#') 551 filterWhat = filterWhat[1..$]; 552 553 auto fw = document.getElementById(filterWhat); 554 assert(fw !is null); 555 556 foreach(a; fw.getElementsBySelector(e.attrs.filter_by)) { 557 a.addClass("filterable_content"); 558 } 559 560 e.removeAttribute("filter_what"); 561 e.removeAttribute("filter_by"); 562 563 e.attrs.onkeydown = e.attrs.onkeyup = ` 564 var value = this.value; 565 var a = document.getElementById("`~filterWhat~`"); 566 var children = a.childNodes; 567 for(var b = 0; b < children.length; b++) { 568 var child = children[b]; 569 if(child.nodeType != 1) 570 continue; 571 572 var spans = child.getElementsByTagName('span'); // FIXME 573 for(var i = 0; i < spans.length; i++) { 574 var span = spans[i]; 575 if(hasClass(span, "filterable_content")) { 576 if(value.length && span.innerHTML.match(RegExp(value, "i"))) { // FIXME 577 addClass(child, "good-match"); 578 removeClass(child, "bad-match"); 579 //if(!got) { 580 // holder.scrollTop = child.offsetTop; 581 // got = true; 582 //} 583 } else { 584 removeClass(child, "good-match"); 585 if(value.length) 586 addClass(child, "bad-match"); 587 else 588 removeClass(child, "bad-match"); 589 } 590 } 591 } 592 } 593 `; 594 } 595 } 596 597 enum TextWrapperWhitespaceBehavior { 598 wrap, 599 ignore, 600 stripOut 601 } 602 603 /// This wraps every non-empty text mode in the document body with 604 /// <t:t></t:t>, and sets an xmlns:t to the html root. 605 /// 606 /// If you use it, be sure it's the last thing you do before 607 /// calling toString 608 /// 609 /// Why would you want this? Because CSS sucks. If it had a 610 /// :text pseudoclass, we'd be right in business, but it doesn't 611 /// so we'll hack it with this custom tag. 612 /// 613 /// It's in an xml namespace so it should affect or be affected by 614 /// your existing code, while maintaining excellent browser support. 615 /// 616 /// To style it, use myelement > t\:t { style here } in your css. 617 /// 618 /// Note: this can break the css adjacent sibling selector, first-child, 619 /// and other structural selectors. For example, if you write 620 /// <span>hello</span> <span>world</span>, normally, css span + span would 621 /// select "world". But, if you call wrapTextNodes, there's a <t:t> in the 622 /// middle.... so now it no longer matches. 623 /// 624 /// Of course, it can also have an effect on your javascript, especially, 625 /// again, when working with siblings or firstChild, etc. 626 /// 627 /// You must handle all this yourself, which may limit the usefulness of this 628 /// function. 629 /// 630 /// The second parameter, whatToDoWithWhitespaceNodes, tries to mitigate 631 /// this somewhat by giving you some options about what to do with text 632 /// nodes that consist of nothing but whitespace. 633 /// 634 /// You can: wrap them, like all other text nodes, you can ignore 635 /// them entirely, leaving them unwrapped, and in the document normally, 636 /// or you can use stripOut to remove them from the document. 637 /// 638 /// Beware with stripOut: <span>you</span> <span>rock</span> -- that space 639 /// between the spans is a text node of nothing but whitespace, so it would 640 /// be stripped out - probably not what you want! 641 /// 642 /// ignore is the default, since this should break the least of your 643 /// expectations with document structure, while still letting you use this 644 /// function. 645 void wrapTextNodes(Document document, TextWrapperWhitespaceBehavior whatToDoWithWhitespaceNodes = TextWrapperWhitespaceBehavior.ignore) { 646 enum ourNamespace = "t"; 647 enum ourTag = ourNamespace ~ ":t"; 648 document.root.setAttribute("xmlns:" ~ ourNamespace, null); 649 foreach(e; document.mainBody.tree) { 650 if(e.tagName == "script") 651 continue; 652 if(e.nodeType != NodeType.Text) 653 continue; 654 auto tn = cast(TextNode) e; 655 if(tn is null) 656 continue; 657 658 if(tn.contents.length == 0) 659 continue; 660 661 if(tn.parentNode !is null 662 && tn.parentNode.tagName == ourTag) 663 { 664 // this is just a sanity check to make sure 665 // we don't double wrap anything 666 continue; 667 } 668 669 final switch(whatToDoWithWhitespaceNodes) { 670 case TextWrapperWhitespaceBehavior.wrap: 671 break; // treat it like all other text 672 case TextWrapperWhitespaceBehavior.stripOut: 673 // if it's actually whitespace... 674 if(tn.contents.strip().length == 0) { 675 tn.removeFromTree(); 676 continue; 677 } 678 break; 679 case TextWrapperWhitespaceBehavior.ignore: 680 // if it's actually whitespace... 681 if(tn.contents.strip().length == 0) 682 continue; 683 } 684 685 tn.replaceWith(Element.make(ourTag, tn.contents)); 686 } 687 } 688 689 690 void translateInputTitles(Document document) { 691 translateInputTitles(document.root); 692 } 693 694 /// find <input> elements with a title. Make the title the default internal content 695 void translateInputTitles(Element rootElement) { 696 foreach(form; rootElement.getElementsByTagName("form")) { 697 string os; 698 foreach(e; form.getElementsBySelector("input[type=text][title], input[type=email][title], textarea[title]")) { 699 if(e.hasClass("has-placeholder")) 700 continue; 701 e.addClass("has-placeholder"); 702 e.attrs.onfocus = e.attrs.onfocus ~ ` 703 removeClass(this, 'default'); 704 if(this.value == this.getAttribute('title')) 705 this.value = ''; 706 `; 707 708 e.attrs.onblur = e.attrs.onblur ~ ` 709 if(this.value == '') { 710 addClass(this, 'default'); 711 this.value = this.getAttribute('title'); 712 } 713 `; 714 715 os ~= ` 716 temporaryItem = this.elements["`~e.name~`"]; 717 if(temporaryItem.value == temporaryItem.getAttribute('title')) 718 temporaryItem.value = ''; 719 `; 720 721 if(e.tagName == "input") { 722 if(e.value == "") { 723 e.attrs.value = e.attrs.title; 724 e.addClass("default"); 725 } 726 } else { 727 if(e.innerText.length == 0) { 728 e.innerText = e.attrs.title; 729 e.addClass("default"); 730 } 731 } 732 } 733 734 form.attrs.onsubmit = os ~ form.attrs.onsubmit; 735 } 736 } 737 738 739 /// Adds some script to run onload 740 /// FIXME: not implemented 741 void addOnLoad(Document document) { 742 743 } 744 745 746 747 748 749 750 mixin template opDispatches(R) { 751 auto opDispatch(string fieldName)(...) { 752 if(_arguments.length == 0) { 753 // a zero argument function call OR a getter.... 754 // we can't tell which for certain, so assume getter 755 // since they can always use the call method on the returned 756 // variable 757 static if(is(R == Variable)) { 758 auto v = *(new Variable(name ~ "." ~ fieldName, group)); 759 } else { 760 auto v = *(new Variable(fieldName, vars)); 761 } 762 return v; 763 } else { 764 // we have some kind of assignment, but no help from the 765 // compiler to get the type of assignment... 766 767 // FIXME: once Variant is able to handle this, use it! 768 static if(is(R == Variable)) { 769 auto v = *(new Variable(this.name ~ "." ~ name, group)); 770 } else 771 auto v = *(new Variable(fieldName, vars)); 772 773 string attempt(string type) { 774 return `if(_arguments[0] == typeid(`~type~`)) v = va_arg!(`~type~`)(_argptr);`; 775 } 776 777 mixin(attempt("int")); 778 mixin(attempt("string")); 779 mixin(attempt("double")); 780 mixin(attempt("Element")); 781 mixin(attempt("ClientSideScript.Variable")); 782 mixin(attempt("real")); 783 mixin(attempt("long")); 784 785 return v; 786 } 787 } 788 789 auto opDispatch(string fieldName, T...)(T t) if(T.length != 0) { 790 static if(is(R == Variable)) { 791 auto tmp = group.codes.pop; 792 scope(exit) group.codes.push(tmp); 793 return *(new Variable(callFunction(name ~ "." ~ fieldName, t).toString[1..$-2], group)); // cut off the ending ;\n 794 } else { 795 return *(new Variable(callFunction(fieldName, t).toString, vars)); 796 } 797 } 798 799 800 } 801 802 803 804 /** 805 This wraps up a bunch of javascript magic. It doesn't 806 actually parse or run it - it just collects it for 807 attachment to a DOM document. 808 809 When it returns a variable, it returns it as a string 810 suitable for output into Javascript source. 811 812 813 auto js = new ClientSideScript; 814 815 js.myvariable = 10; 816 817 js.somefunction = ClientSideScript.Function( 818 819 820 js.block = { 821 js.alert("hello"); 822 auto a = "asds"; 823 824 js.alert(a, js.somevar); 825 }; 826 827 Translates into javascript: 828 alert("hello"); 829 alert("asds", somevar); 830 831 832 The passed code is evaluated lazily. 833 */ 834 835 /+ 836 class ClientSideScript : Element { 837 private Stack!(string*) codes; 838 this(Document par) { 839 codes = new Stack!(string*); 840 vars = new VariablesGroup; 841 vars.codes = codes; 842 super(par, "script"); 843 } 844 845 string name; 846 847 struct Source { string source; string toString() { return source; } } 848 849 void innerCode(void delegate() theCode) { 850 myCode = theCode; 851 } 852 853 override void innerRawSource(string s) { 854 myCode = null; 855 super.innerRawSource(s); 856 } 857 858 private void delegate() myCode; 859 860 override string toString() const { 861 auto HACK = cast(ClientSideScript) this; 862 if(HACK.myCode) { 863 string code; 864 865 HACK.codes.push(&code); 866 HACK.myCode(); 867 HACK.codes.pop(); 868 869 HACK.innerRawSource = "\n" ~ code; 870 } 871 872 return super.toString(); 873 } 874 875 enum commitCode = ` if(!codes.empty) { auto magic = codes.peek; (*magic) ~= code; }`; 876 877 struct Variable { 878 string name; 879 VariablesGroup group; 880 881 // formats it for use in an inline event handler 882 string inline() { 883 return name.replace("\t", ""); 884 } 885 886 this(string n, VariablesGroup g) { 887 name = n; 888 group = g; 889 } 890 891 Source set(T)(T t) { 892 string code = format("\t%s = %s;\n", name, toJavascript(t)); 893 if(!group.codes.empty) { 894 auto magic = group.codes.peek; 895 (*magic) ~= code; 896 } 897 898 //Variant v = t; 899 //group.repository[name] = v; 900 901 return Source(code); 902 } 903 904 Variant _get() { 905 return (group.repository)[name]; 906 } 907 908 Variable doAssignCode(string code) { 909 if(!group.codes.empty) { 910 auto magic = group.codes.peek; 911 (*magic) ~= "\t" ~ code ~ ";\n"; 912 } 913 return * ( new Variable(code, group) ); 914 } 915 916 Variable opSlice(size_t a, size_t b) { 917 return * ( new Variable(name ~ ".substring("~to!string(a) ~ ", " ~ to!string(b)~")", group) ); 918 } 919 920 Variable opBinary(string op, T)(T rhs) { 921 return * ( new Variable(name ~ " " ~ op ~ " " ~ toJavascript(rhs), group) ); 922 } 923 Variable opOpAssign(string op, T)(T rhs) { 924 return doAssignCode(name ~ " " ~ op ~ "= " ~ toJavascript(rhs)); 925 } 926 Variable opIndex(T)(T i) { 927 return * ( new Variable(name ~ "[" ~ toJavascript(i) ~ "]" , group) ); 928 } 929 Variable opIndexOpAssign(string op, T, R)(R rhs, T i) { 930 return doAssignCode(name ~ "[" ~ toJavascript(i) ~ "] " ~ op ~ "= " ~ toJavascript(rhs)); 931 } 932 Variable opIndexAssign(T, R)(R rhs, T i) { 933 return doAssignCode(name ~ "[" ~ toJavascript(i) ~ "]" ~ " = " ~ toJavascript(rhs)); 934 } 935 Variable opUnary(string op)() { 936 return * ( new Variable(op ~ name, group) ); 937 } 938 939 void opAssign(T)(T rhs) { 940 set(rhs); 941 } 942 943 // used to call with zero arguments 944 Source call() { 945 string code = "\t" ~ name ~ "();\n"; 946 if(!group.codes.empty) { 947 auto magic = group.codes.peek; 948 (*magic) ~= code; 949 } 950 return Source(code); 951 } 952 mixin opDispatches!(Variable); 953 954 // returns code to call a function 955 Source callFunction(T...)(string name, T t) { 956 string code = "\t" ~ name ~ "("; 957 958 bool outputted = false; 959 foreach(v; t) { 960 if(outputted) 961 code ~= ", "; 962 else 963 outputted = true; 964 965 code ~= toJavascript(v); 966 } 967 968 code ~= ");\n"; 969 970 if(!group.codes.empty) { 971 auto magic = group.codes.peek; 972 (*magic) ~= code; 973 } 974 return Source(code); 975 } 976 977 978 } 979 980 // this exists only to allow easier access 981 class VariablesGroup { 982 /// If the variable is a function, we call it. If not, we return the source 983 @property Variable opDispatch(string name)() { 984 return * ( new Variable(name, this) ); 985 } 986 987 Variant[string] repository; 988 Stack!(string*) codes; 989 } 990 991 VariablesGroup vars; 992 993 mixin opDispatches!(ClientSideScript); 994 995 // returns code to call a function 996 Source callFunction(T...)(string name, T t) { 997 string code = "\t" ~ name ~ "("; 998 999 bool outputted = false; 1000 foreach(v; t) { 1001 if(outputted) 1002 code ~= ", "; 1003 else 1004 outputted = true; 1005 1006 code ~= toJavascript(v); 1007 } 1008 1009 code ~= ");\n"; 1010 1011 mixin(commitCode); 1012 return Source(code); 1013 } 1014 1015 Variable thisObject() { 1016 return Variable("this", vars); 1017 } 1018 1019 Source setVariable(T)(string var, T what) { 1020 auto v = Variable(var, vars); 1021 return v.set(what); 1022 } 1023 1024 Source appendSource(string code) { 1025 mixin(commitCode); 1026 return Source(code); 1027 } 1028 1029 ref Variable var(string name) { 1030 string code = "\tvar " ~ name ~ ";\n"; 1031 mixin(commitCode); 1032 1033 auto v = new Variable(name, vars); 1034 1035 return *v; 1036 } 1037 } 1038 +/ 1039 1040 /* 1041 Interesting things with scripts: 1042 1043 1044 set script value with ease 1045 get a script value we've already set 1046 set script functions 1047 set script events 1048 call a script on pageload 1049 1050 document.scripts 1051 1052 1053 set styles 1054 get style precedence 1055 get style thing 1056 1057 */ 1058 1059 import std.conv; 1060 1061 /+ 1062 void main() { 1063 auto document = new Document("<lol></lol>"); 1064 auto js = new ClientSideScript(document); 1065 1066 auto ele = document.createElement("a"); 1067 document.root.appendChild(ele); 1068 1069 int dInt = 50; 1070 1071 js.innerCode = { 1072 js.var("funclol") = "hello, world"; // local variable definition 1073 js.funclol = "10"; // parens are (currently) required when setting 1074 js.funclol = 10; // works with a variety of basic types 1075 js.funclol = 10.4; 1076 js.funclol = js.rofl; // can also set to another js variable 1077 js.setVariable("name", [10, 20]); // try setVariable for complex types 1078 js.setVariable("name", 100); // it can also set with strings for names 1079 js.alert(js.funclol, dInt); // call functions with js and D arguments 1080 js.funclol().call; // to call without arguments, use the call method 1081 js.funclol(10); // calling with arguments looks normal 1082 js.funclol(10, "20"); // including multiple, varied arguments 1083 js.myelement = ele; // works with DOM references too 1084 js.a = js.b + js.c; // some operators work too 1085 js.a() += js.d; // for some ops, you need the parens to please the compiler 1086 js.o = js.b[10]; // indexing works too 1087 js.e[10] = js.a; // so does index assign 1088 js.e[10] += js.a; // and index op assign... 1089 1090 js.eles = js.document.getElementsByTagName("as"); // js objects are accessible too 1091 js.aaa = js.document.rofl.copter; // arbitrary depth 1092 1093 js.ele2 = js.myelement; 1094 1095 foreach(i; 0..5) // loops are done on the server - it may be unrolled 1096 js.a() += js.w; // in the script outputted, or not work properly... 1097 1098 js.one = js.a[0..5]; 1099 1100 js.math = js.a + js.b - js.c; // multiple things work too 1101 js.math = js.a + (js.b - js.c); // FIXME: parens to NOT work. 1102 1103 js.math = js.s + 30; // and math with literals 1104 js.math = js.s + (40 + dInt) - 10; // and D variables, which may be 1105 // optimized by the D compiler with parens 1106 1107 }; 1108 1109 write(js.toString); 1110 } 1111 +/ 1112 import std.stdio; 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 // helper for json 1129 1130 1131 import std.json; 1132 import std.traits; 1133 1134 /+ 1135 string toJavascript(T)(T a) { 1136 static if(is(T == ClientSideScript.Variable)) { 1137 return a.name; 1138 } else static if(is(T : Element)) { 1139 if(a is null) 1140 return "null"; 1141 1142 if(a.id.length == 0) { 1143 static int count; 1144 a.id = "javascript-referenced-element-" ~ to!string(++count); 1145 } 1146 1147 return `document.getElementById("`~ a.id ~`")`; 1148 } else { 1149 auto jsonv = toJsonValue(a); 1150 return toJSON(&jsonv); 1151 } 1152 } 1153 1154 import arsd.web; // for toJsonValue 1155 1156 /+ 1157 string passthrough(string d)() { 1158 return d; 1159 } 1160 1161 string dToJs(string d)(Document document) { 1162 auto js = new ClientSideScript(document); 1163 mixin(passthrough!(d)()); 1164 return js.toString(); 1165 } 1166 1167 string translateJavascriptSourceWithDToStandardScript(string src)() { 1168 // blocks of D { /* ... */ } are executed. Comments should work but 1169 // don't. 1170 1171 int state = 0; 1172 1173 int starting = 0; 1174 int ending = 0; 1175 1176 int startingString = 0; 1177 int endingString = 0; 1178 1179 int openBraces = 0; 1180 1181 1182 string result; 1183 1184 Document document = new Document("<root></root>"); 1185 1186 foreach(i, c; src) { 1187 switch(state) { 1188 case 0: 1189 if(c == 'D') { 1190 endingString = i; 1191 state++; 1192 } 1193 break; 1194 case 1: 1195 if(c == ' ') { 1196 state++; 1197 } else { 1198 state = 0; 1199 } 1200 break; 1201 case 2: 1202 if(c == '{') { 1203 state++; 1204 starting = i; 1205 openBraces = 1; 1206 } else { 1207 state = 0; 1208 } 1209 break; 1210 case 3: 1211 // We're inside D 1212 if(c == '{') 1213 openBraces++; 1214 if(c == '}') { 1215 openBraces--; 1216 if(openBraces == 0) { 1217 state = 0; 1218 ending = i + 1; 1219 1220 // run some D.. 1221 1222 string str = src[startingString .. endingString]; 1223 1224 startingString = i + 1; 1225 string d = src[starting .. ending]; 1226 1227 1228 result ~= str; 1229 1230 //result ~= dToJs!(d)(document); 1231 1232 result ~= "/* " ~ d ~ " */"; 1233 } 1234 } 1235 break; 1236 } 1237 } 1238 1239 result ~= src[startingString .. $]; 1240 1241 return result; 1242 } 1243 +/ 1244 +/ 1245 1246 abstract class CssPart { 1247 string comment; 1248 override string toString() const; 1249 CssPart clone() const; 1250 } 1251 1252 class CssAtRule : CssPart { 1253 this() {} 1254 this(ref string css) { 1255 assert(css.length); 1256 assert(css[0] == '@'); 1257 1258 auto cssl = css.length; 1259 int braceCount = 0; 1260 int startOfInnerSlice = -1; 1261 1262 foreach(i, c; css) { 1263 if(braceCount == 0 && c == ';') { 1264 content = css[0 .. i + 1]; 1265 css = css[i + 1 .. $]; 1266 1267 opener = content; 1268 break; 1269 } 1270 1271 if(c == '{') { 1272 braceCount++; 1273 if(startOfInnerSlice == -1) 1274 startOfInnerSlice = cast(int) i; 1275 } 1276 if(c == '}') { 1277 braceCount--; 1278 if(braceCount < 0) 1279 throw new Exception("Bad CSS: mismatched }"); 1280 1281 if(braceCount == 0) { 1282 opener = css[0 .. startOfInnerSlice]; 1283 inner = css[startOfInnerSlice + 1 .. i]; 1284 1285 content = css[0 .. i + 1]; 1286 css = css[i + 1 .. $]; 1287 break; 1288 } 1289 } 1290 } 1291 1292 if(cssl == css.length) { 1293 throw new Exception("Bad CSS: unclosed @ rule. " ~ to!string(braceCount) ~ " brace(s) uncloced"); 1294 } 1295 1296 innerParts = lexCss(inner, false); 1297 } 1298 1299 string content; 1300 1301 string opener; 1302 string inner; 1303 1304 CssPart[] innerParts; 1305 1306 override CssAtRule clone() const { 1307 auto n = new CssAtRule(); 1308 n.content = content; 1309 n.opener = opener; 1310 n.inner = inner; 1311 foreach(part; innerParts) 1312 n.innerParts ~= part.clone(); 1313 return n; 1314 } 1315 override string toString() const { 1316 string c; 1317 if(comment.length) 1318 c ~= "/* " ~ comment ~ "*/\n"; 1319 c ~= opener.strip(); 1320 if(innerParts.length) { 1321 string i; 1322 foreach(part; innerParts) 1323 i ~= part.toString() ~ "\n"; 1324 1325 c ~= " {\n"; 1326 foreach(line; i.splitLines) 1327 c ~= "\t" ~ line ~ "\n"; 1328 c ~= "}"; 1329 } 1330 return c; 1331 } 1332 } 1333 1334 class CssRuleSet : CssPart { 1335 this() {} 1336 1337 this(ref string css) { 1338 auto idx = css.indexOf("{"); 1339 assert(idx != -1); 1340 foreach(selector; css[0 .. idx].split(",")) 1341 selectors ~= selector.strip; 1342 1343 css = css[idx .. $]; 1344 int braceCount = 0; 1345 string content; 1346 size_t f = css.length; 1347 foreach(i, c; css) { 1348 if(c == '{') 1349 braceCount++; 1350 if(c == '}') { 1351 braceCount--; 1352 if(braceCount == 0) { 1353 f = i; 1354 break; 1355 } 1356 } 1357 } 1358 1359 content = css[1 .. f]; // skipping the { 1360 if(f < css.length && css[f] == '}') 1361 f++; 1362 css = css[f .. $]; 1363 1364 contents = lexCss(content, false); 1365 } 1366 1367 string[] selectors; 1368 CssPart[] contents; 1369 1370 override CssRuleSet clone() const { 1371 auto n = new CssRuleSet(); 1372 n.selectors = selectors.dup; 1373 foreach(part; contents) 1374 n.contents ~= part.clone(); 1375 return n; 1376 } 1377 1378 CssRuleSet[] deNest(CssRuleSet outer = null) const { 1379 CssRuleSet[] ret; 1380 1381 CssRuleSet levelOne = new CssRuleSet(); 1382 ret ~= levelOne; 1383 if(outer is null) 1384 levelOne.selectors = selectors.dup; 1385 else { 1386 foreach(outerSelector; outer.selectors.length ? outer.selectors : [""]) 1387 foreach(innerSelector; selectors) { 1388 /* 1389 it would be great to do a top thing and a bottom, examples: 1390 .awesome, .awesome\& { 1391 .something img {} 1392 } 1393 1394 should give: 1395 .awesome .something img, .awesome.something img { } 1396 1397 And also 1398 \&.cool { 1399 .something img {} 1400 } 1401 1402 should give: 1403 .something img.cool {} 1404 1405 OR some such syntax. 1406 1407 1408 The idea though is it will ONLY apply to end elements with that particular class. Why is this good? We might be able to isolate the css more for composited files. 1409 1410 idk though. 1411 */ 1412 /+ 1413 // FIXME: this implementation is useless, but the idea of allowing combinations at the top level rox. 1414 if(outerSelector.length > 2 && outerSelector[$-2] == '\\' && outerSelector[$-1] == '&') { 1415 // the outer one is an adder... so we always want to paste this on, and if the inner has it, collapse it 1416 if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&') 1417 levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector[2 .. $]; 1418 else 1419 levelOne.selectors ~= outerSelector[0 .. $-2] ~ innerSelector; 1420 } else 1421 +/ 1422 1423 // we want to have things like :hover, :before, etc apply without implying 1424 // a descendant. 1425 1426 // If you want it to be a descendant pseudoclass, use the *:something - the 1427 // wildcard tag - instead of just a colon. 1428 1429 // But having this is too useful to ignore. 1430 if(innerSelector.length && innerSelector[0] == ':') 1431 levelOne.selectors ~= outerSelector ~ innerSelector; 1432 // we also allow \&something to get them concatenated 1433 else if(innerSelector.length > 2 && innerSelector[0] == '\\' && innerSelector[1] == '&') 1434 levelOne.selectors ~= outerSelector ~ innerSelector[2 .. $].strip; 1435 else 1436 levelOne.selectors ~= outerSelector ~ " " ~ innerSelector; // otherwise, use some other operator... 1437 } 1438 } 1439 1440 foreach(part; contents) { 1441 auto set = cast(CssRuleSet) part; 1442 if(set is null) 1443 levelOne.contents ~= part.clone(); 1444 else { 1445 // actually gotta de-nest this 1446 ret ~= set.deNest(levelOne); 1447 } 1448 } 1449 1450 return ret; 1451 } 1452 1453 override string toString() const { 1454 string ret; 1455 1456 1457 if(comment.length) 1458 ret ~= "/* " ~ comment ~ "*/\n"; 1459 1460 bool outputtedSelector = false; 1461 foreach(selector; selectors) { 1462 if(outputtedSelector) 1463 ret ~= ", "; 1464 else 1465 outputtedSelector = true; 1466 1467 ret ~= selector; 1468 } 1469 1470 ret ~= " {\n"; 1471 foreach(content; contents) { 1472 auto str = content.toString(); 1473 if(str.length) 1474 str = "\t" ~ str.replace("\n", "\n\t") ~ "\n"; 1475 1476 ret ~= str; 1477 } 1478 ret ~= "}"; 1479 1480 return ret; 1481 } 1482 } 1483 1484 class CssRule : CssPart { 1485 this() {} 1486 1487 this(ref string css, int endOfStatement) { 1488 content = css[0 .. endOfStatement]; 1489 if(endOfStatement < css.length && css[endOfStatement] == ';') 1490 endOfStatement++; 1491 1492 css = css[endOfStatement .. $]; 1493 } 1494 1495 // note: does not include the ending semicolon 1496 string content; 1497 1498 string key() const { 1499 auto idx = content.indexOf(":"); 1500 if(idx == -1) 1501 throw new Exception("Bad css, missing colon in " ~ content); 1502 return content[0 .. idx].strip.toLower; 1503 } 1504 1505 string value() const { 1506 auto idx = content.indexOf(":"); 1507 if(idx == -1) 1508 throw new Exception("Bad css, missing colon in " ~ content); 1509 1510 return content[idx + 1 .. $].strip; 1511 } 1512 1513 override CssRule clone() const { 1514 auto n = new CssRule(); 1515 n.content = content; 1516 return n; 1517 } 1518 1519 override string toString() const { 1520 string ret; 1521 if(strip(content).length == 0) 1522 ret = ""; 1523 else 1524 ret = key ~ ": " ~ value ~ ";"; 1525 1526 if(comment.length) 1527 ret ~= " /* " ~ comment ~ " */"; 1528 1529 return ret; 1530 } 1531 } 1532 1533 // Never call stripComments = false unless you have already stripped them. 1534 // this thing can't actually handle comments intelligently. 1535 CssPart[] lexCss(string css, bool stripComments = true) { 1536 if(stripComments) { 1537 import std.regex; 1538 css = std.regex.replace(css, regex(r"\/\*[^*]*\*+([^/*][^*]*\*+)*\/", "g"), ""); 1539 } 1540 1541 CssPart[] ret; 1542 css = css.stripLeft(); 1543 1544 int cnt; 1545 1546 while(css.length > 1) { 1547 CssPart p; 1548 1549 if(css[0] == '@') { 1550 p = new CssAtRule(css); 1551 } else { 1552 // non-at rules can be either rules or sets. 1553 // The question is: which comes first, the ';' or the '{' ? 1554 1555 auto endOfStatement = css.indexOfCssSmart(';'); 1556 if(endOfStatement == -1) 1557 endOfStatement = css.indexOf("}"); 1558 if(endOfStatement == -1) 1559 endOfStatement = css.length; 1560 1561 auto beginningOfBlock = css.indexOf("{"); 1562 if(beginningOfBlock == -1 || endOfStatement < beginningOfBlock) 1563 p = new CssRule(css, cast(int) endOfStatement); 1564 else 1565 p = new CssRuleSet(css); 1566 } 1567 1568 assert(p !is null); 1569 ret ~= p; 1570 1571 css = css.stripLeft(); 1572 } 1573 1574 return ret; 1575 } 1576 1577 // This needs to skip characters inside parens or quotes, so it 1578 // doesn't trip up on stuff like data uris when looking for a terminating 1579 // character. 1580 ptrdiff_t indexOfCssSmart(string i, char find) { 1581 int parenCount; 1582 char quote; 1583 bool escaping; 1584 foreach(idx, ch; i) { 1585 if(escaping) { 1586 escaping = false; 1587 continue; 1588 } 1589 if(quote != char.init) { 1590 if(ch == quote) 1591 quote = char.init; 1592 continue; 1593 } 1594 if(ch == '\'' || ch == '"') { 1595 quote = ch; 1596 continue; 1597 } 1598 1599 if(ch == '(') 1600 parenCount++; 1601 1602 if(parenCount) { 1603 if(ch == ')') 1604 parenCount--; 1605 continue; 1606 } 1607 1608 // at this point, we are not in parenthesis nor are we in 1609 // a quote, so we can actually search for the relevant character 1610 1611 if(ch == find) 1612 return idx; 1613 } 1614 return -1; 1615 } 1616 1617 string cssToString(in CssPart[] css) { 1618 string ret; 1619 foreach(c; css) { 1620 if(ret.length) { 1621 if(ret[$ -1] == '}') 1622 ret ~= "\n\n"; 1623 else 1624 ret ~= "\n"; 1625 } 1626 ret ~= c.toString(); 1627 } 1628 1629 return ret; 1630 } 1631 1632 /// Translates nested css 1633 const(CssPart)[] denestCss(CssPart[] css) { 1634 CssPart[] ret; 1635 foreach(part; css) { 1636 auto at = cast(CssAtRule) part; 1637 if(at is null) { 1638 auto set = cast(CssRuleSet) part; 1639 if(set is null) 1640 ret ~= part; 1641 else { 1642 ret ~= set.deNest(); 1643 } 1644 } else { 1645 // at rules with content may be denested at the top level... 1646 // FIXME: is this even right all the time? 1647 1648 if(at.inner.length) { 1649 auto newCss = at.opener ~ "{\n"; 1650 1651 // the whitespace manipulations are just a crude indentation thing 1652 newCss ~= "\t" ~ (cssToString(denestCss(lexCss(at.inner, false))).replace("\n", "\n\t").replace("\n\t\n\t", "\n\n\t")); 1653 1654 newCss ~= "\n}"; 1655 1656 ret ~= new CssAtRule(newCss); 1657 } else { 1658 ret ~= part; // no inner content, nothing special needed 1659 } 1660 } 1661 } 1662 1663 return ret; 1664 } 1665 1666 /* 1667 Forms: 1668 1669 ¤var 1670 ¤lighten(¤foreground, 0.5) 1671 ¤lighten(¤foreground, 0.5); -- exactly one semicolon shows up at the end 1672 ¤var(something, something_else) { 1673 final argument 1674 } 1675 1676 ¤function { 1677 argument 1678 } 1679 1680 1681 Possible future: 1682 1683 Recursive macros: 1684 1685 ¤define(li) { 1686 <li>¤car</li> 1687 list(¤cdr) 1688 } 1689 1690 ¤define(list) { 1691 ¤li(¤car) 1692 } 1693 1694 1695 car and cdr are borrowed from lisp... hmm 1696 do i really want to do this... 1697 1698 1699 1700 But if the only argument is cdr, and it is empty the function call is cancelled. 1701 This lets you do some looping. 1702 1703 1704 hmmm easier would be 1705 1706 ¤loop(macro_name, args...) { 1707 body 1708 } 1709 1710 when you call loop, it calls the macro as many times as it can for the 1711 given args, and no more. 1712 1713 1714 1715 Note that set is a macro; it doesn't expand it's arguments. 1716 To force expansion, use echo (or expand?) on the argument you set. 1717 */ 1718 1719 // Keep in mind that this does not understand comments! 1720 class MacroExpander { 1721 dstring delegate(dstring[])[dstring] functions; 1722 dstring[dstring] variables; 1723 1724 /// This sets a variable inside the macro system 1725 void setValue(string key, string value) { 1726 variables[to!dstring(key)] = to!dstring(value); 1727 } 1728 1729 struct Macro { 1730 dstring name; 1731 dstring[] args; 1732 dstring definition; 1733 } 1734 1735 Macro[dstring] macros; 1736 1737 // FIXME: do I want user defined functions or something? 1738 1739 this() { 1740 functions["get"] = &get; 1741 functions["set"] = &set; 1742 functions["define"] = &define; 1743 functions["loop"] = &loop; 1744 1745 functions["echo"] = delegate dstring(dstring[] args) { 1746 dstring ret; 1747 bool outputted; 1748 foreach(arg; args) { 1749 if(outputted) 1750 ret ~= ", "; 1751 else 1752 outputted = true; 1753 ret ~= arg; 1754 } 1755 1756 return ret; 1757 }; 1758 1759 functions["uriEncode"] = delegate dstring(dstring[] args) { 1760 return to!dstring(std.uri.encodeComponent(to!string(args[0]))); 1761 }; 1762 1763 functions["test"] = delegate dstring(dstring[] args) { 1764 assert(0, to!string(args.length) ~ " args: " ~ to!string(args)); 1765 }; 1766 1767 functions["include"] = &include; 1768 } 1769 1770 string[string] includeFiles; 1771 1772 dstring include(dstring[] args) { 1773 string s; 1774 foreach(arg; args) { 1775 string lol = to!string(arg); 1776 s ~= to!string(includeFiles[lol]); 1777 } 1778 1779 return to!dstring(s); 1780 } 1781 1782 // the following are used inside the user text 1783 1784 dstring define(dstring[] args) { 1785 enforce(args.length > 1, "requires at least a macro name and definition"); 1786 1787 Macro m; 1788 m.name = args[0]; 1789 if(args.length > 2) 1790 m.args = args[1 .. $ - 1]; 1791 m.definition = args[$ - 1]; 1792 1793 macros[m.name] = m; 1794 1795 return null; 1796 } 1797 1798 dstring set(dstring[] args) { 1799 enforce(args.length == 2, "requires two arguments. got " ~ to!string(args)); 1800 variables[args[0]] = args[1]; 1801 return ""; 1802 } 1803 1804 dstring get(dstring[] args) { 1805 enforce(args.length == 1); 1806 if(args[0] !in variables) 1807 return ""; 1808 return variables[args[0]]; 1809 } 1810 1811 dstring loop(dstring[] args) { 1812 enforce(args.length > 1, "must provide a macro name and some arguments"); 1813 auto m = macros[args[0]]; 1814 args = args[1 .. $]; 1815 dstring returned; 1816 1817 size_t iterations = args.length; 1818 if(m.args.length != 0) 1819 iterations = (args.length + m.args.length - 1) / m.args.length; 1820 1821 foreach(i; 0 .. iterations) { 1822 returned ~= expandMacro(m, args); 1823 if(m.args.length < args.length) 1824 args = args[m.args.length .. $]; 1825 else 1826 args = null; 1827 } 1828 1829 return returned; 1830 } 1831 1832 /// Performs the expansion 1833 string expand(string srcutf8) { 1834 auto src = expand(to!dstring(srcutf8)); 1835 return to!string(src); 1836 } 1837 1838 private int depth = 0; 1839 /// ditto 1840 dstring expand(dstring src) { 1841 return expandImpl(src, null); 1842 } 1843 1844 // FIXME: the order of evaluation shouldn't matter. Any top level sets should be run 1845 // before anything is expanded. 1846 private dstring expandImpl(dstring src, dstring[dstring] localVariables) { 1847 depth ++; 1848 if(depth > 10) 1849 throw new Exception("too much recursion depth in macro expansion"); 1850 1851 bool doneWithSetInstructions = false; // this is used to avoid double checks each loop 1852 for(;;) { 1853 // we do all the sets first since the latest one is supposed to be used site wide. 1854 // this allows a later customization to apply to the entire document. 1855 auto idx = doneWithSetInstructions ? -1 : src.indexOf("¤set"); 1856 if(idx == -1) { 1857 doneWithSetInstructions = true; 1858 idx = src.indexOf("¤"); 1859 } 1860 if(idx == -1) { 1861 depth--; 1862 return src; 1863 } 1864 1865 // the replacement goes 1866 // src[0 .. startingSliceForReplacement] ~ new ~ src[endingSliceForReplacement .. $]; 1867 sizediff_t startingSliceForReplacement, endingSliceForReplacement; 1868 1869 dstring functionName; 1870 dstring[] arguments; 1871 bool addTrailingSemicolon; 1872 1873 startingSliceForReplacement = idx; 1874 // idx++; // because the star in UTF 8 is two characters. FIXME: hack -- not needed thx to dstrings 1875 auto possibility = src[idx + 1 .. $]; 1876 size_t argsBegin; 1877 1878 bool found = false; 1879 foreach(i, c; possibility) { 1880 if(!( 1881 // valid identifiers 1882 (c >= 'A' && c <= 'Z') 1883 || 1884 (c >= 'a' && c <= 'z') 1885 || 1886 (c >= '0' && c <= '9') 1887 || 1888 c == '_' 1889 )) { 1890 // not a valid identifier means 1891 // we're done reading the name 1892 functionName = possibility[0 .. i]; 1893 argsBegin = i; 1894 found = true; 1895 break; 1896 } 1897 } 1898 1899 if(!found) { 1900 functionName = possibility; 1901 argsBegin = possibility.length; 1902 } 1903 1904 auto endOfVariable = argsBegin + idx + 1; // this is the offset into the original source 1905 1906 bool checkForAllArguments = true; 1907 1908 moreArguments: 1909 1910 assert(argsBegin); 1911 1912 endingSliceForReplacement = argsBegin + idx + 1; 1913 1914 while( 1915 argsBegin < possibility.length && ( 1916 possibility[argsBegin] == ' ' || 1917 possibility[argsBegin] == '\t' || 1918 possibility[argsBegin] == '\n' || 1919 possibility[argsBegin] == '\r')) 1920 { 1921 argsBegin++; 1922 } 1923 1924 if(argsBegin == possibility.length) { 1925 endingSliceForReplacement = src.length; 1926 goto doReplacement; 1927 } 1928 1929 switch(possibility[argsBegin]) { 1930 case '(': 1931 if(!checkForAllArguments) 1932 goto doReplacement; 1933 1934 // actually parsing the arguments 1935 size_t currentArgumentStarting = argsBegin + 1; 1936 1937 int open; 1938 1939 bool inQuotes; 1940 bool inTicks; 1941 bool justSawBackslash; 1942 foreach(i, c; possibility[argsBegin .. $]) { 1943 if(c == '`') 1944 inTicks = !inTicks; 1945 1946 if(inTicks) 1947 continue; 1948 1949 if(!justSawBackslash && c == '"') 1950 inQuotes = !inQuotes; 1951 1952 if(c == '\\') 1953 justSawBackslash = true; 1954 else 1955 justSawBackslash = false; 1956 1957 if(inQuotes) 1958 continue; 1959 1960 if(open == 1 && c == ',') { // don't want to push a nested argument incorrectly... 1961 // push the argument 1962 arguments ~= possibility[currentArgumentStarting .. i + argsBegin]; 1963 currentArgumentStarting = argsBegin + i + 1; 1964 } 1965 1966 if(c == '(') 1967 open++; 1968 if(c == ')') { 1969 open--; 1970 if(open == 0) { 1971 // push the last argument 1972 arguments ~= possibility[currentArgumentStarting .. i + argsBegin]; 1973 1974 endingSliceForReplacement = argsBegin + idx + 1 + i; 1975 argsBegin += i + 1; 1976 break; 1977 } 1978 } 1979 } 1980 1981 // then see if there's a { argument too 1982 checkForAllArguments = false; 1983 goto moreArguments; 1984 case '{': 1985 // find the match 1986 int open; 1987 foreach(i, c; possibility[argsBegin .. $]) { 1988 if(c == '{') 1989 open ++; 1990 if(c == '}') { 1991 open --; 1992 if(open == 0) { 1993 // cutting off the actual braces here 1994 arguments ~= possibility[argsBegin + 1 .. i + argsBegin]; 1995 // second +1 is there to cut off the } 1996 endingSliceForReplacement = argsBegin + idx + 1 + i + 1; 1997 1998 argsBegin += i + 1; 1999 break; 2000 } 2001 } 2002 } 2003 2004 goto doReplacement; 2005 default: 2006 goto doReplacement; 2007 } 2008 2009 doReplacement: 2010 if(endingSliceForReplacement < src.length && src[endingSliceForReplacement] == ';') { 2011 endingSliceForReplacement++; 2012 addTrailingSemicolon = true; // don't want a doubled semicolon 2013 // FIXME: what if it's just some whitespace after the semicolon? should that be 2014 // stripped or no? 2015 } 2016 2017 foreach(ref argument; arguments) { 2018 argument = argument.strip(); 2019 if(argument.length > 2 && argument[0] == '`' && argument[$-1] == '`') 2020 argument = argument[1 .. $ - 1]; // strip ticks here 2021 else 2022 if(argument.length > 2 && argument[0] == '"' && argument[$-1] == '"') 2023 argument = argument[1 .. $ - 1]; // strip quotes here 2024 2025 // recursive macro expanding 2026 // these need raw text, since they expand later. FIXME: should it just be a list of functions? 2027 if(functionName != "define" && functionName != "quote" && functionName != "set") 2028 argument = this.expandImpl(argument, localVariables); 2029 } 2030 2031 dstring returned = ""; 2032 if(functionName in localVariables) { 2033 /* 2034 if(functionName == "_head") 2035 returned = arguments[0]; 2036 else if(functionName == "_tail") 2037 returned = arguments[1 .. $]; 2038 else 2039 */ 2040 returned = localVariables[functionName]; 2041 } else if(functionName in functions) 2042 returned = functions[functionName](arguments); 2043 else if(functionName in variables) { 2044 returned = variables[functionName]; 2045 // FIXME 2046 // we also need to re-attach the arguments array, since variable pulls can't have args 2047 assert(endOfVariable > startingSliceForReplacement); 2048 endingSliceForReplacement = endOfVariable; 2049 } else if(functionName in macros) { 2050 returned = expandMacro(macros[functionName], arguments); 2051 } 2052 2053 if(addTrailingSemicolon && returned.length > 1 && returned[$ - 1] != ';') 2054 returned ~= ";"; 2055 2056 src = src[0 .. startingSliceForReplacement] ~ returned ~ src[endingSliceForReplacement .. $]; 2057 } 2058 assert(0); // not reached 2059 } 2060 2061 dstring expandMacro(Macro m, dstring[] arguments) { 2062 dstring[dstring] locals; 2063 foreach(i, arg; m.args) { 2064 if(i == arguments.length) 2065 break; 2066 locals[arg] = arguments[i]; 2067 } 2068 2069 return this.expandImpl(m.definition, locals); 2070 } 2071 } 2072 2073 2074 class CssMacroExpander : MacroExpander { 2075 this() { 2076 super(); 2077 2078 functions["prefixed"] = &prefixed; 2079 2080 functions["lighten"] = &(colorFunctionWrapper!lighten); 2081 functions["darken"] = &(colorFunctionWrapper!darken); 2082 functions["moderate"] = &(colorFunctionWrapper!moderate); 2083 functions["extremify"] = &(colorFunctionWrapper!extremify); 2084 functions["makeTextColor"] = &(oneArgColorFunctionWrapper!makeTextColor); 2085 2086 functions["oppositeLightness"] = &(oneArgColorFunctionWrapper!oppositeLightness); 2087 2088 functions["rotateHue"] = &(colorFunctionWrapper!rotateHue); 2089 2090 functions["saturate"] = &(colorFunctionWrapper!saturate); 2091 functions["desaturate"] = &(colorFunctionWrapper!desaturate); 2092 2093 functions["setHue"] = &(colorFunctionWrapper!setHue); 2094 functions["setSaturation"] = &(colorFunctionWrapper!setSaturation); 2095 functions["setLightness"] = &(colorFunctionWrapper!setLightness); 2096 } 2097 2098 // prefixed(border-radius: 12px); 2099 dstring prefixed(dstring[] args) { 2100 dstring ret; 2101 foreach(prefix; ["-moz-"d, "-webkit-"d, "-o-"d, "-ms-"d, "-khtml-"d, ""d]) 2102 ret ~= prefix ~ args[0] ~ ";"; 2103 return ret; 2104 } 2105 2106 /// Runs the macro expansion but then a CSS densesting 2107 string expandAndDenest(string cssSrc) { 2108 return cssToString(denestCss(lexCss(this.expand(cssSrc)))); 2109 } 2110 2111 // internal things 2112 dstring colorFunctionWrapper(alias func)(dstring[] args) { 2113 auto color = readCssColor(to!string(args[0])); 2114 auto percentage = readCssNumber(args[1]); 2115 return "#"d ~ to!dstring(func(color, percentage).toString()); 2116 } 2117 2118 dstring oneArgColorFunctionWrapper(alias func)(dstring[] args) { 2119 auto color = readCssColor(to!string(args[0])); 2120 return "#"d ~ to!dstring(func(color).toString()); 2121 } 2122 } 2123 2124 2125 real readCssNumber(dstring s) { 2126 s = s.replace(" "d, ""d); 2127 if(s.length == 0) 2128 return 0; 2129 if(s[$-1] == '%') 2130 return (to!real(s[0 .. $-1]) / 100f); 2131 return to!real(s); 2132 } 2133 2134 import std.format; 2135 2136 class JavascriptMacroExpander : MacroExpander { 2137 this() { 2138 super(); 2139 functions["foreach"] = &foreachLoop; 2140 } 2141 2142 2143 /** 2144 ¤foreach(item; array) { 2145 // code 2146 } 2147 2148 so arg0 .. argn-1 is the stuff inside. Conc 2149 */ 2150 2151 int foreachLoopCounter; 2152 dstring foreachLoop(dstring[] args) { 2153 enforce(args.length >= 2, "foreach needs parens and code"); 2154 dstring parens; 2155 bool outputted = false; 2156 foreach(arg; args[0 .. $ - 1]) { 2157 if(outputted) 2158 parens ~= ", "; 2159 else 2160 outputted = true; 2161 parens ~= arg; 2162 } 2163 2164 dstring variableName, arrayName; 2165 2166 auto it = parens.split(";"); 2167 variableName = it[0].strip; 2168 arrayName = it[1].strip; 2169 2170 dstring insideCode = args[$-1]; 2171 2172 dstring iteratorName; 2173 iteratorName = "arsd_foreach_loop_counter_"d ~ to!dstring(++foreachLoopCounter); 2174 dstring temporaryName = "arsd_foreach_loop_temporary_"d ~ to!dstring(++foreachLoopCounter); 2175 2176 auto writer = appender!dstring(); 2177 2178 formattedWrite(writer, " 2179 var %2$s = %5$s; 2180 if(%2$s != null) 2181 for(var %1$s = 0; %1$s < %2$s.length; %1$s++) { 2182 var %3$s = %2$s[%1$s]; 2183 %4$s 2184 }"d, iteratorName, temporaryName, variableName, insideCode, arrayName); 2185 2186 auto code = writer.data; 2187 2188 return to!dstring(code); 2189 } 2190 } 2191 2192 string beautifyCss(string css) { 2193 css = css.replace(":", ": "); 2194 css = css.replace(": ", ": "); 2195 css = css.replace("{", " {\n\t"); 2196 css = css.replace(";", ";\n\t"); 2197 css = css.replace("\t}", "}\n\n"); 2198 return css.strip; 2199 } 2200 2201 int fromHex(string s) { 2202 int result = 0; 2203 2204 int exp = 1; 2205 foreach(c; retro(s)) { 2206 if(c >= 'A' && c <= 'F') 2207 result += exp * (c - 'A' + 10); 2208 else if(c >= 'a' && c <= 'f') 2209 result += exp * (c - 'a' + 10); 2210 else if(c >= '0' && c <= '9') 2211 result += exp * (c - '0'); 2212 else 2213 throw new Exception("invalid hex character: " ~ cast(char) c); 2214 2215 exp *= 16; 2216 } 2217 2218 return result; 2219 } 2220 2221 Color readCssColor(string cssColor) { 2222 cssColor = cssColor.strip().toLower(); 2223 2224 if(cssColor.startsWith("#")) { 2225 cssColor = cssColor[1 .. $]; 2226 if(cssColor.length == 3) { 2227 cssColor = "" ~ cssColor[0] ~ cssColor[0] 2228 ~ cssColor[1] ~ cssColor[1] 2229 ~ cssColor[2] ~ cssColor[2]; 2230 } 2231 2232 if(cssColor.length == 6) 2233 cssColor ~= "ff"; 2234 2235 /* my extension is to do alpha */ 2236 if(cssColor.length == 8) { 2237 return Color( 2238 fromHex(cssColor[0 .. 2]), 2239 fromHex(cssColor[2 .. 4]), 2240 fromHex(cssColor[4 .. 6]), 2241 fromHex(cssColor[6 .. 8])); 2242 } else 2243 throw new Exception("invalid color " ~ cssColor); 2244 } else if(cssColor.startsWith("rgba")) { 2245 assert(0); // FIXME: implement 2246 /* 2247 cssColor = cssColor.replace("rgba", ""); 2248 cssColor = cssColor.replace(" ", ""); 2249 cssColor = cssColor.replace("(", ""); 2250 cssColor = cssColor.replace(")", ""); 2251 2252 auto parts = cssColor.split(","); 2253 */ 2254 } else if(cssColor.startsWith("rgb")) { 2255 assert(0); // FIXME: implement 2256 } else if(cssColor.startsWith("hsl")) { 2257 assert(0); // FIXME: implement 2258 } else 2259 return Color.fromNameString(cssColor); 2260 /* 2261 switch(cssColor) { 2262 default: 2263 // FIXME let's go ahead and try naked hex for compatibility with my gradient program 2264 assert(0, "Unknown color: " ~ cssColor); 2265 } 2266 */ 2267 } 2268 2269 /* 2270 Copyright: Adam D. Ruppe, 2010 - 2015 2271 License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>. 2272 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky and Trass3r 2273 2274 Copyright Adam D. Ruppe 2010-2015. 2275 Distributed under the Boost Software License, Version 1.0. 2276 (See accompanying file LICENSE_1_0.txt or copy at 2277 http://www.boost.org/LICENSE_1_0.txt) 2278 */