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