diff --git a/cgi.d b/cgi.d
index bf236e8..242e4ec 100644
--- a/cgi.d
+++ b/cgi.d
@@ -52,6 +52,14 @@
+/
module arsd.cgi;
+/*
+
+ To do a file download offer in the browser:
+
+ cgi.setResponseContentType("text/csv");
+ cgi.header("Content-Disposition: attachment; filename=\"customers.csv\"");
+*/
+
// FIXME: the location header is supposed to be an absolute url I guess.
// FIXME: would be cool to flush part of a dom document before complete
diff --git a/email.d b/email.d
new file mode 100644
index 0000000..d379283
--- /dev/null
+++ b/email.d
@@ -0,0 +1,153 @@
+module arsd.email;
+
+import std.net.curl;
+pragma(lib, "curl");
+
+import std.base64;
+
+// SEE ALSO: std.net.curl.SMTP
+
+struct RelayInfo {
+ string server;
+ string username;
+ string password;
+}
+
+class EmailMessage {
+ void setHeader(string name, string value) {
+ headers ~= name ~ ": " ~ value;
+ }
+
+ string[] to;
+ string[] cc;
+ string[] bcc;
+ string from;
+ string replyTo;
+ string inReplyTo;
+ string textBody;
+ string htmlBody;
+ string subject;
+
+ string[] headers;
+
+ private bool isMime = false;
+ private bool isHtml = false;
+
+ void setTextBody(string text) {}
+ void setHtmlBody(string html) {
+ isMime = true;
+ isHtml = true;
+ htmlBody = html;
+
+ import arsd.htmltotext;
+ if(textBody is null)
+ textBody = htmlToText(html);
+ }
+
+ struct MimeAttachment {
+ string type;
+ string filename;
+ const(void)[] content;
+ }
+
+ const(MimeAttachment)[] attachments;
+
+ void addAttachment(string mimeType, string filename, in void[] content) {
+ isMime = true;
+ attachments ~= MimeAttachment(mimeType, filename, content);
+ }
+
+
+ string toString() {
+ string boundary = "0016e64be86203dd36047610926a"; // FIXME
+
+ assert(!isHtml || (isHtml && isMime));
+
+ auto headers = this.headers;
+
+ string toHeader = "To: ";
+ bool toHeaderOutputted = false;
+ foreach(t; to) {
+ if(toHeaderOutputted)
+ toHeader ~= ", ";
+ else
+ toHeaderOutputted = true;
+
+ toHeader ~= t;
+ }
+
+ if(to.length)
+ headers ~= toHeader;
+
+ if(subject !is null)
+ headers ~= "Subject: " ~ subject;
+
+ if(isMime)
+ headers ~= "MIME-Version: 1.0";
+
+ if(attachments.length)
+ headers ~= "Content-Type: multipart/mixed; boundary=" ~ boundary;
+ else if(isHtml)
+ headers ~= "Content-Type: multipart/alternative; boundary=" ~ boundary;
+ else
+ headers ~= "Content-Type: text/plain; charset=UTF-8";
+
+ string msg;
+ msg.reserve(htmlBody.length + textBody.length + 1024);
+
+ foreach(header; headers)
+ msg ~= header ~ "\r\n";
+ if(msg.length) // has headers
+ msg ~= "\r\n";
+
+ if(isMime) {
+ msg ~= "--" ~ boundary ~ "\r\n";
+ msg ~= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
+ }
+
+ msg ~= textBody;
+
+ if(isMime)
+ msg ~= "\r\n--" ~ boundary;
+ if(isHtml) {
+ msg ~= "\r\n";
+ msg ~= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
+ msg ~= htmlBody;
+ msg ~= "\r\n--" ~ boundary;
+ }
+
+ foreach(attachment; attachments) {
+ assert(isMime);
+ msg ~= "\r\n";
+ msg ~= "Content-Type: " ~ attachment.type ~ "\r\n";
+ msg ~= "Content-Disposition: attachment; filename=\""~attachment.filename~"\"\r\n";
+ msg ~= "Content-Transfer-Encoding: base64\r\n";
+ msg ~= "\r\n";
+ msg ~= Base64.encode(cast(const(ubyte)[]) attachment.content);
+ msg ~= "\r\n--" ~ boundary;
+ }
+
+ if(isMime)
+ msg ~= "--\r\n";
+
+ return msg;
+ }
+
+ void send(RelayInfo mailServer = RelayInfo("smtp://localhost")) {
+ auto smtp = new SMTP(mailServer.server);
+ const(char)[][] allRecipients = cast(const(char)[][]) (to ~ cc ~ bcc); // WTF cast
+ smtp.mailTo(allRecipients);
+ smtp.mailFrom = from;
+ smtp.message = this.toString();
+ smtp.perform();
+ }
+}
+
+void email(string to, string subject, string message, string from) {
+ auto msg = new EmailMessage();
+ msg.from = from;
+ msg.to = [to];
+ msg.subject = subject;
+ msg.textBody = message;
+ msg.send();
+}
diff --git a/html.d b/html.d
index 2883e78..b0d3734 100644
--- a/html.d
+++ b/html.d
@@ -233,61 +233,10 @@ Html linkify(string text) {
/// Returns true of the string appears to be html/xml - if it matches the pattern
/// for tags or entities.
bool appearsToBeHtml(string src) {
- return false;
+ import std.regex;
+ return cast(bool) match(src, `.*\<[A-Za-z]+>.*`);
}
-/+
-void qsaFilter(string logicalScriptName) {
- string logicalScriptName = siteBase[0 .. $-1];
-
- foreach(a; document.querySelectorAll("a[qsa]")) {
- string href = logicalScriptName ~ _cgi.pathInfo ~ "?";
-
- int matches, possibilities;
-
- string[][string] vars;
- foreach(k, v; _cgi.getArray)
- vars[k] = cast(string[]) v;
- foreach(k, v; decodeVariablesSingle(a.qsa)) {
- if(k in _cgi.get && _cgi.get[k] == v)
- matches++;
- possibilities++;
-
- if(k !in vars || vars[k].length <= 1)
- vars[k] = [v];
- else
- assert(0, "qsa doesn't work here");
- }
-
- string[] clear = a.getAttribute("qsa-clear").split("&");
- clear ~= "ajaxLoading";
- if(a.parentNode !is null)
- clear ~= a.parentNode.getAttribute("qsa-clear").split("&");
-
- bool outputted = false;
- varskip: foreach(k, varr; vars) {
- foreach(item; clear)
- if(k == item)
- continue varskip;
- foreach(v; varr) {
- if(outputted)
- href ~= "&";
- else
- outputted = true;
-
- href ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
- }
- }
-
- a.href = href;
-
- a.removeAttribute("qsa");
-
- if(matches == possibilities)
- a.addClass("current");
- }
-}
-+/
string favicon(Document document) {
auto item = document.querySelector("link[rel~=icon]");
if(item !is null)
diff --git a/htmltotext.d b/htmltotext.d
new file mode 100644
index 0000000..163f8d4
--- /dev/null
+++ b/htmltotext.d
@@ -0,0 +1,155 @@
+module arsd.htmltotext;
+
+import arsd.dom;
+import std.string;
+import std.array : replace;
+
+string repeat(string s, int num) {
+ string ret;
+ foreach(i; 0 .. num)
+ ret ~= s;
+ return ret;
+}
+
+import std.stdio;
+
+static import std.regex;
+string htmlToText(string html, bool wantWordWrap = true, int wrapAmount = 74) {
+ Document document = new Document;
+
+
+ html = html.replace(" ", " ");
+ html = html.replace(" ", " ");
+ html = html.replace(" ", " ");
+ html = html.replace("\n", "");
+ html = html.replace("\r", "");
+ html = std.regex.replace(html, std.regex.regex("[\n\r\t \u00a0]+", "gm"), " ");
+
+ document.parse("" ~ html ~ "");
+
+ Element start;
+ auto bod = document.getElementsByTagName("body");
+ if(bod.length)
+ start = bod[0];
+ else
+ start = document.root;
+
+ start.innerHTML = start.innerHTML().replace("
", "\u0001");
+
+ again:
+ string result = "";
+ foreach(ele; start.tree) {
+ if(ele is start) continue;
+ if(ele.nodeType != 1) continue;
+
+ switch(ele.tagName) {
+ case "b":
+ case "strong":
+ ele.innerText = "*" ~ ele.innerText ~ "*";
+ ele.stripOut();
+ goto again;
+ break;
+ case "i":
+ case "em":
+ ele.innerText = "/" ~ ele.innerText ~ "/";
+ ele.stripOut();
+ goto again;
+ break;
+ case "u":
+ ele.innerText = "_" ~ ele.innerText ~ "_";
+ ele.stripOut();
+ goto again;
+ break;
+ case "h1":
+ ele.innerText = "\r" ~ ele.innerText ~ "\n" ~ repeat("=", ele.innerText.length) ~ "\r";
+ ele.stripOut();
+ goto again;
+ break;
+ case "h2":
+ ele.innerText = "\r" ~ ele.innerText ~ "\n" ~ repeat("-", ele.innerText.length) ~ "\r";
+ ele.stripOut();
+ goto again;
+ break;
+ case "h3":
+ ele.innerText = "\r" ~ ele.innerText.toUpper ~ "\r";
+ ele.stripOut();
+ goto again;
+ break;
+ case "p":
+ /*
+ if(ele.innerHTML.length > 1)
+ ele.innerHTML = "\r" ~ wrap(ele.innerHTML) ~ "\r";
+ ele.stripOut();
+ goto again;
+ */
+ break;
+ case "a":
+ string href = ele.getAttribute("href");
+ if(href) {
+ if(ele.innerText != href)
+ ele.innerText = ele.innerText ~ " <" ~ href ~ "> ";
+ }
+ ele.stripOut();
+ goto again;
+ break;
+ case "ol":
+ case "ul":
+ ele.innerHTML = "\r" ~ ele.innerHTML ~ "\r";
+ break;
+ case "li":
+ ele.innerHTML = "\t* " ~ ele.innerHTML ~ "\r";
+ ele.stripOut();
+ break;
+ case "sup":
+ ele.innerText = "^" ~ ele.innerText;
+ ele.stripOut();
+ break;
+ /*
+ case "img":
+ string alt = ele.getAttribute("alt");
+ if(alt)
+ result ~= ele.alt;
+ break;
+ */
+ default:
+ ele.stripOut();
+ goto again;
+ }
+ }
+
+ again2:
+ start.innerHTML = start.innerHTML().replace("\u0001", "\n");
+
+ foreach(ele; start.tree) {
+ if(ele.tagName == "p") {
+ if(strip(ele.innerText()).length > 1) {
+ string res = "";
+ string all = ele.innerText().replace("\n \n", "\n\n");
+ foreach(part; all.split("\n\n"))
+ res ~= "\r" ~ strip( wantWordWrap ? wrap(part, /*74*/ wrapAmount) : part ) ~ "\r";
+ ele.innerText = res;
+ } else
+ ele.innerText = strip(ele.innerText);
+ ele.stripOut();
+ goto again2;
+ }
+ }
+
+ result = start.innerText();
+
+ result = result.replace("\r ", "\r");
+ result = result.replace(" \r", "\r");
+
+ //result = result.replace("\u00a0", " ");
+
+
+ result = squeeze(result, "\r");
+ result = result.replace("\r", "\n\n");
+
+ result = result.replace("舗", "'"); // HACK: this shouldn't be needed, but apparently is in practice surely due to a bug elsewhere
+ result = result.replace(""", "\""); // HACK: this shouldn't be needed, but apparently is in practice surely due to a bug elsewhere
+ //result = htmlEntitiesDecode(result); // for special chars mainly
+
+ //a = std.regex.replace(a, std.regex.regex("(\n\t)+", "g"), "\n"); //\t");
+ return result.strip;
+}
diff --git a/web.d b/web.d
index 4e743a1..ac2c379 100644
--- a/web.d
+++ b/web.d
@@ -3201,6 +3201,58 @@ Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if(
}
}
+/// This adds a custom attribute to links in the document called qsa which modifies the values on the query string
+void translateQsa(Document document, Cgi cgi, string logicalScriptName = null) {
+ if(logicalScriptName is null)
+ logicalScriptName = cgi.scriptName;
+
+ foreach(a; document.querySelectorAll("a[qsa]")) {
+ string href = logicalScriptName ~ cgi.pathInfo ~ "?";
+
+ int matches, possibilities;
+
+ string[][string] vars;
+ foreach(k, v; cgi.getArray)
+ vars[k] = cast(string[]) v;
+ foreach(k, v; decodeVariablesSingle(a.qsa)) {
+ if(k in cgi.get && cgi.get[k] == v)
+ matches++;
+ possibilities++;
+
+ if(k !in vars || vars[k].length <= 1)
+ vars[k] = [v];
+ else
+ assert(0, "qsa doesn't work here");
+ }
+
+ string[] clear = a.getAttribute("qsa-clear").split("&");
+ clear ~= "ajaxLoading";
+ if(a.parentNode !is null)
+ clear ~= a.parentNode.getAttribute("qsa-clear").split("&");
+
+ bool outputted = false;
+ varskip: foreach(k, varr; vars) {
+ foreach(item; clear)
+ if(k == item)
+ continue varskip;
+ foreach(v; varr) {
+ if(outputted)
+ href ~= "&";
+ else
+ outputted = true;
+
+ href ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
+ }
+ }
+
+ a.href = href;
+
+ a.removeAttribute("qsa");
+
+ if(matches == possibilities)
+ a.addClass("current");
+ }
+}
/// This uses reflection info to generate Javascript that can call the server with some ease.
/// Also includes javascript base (see bottom of this file)