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