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