Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit of files

  • Loading branch information...
commit 641093ba3a0c314bef97c4c54c0fca146b5519a1 0 parents
@mheadd authored
Showing with 4,117 additions and 0 deletions.
  1. +3 −0  .couchappignore
  2. +1 −0  .couchapprc
  3. +48 −0 README.md
  4. +65 −0 _attachments/index.html
  5. +51 −0 _attachments/style/main.css
  6. +1 −0  _id
  7. +4 −0 couchapp.json
  8. +11 −0 evently/items/_changes/data.js
  9. +18 −0 evently/items/_changes/mustache.html
  10. +5 −0 evently/items/_changes/query.json
  11. +14 −0 evently/profile/profileReady/mustache.html
  12. +12 −0 evently/profile/profileReady/selectors/form/submit.js
  13. +1 −0  language
  14. +34 −0 rewrites.json
  15. +60 −0 tropo/sms-vote.php
  16. +235 −0 vendor/couchapp/_attachments/jquery.couch.app.js
  17. +90 −0 vendor/couchapp/_attachments/jquery.couch.app.util.js
  18. +399 −0 vendor/couchapp/_attachments/jquery.evently.js
  19. +346 −0 vendor/couchapp/_attachments/jquery.mustache.js
  20. +174 −0 vendor/couchapp/_attachments/jquery.pathbinder.js
  21. +17 −0 vendor/couchapp/_attachments/loader.js
  22. +22 −0 vendor/couchapp/evently/README.md
  23. +16 −0 vendor/couchapp/evently/account/_init.js
  24. +1 −0  vendor/couchapp/evently/account/adminParty/mustache.html
  25. +10 −0 vendor/couchapp/evently/account/doLogin.js
  26. +8 −0 vendor/couchapp/evently/account/doLogout.js
  27. +10 −0 vendor/couchapp/evently/account/doSignup.js
  28. +4 −0 vendor/couchapp/evently/account/loggedIn/after.js
  29. +7 −0 vendor/couchapp/evently/account/loggedIn/data.js
  30. +4 −0 vendor/couchapp/evently/account/loggedIn/mustache.html
  31. +3 −0  vendor/couchapp/evently/account/loggedIn/selectors.json
  32. +1 −0  vendor/couchapp/evently/account/loggedOut/mustache.html
  33. +4 −0 vendor/couchapp/evently/account/loggedOut/selectors.json
  34. +3 −0  vendor/couchapp/evently/account/loginForm/after.js
  35. +6 −0 vendor/couchapp/evently/account/loginForm/mustache.html
  36. +6 −0 vendor/couchapp/evently/account/loginForm/selectors/form/submit.js
  37. +3 −0  vendor/couchapp/evently/account/signupForm/after.js
  38. +6 −0 vendor/couchapp/evently/account/signupForm/mustache.html
  39. +6 −0 vendor/couchapp/evently/account/signupForm/selectors/form/submit.js
  40. +21 −0 vendor/couchapp/evently/profile/loggedIn.js
  41. +3 −0  vendor/couchapp/evently/profile/loggedOut/after.js
  42. +1 −0  vendor/couchapp/evently/profile/loggedOut/mustache.html
  43. +3 −0  vendor/couchapp/evently/profile/noProfile/data.js
  44. +11 −0 vendor/couchapp/evently/profile/noProfile/mustache.html
  45. +36 −0 vendor/couchapp/evently/profile/noProfile/selectors/form/submit.js
  46. +3 −0  vendor/couchapp/evently/profile/profileReady/after.js
  47. +3 −0  vendor/couchapp/evently/profile/profileReady/data.js
  48. +8 −0 vendor/couchapp/evently/profile/profileReady/mustache.html
  49. +39 −0 vendor/couchapp/lib/atom.js
  50. +25 −0 vendor/couchapp/lib/cache.js
  51. +20 −0 vendor/couchapp/lib/code.js
  52. +121 −0 vendor/couchapp/lib/docform.js
  53. +18 −0 vendor/couchapp/lib/linkup.js
  54. +13 −0 vendor/couchapp/lib/list.js
  55. +1,300 −0 vendor/couchapp/lib/markdown.js
  56. +261 −0 vendor/couchapp/lib/md5.js
  57. +339 −0 vendor/couchapp/lib/mustache.js
  58. +83 −0 vendor/couchapp/lib/path.js
  59. +8 −0 vendor/couchapp/lib/redirect.js
  60. +21 −0 vendor/couchapp/lib/utils.js
  61. +53 −0 vendor/couchapp/lib/validate.js
  62. +5 −0 vendor/couchapp/metadata.json
  63. +4 −0 views/number/map.js
  64. +4 −0 views/selections/map.js
  65. +5 −0 views/votes/map.js
3  .couchappignore
@@ -0,0 +1,3 @@
+[
+ "tropo"
+]
1  .couchapprc
@@ -0,0 +1 @@
+{}
48 README.md
@@ -0,0 +1,48 @@
+## CouchConf NYC Demo
+
+This is a simple [Tropo](http://www.tropo.com/) + [CouchBase](http://www.couchbase.com/) app I demoed at [CouchConf NYC](http://www.couchbase.com/couchconf-nyc). It's a realtime SMS voting app that was used to select the best rock song over 6 minutes in length, but it can be used as the basis for any kind of voting app you want to build.
+
+The magic is in Tropo's ability to make HTTP requests from inside a running telephony application to a CouchDB instance running on your local machine, your server, in the cloud - anywhere!
+
+Tropo + CouchBase == Cloud Telephony Awesomeness!
+
+## What is a CouchApp?
+
+CouchApps are web applications which can be served directly from [CouchDB](http://couchdb.apache.org). This gives them the nice property of replicating just like any other data stored in CouchDB. They are also simple to write as they can use the built-in jQuery libraries and plugins that ship with CouchDB.
+
+[More info about CouchApps here.](http://couchapp.org)
+
+## Deploying this app
+
+Assuming you just cloned this app from git, and you have changed into the app directory in your terminal, you want to push it to your CouchDB with the CouchApp command line tool, like this:
+
+ couchapp push . http://name:password@hostname:5984/mydatabase
+
+If you don't have a password on your CouchDB (admin party) you can do it like this (but it's a bad, idea, set a password):
+
+ couchapp push . http://hostname:5984/mydatabase
+
+If you get sick of typing the URL, you should setup a `.couchapprc` file in the root of your directory. Remember not to check this into version control as it will have passwords in it.
+
+The `.couchapprc` file should have contents like this:
+
+ {
+ "env" : {
+ "public" : {
+ "db" : "http://name:pass@mycouch.couchone.com/mydatabase"
+ },
+ "default" : {
+ "db" : "http://name:pass@localhost:5984/mydatabase"
+ }
+ }
+ }
+
+Now that you have the `.couchapprc` file set up, you can push your app to the CouchDB as simply as:
+
+ couchapp push
+
+This pushes to the `default` as specified. To push to the `public` you'd run:
+
+ couchapp push public
+
+Of course you can continue to add more deployment targets as you see fit, and give them whatever names you like.
65 _attachments/index.html
@@ -0,0 +1,65 @@
+<html>
+<head>
+<title>Tropo-Powered Realtime SMS Voting</title>
+<link rel="stylesheet" href="style/main.css" type="text/css"></head>
+<body>
+<h2></h2>
+<center>
+<div id="row"></div>
+</center>
+<p class="footer">Powered by <a href="http://tropo.com">Tropo</a></p>
+</body>
+<script src="vendor/couchapp/loader.js"></script>
+<script type="text/javascript" charset="utf-8">
+
+ $.couch.app(function(app) {
+
+ // Set the database to use.
+ var db = $.couch.db("couchconfdemo");
+
+ // Get the phone number for voting and render.
+ db.view("couchconfdemo/number", {
+ success: function(data) {
+ for(var i=0; i<data.rows.length; i++) {
+ $("h2").append(data.rows[i].value);
+ }
+ }
+ });
+
+ // Get all selections available for voting.
+ db.view("couchconfdemo/selections", {
+ success: function(data) {
+ for(var i=0; i<data.rows.length; i++) {
+ var selection = "<div id=\"choice\"><p class=\"score\" id=\"" + data.rows[i].id + "\">0</p><div class=\"selection\">";
+ selection += data.rows[i].id + "</div><p class=\"name\">" + data.rows[i].value.song;
+ selection += " (" + data.rows[i].value.group + ")</p></div>";
+ $("#row").append(selection);
+ }
+ }
+ });
+
+ // Get all past votes and incremement total.
+ db.view("couchconfdemo/votes", {
+ success: function(data) {
+ for(var i=0; i<data.rows.length; i++) {
+ var selector = "#" + data.rows[i].value;
+ var newValue = parseInt($(selector).html()) + 1;
+ $(selector).html(newValue);
+ }
+ }
+ });
+
+ // Watch the changes API for new votes.
+ db.changes(null, {include_docs: true}).onChange(function (data) {
+ for(var i=0; i<data.results.length; i++) {
+ var selector = "#" + data.results[i].doc.selection;
+ var newValue = parseInt($(selector).html()) + 1;
+ $(selector).html(newValue);
+ }
+ });
+
+ });
+
+</script>
+<script type="text/javascript" src="/_utils/script/jquery.couch.js"></script>
+</html>
51 _attachments/style/main.css
@@ -0,0 +1,51 @@
+body {
+ text-align: center;
+ background-image: url('http://blog.tropo.com/files/2011/10/Tropo-Vert.png'), url('http://blog.tropo.com/files/2011/10/couch-300x300.png');
+ background-position: 2% 2%, 98% 2%;
+ background-size: 10%, 10%;
+ background-repeat: no-repeat, no-repeat;;
+}
+
+#row {
+ width: 65%;
+ margin-left: 5px;
+}
+
+#choice {
+ text-align: center;
+ padding: 5px;
+ border-style: dashed;
+ border-width: 1px;
+ margin: 2px;
+ margin-top: 0px;
+ background-color: silver;
+}
+
+.score {
+ font-weight: bold;
+ color: maroon;
+ font-size: 115%;
+ margin-bottom: 2px;
+ margin-top: 2px;
+}
+
+.name {
+ font-weight: bold;
+}
+
+.selection {
+ float: left;
+ font-size: 125%;
+ font-weight: bold;
+ color: blue;
+ margin-top: 3px;
+ text-decoration: overline;
+}
+
+h4 {
+ margin-top: 5px;
+}
+
+.footer {
+font-size: 80%;
+}
1  _id
@@ -0,0 +1 @@
+_design/couchconfdemo
4 couchapp.json
@@ -0,0 +1,4 @@
+{
+ "name": "Tropo-Powered Realtime SMS Voting",
+ "description": "CouchApp"
+}
11 evently/items/_changes/data.js
@@ -0,0 +1,11 @@
+function(data) {
+ // $.log(data)
+ var p;
+ return {
+ items : data.rows.map(function(r) {
+ p = (r.value && r.value.profile) || {};
+ p.message = r.value && r.value.message;
+ return p;
+ })
+ }
+};
18 evently/items/_changes/mustache.html
@@ -0,0 +1,18 @@
+<p>Customize this format here: <tt>ddoc.evently.items._changes.mustache</tt></p>
+<h3>Recent Messages</h3>
+<ul>
+ {{#items}}
+ <li>
+ <div class="avatar">
+ {{#gravatar_url}}<img src="{{gravatar_url}}" alt="{{name}}"/>{{/gravatar_url}}
+ <div class="name">
+ {{nickname}}
+ </div>
+ </div>
+ <p>{{message}}</p>
+ <div style="clear:left;"></div>
+ </li>
+ {{/items}}
+</ul>
+<p><em>Protip:</em> If you setup continuous replication between this database and a remote one, this list will reflect remote changes in near real-time.</p>
+<p>This would be a good place to add pagination.</p>
5 evently/items/_changes/query.json
@@ -0,0 +1,5 @@
+{
+ "view" : "recent-items",
+ "descending" : "true",
+ "limit" : 50
+}
14 evently/profile/profileReady/mustache.html
@@ -0,0 +1,14 @@
+<p>Most applications will customize this template (<tt>ddoc.evently.profile.profileReady.mustache</tt>) for user input.</p>
+
+<div class="avatar">
+ {{#gravatar_url}}<img src="{{gravatar_url}}"/>{{/gravatar_url}}
+ <div class="name">
+ {{name}}
+ </div>
+</div>
+
+<form>
+ <label>New message from {{nickname}}: <input type="text" name="message" size=60 value=""></label>
+</form>
+
+<div style="clear:left;"></div>
12 evently/profile/profileReady/selectors/form/submit.js
@@ -0,0 +1,12 @@
+function() {
+ var form = $(this);
+ var fdoc = form.serializeObject();
+ fdoc.created_at = new Date();
+ fdoc.profile = $$("#profile").profile;
+ $$(this).app.db.saveDoc(fdoc, {
+ success : function() {
+ form[0].reset();
+ }
+ });
+ return false;
+};
1  language
@@ -0,0 +1 @@
+javascript
34 rewrites.json
@@ -0,0 +1,34 @@
+[
+{
+ "from": "",
+ "to": "index.html"
+},
+{
+ "from": "vendor/*",
+ "to": "vendor/*"
+},
+{
+ "from": "app/*",
+ "to": "app/*"
+},
+{
+ "from": "style/*",
+ "to": "style/*"
+},
+{
+ "from": "_view/*",
+ "to": "_view/*"
+},
+{
+ "from": "_changes/*",
+ "to": "_changes/*"
+},
+{
+ "from": "couchconfdemo/*",
+ "to": "../../*"
+},
+{
+ "from": "couchconfdemo/",
+ "to": "../../"
+}
+]
60 tropo/sms-vote.php
@@ -0,0 +1,60 @@
+<?php
+
+// CouchDB settings
+define("COUCH_HOST", "");
+define("COUCH_PORT", "");
+define("COUCH_DB_NAME", "");
+define("COUCH_USER", "");
+define("COUCH_PASS", "");
+
+// Function to save vote.
+function saveVote($id, $vote) {
+
+ $doc = json_encode(array("selection" => $vote));
+ $url = COUCH_HOST.":".COUCH_DB_PORT."/".COUCH_DB_NAME."/$id";
+
+ $putData = tmpfile();
+ fwrite($putData, $doc);
+ fseek($putData, 0);
+
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_PUT, true);
+ curl_setopt($ch, CURLOPT_INFILE, $putData);
+ curl_setopt($ch, CURLOPT_INFILESIZE, strlen($doc));
+ curl_setopt($ch, CURLOPT_USERPWD, COUCH_USER . ":" . COUCH_PASS);
+ curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ // Conflict update means someone tried to vote twice. :-(
+ if ($code != '201') {
+ _log("*** HTTP response code: $code ***");
+ return false;
+ }
+ return true;
+
+}
+
+// If the caller uses the voice channel, there is no initialText.
+if(!$currentCall->initialText) {
+ $vote = ask("Please enter the number of the selection you wish to vote for.", array("choices" => "1,2,3,4,5,6", "attempts" => 3));
+}
+
+// If the text channel is used, initialText is used to complete ask();
+else {
+ $vote = ask("", array("choices" => "[ANY]", "attempts" => 1));
+}
+
+// Save the vote.
+if(saveVote($currentCall->callerID, $vote->value)) {
+ if($currentCall->channel == "VOICE") {
+ say("Thank you, your vote has been recorded.");
+ }
+ else {
+ _log("*** Vote recorded for " . $currentCall->callerID . " ***");
+ }
+} else {
+ say("Sorry, there was a problem saving your vote.");
+}
+
+?>
235 vendor/couchapp/_attachments/jquery.couch.app.js
@@ -0,0 +1,235 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Usage: The passed in function is called when the page is ready.
+// CouchApp passes in the app object, which takes care of linking to
+// the proper database, and provides access to the CouchApp helpers.
+// $.couch.app(function(app) {
+// app.db.view(...)
+// ...
+// });
+
+(function($) {
+
+ function Design(db, name, code) {
+ this.doc_id = "_design/"+name;
+ if (code) {
+ this.code_path = this.doc_id + "/" + code;
+ } else {
+ this.code_path = this.doc_id;
+ }
+ this.view = function(view, opts) {
+ db.view(name+'/'+view, opts);
+ };
+ this.list = function(list, view, opts) {
+ db.list(name+'/'+list, view, opts);
+ };
+ }
+
+ function docForm() { alert("docForm has been moved to vendor/couchapp/lib/docForm.js, use app.require to load") };
+
+ function resolveModule(path, names, parents, current) {
+ parents = parents || [];
+ if (names.length === 0) {
+ if (typeof current != "string") {
+ throw ["error","invalid_require_path",
+ 'Must require a JavaScript string, not: '+(typeof current)];
+ }
+ return [current, parents];
+ }
+ var n = names.shift();
+ if (n == '..') {
+ parents.pop();
+ var pp = parents.pop();
+ if (!pp) {
+ throw ["error", "invalid_require_path", path];
+ }
+ return resolveModule(path, names, parents, pp);
+ } else if (n == '.') {
+ var p = parents.pop();
+ if (!p) {
+ throw ["error", "invalid_require_path", path];
+ }
+ return resolveModule(path, names, parents, p);
+ } else {
+ parents = [];
+ }
+ if (!current[n]) {
+ throw ["error", "invalid_require_path", path];
+ }
+ parents.push(current);
+ return resolveModule(path, names, parents, current[n]);
+ }
+
+ function makeRequire(ddoc) {
+ var moduleCache = [];
+ function getCachedModule(name, parents) {
+ var key, i, len = moduleCache.length;
+ for (i=0;i<len;++i) {
+ key = moduleCache[i].key;
+ if (key[0] === name && key[1] === parents) {
+ return moduleCache[i].module;
+ }
+ }
+ return null;
+ }
+ function setCachedModule(name, parents, module) {
+ moduleCache.push({ key: [name, parents], module: module });
+ }
+ var require = function (name, parents) {
+ var cachedModule = getCachedModule(name, parents);
+ if (cachedModule !== null) {
+ return cachedModule;
+ }
+ var exports = {};
+ var resolved = resolveModule(name, name.split('/'), parents, ddoc);
+ var source = resolved[0];
+ parents = resolved[1];
+ var s = "var func = function (exports, require) { " + source + " };";
+ try {
+ eval(s);
+ func.apply(ddoc, [exports, function(name) {return require(name, parents)}]);
+ } catch(e) {
+ throw ["error","compilation_error","Module require('"+name+"') raised error "+e.toSource()];
+ }
+ setCachedModule(name, parents, exports);
+ return exports;
+ }
+ return require;
+ };
+
+ function mockReq() {
+ var p = document.location.pathname.split('/'),
+ qs = document.location.search.replace(/^\?/,'').split('&'),
+ q = {};
+ qs.forEach(function(param) {
+ var ps = param.split('='),
+ k = decodeURIComponent(ps[0]),
+ v = decodeURIComponent(ps[1]);
+ if (["startkey", "endkey", "key"].indexOf(k) != -1) {
+ q[k] = JSON.parse(v);
+ } else {
+ q[k] = v;
+ }
+ });
+ p.shift();
+ return {
+ path : p,
+ query : q
+ };
+ };
+
+ $.couch.app = $.couch.app || function(appFun, opts) {
+ opts = opts || {};
+ var urlPrefix = (opts.urlPrefix || ""),
+ index = urlPrefix.split('/').length,
+ fragments = unescape(document.location.href).split('/'),
+ dbname = opts.db || fragments[index + 2],
+ dname = opts.design || fragments[index + 4];
+ $.couch.urlPrefix = urlPrefix;
+ var db = $.couch.db(dbname),
+ design = new Design(db, dname, opts.load_path);
+ var appExports = $.extend({
+ db : db,
+ design : design,
+ view : design.view,
+ list : design.list,
+ docForm : docForm, // deprecated
+ req : mockReq()
+ }, $.couch.app.app);
+ function handleDDoc(ddoc) {
+ if (ddoc) {
+ appExports.ddoc = ddoc;
+ appExports.require = makeRequire(ddoc);
+ }
+ appFun.apply(appExports, [appExports]);
+ }
+ if (opts.ddoc) {
+ // allow the ddoc to be embedded in the html
+ // to avoid a second http request
+ $.couch.app.ddocs[design.doc_id] = opts.ddoc;
+ }
+ if ($.couch.app.ddocs[design.doc_id]) {
+ $(function() {handleDDoc($.couch.app.ddocs[design.doc_id])});
+ } else {
+ // only open 1 connection for this ddoc
+ if ($.couch.app.ddoc_handlers[design.doc_id]) {
+ // we are already fetching, just wait
+ $.couch.app.ddoc_handlers[design.doc_id].push(handleDDoc);
+ } else {
+ $.couch.app.ddoc_handlers[design.doc_id] = [handleDDoc];
+ // use getDbProperty to bypass %2F encoding on _show/app
+ db.getDbProperty(design.code_path, {
+ success : function(doc) {
+ $.couch.app.ddocs[design.doc_id] = doc;
+ $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) {
+ $(function() {h(doc)});
+ });
+ $.couch.app.ddoc_handlers[design.doc_id] = null;
+ },
+ error : function() {
+ $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) {
+ $(function() {h()});
+ });
+ $.couch.app.ddoc_handlers[design.doc_id] = null;
+ }
+ });
+ }
+ }
+ };
+ $.couch.app.ddocs = {};
+ $.couch.app.ddoc_handlers = {};
+ // legacy support. $.CouchApp is deprecated, please use $.couch.app
+ $.CouchApp = $.couch.app;
+})(jQuery);
+
+// JavaScript 1.6 compatibility functions that are missing from IE7/IE8
+
+if (!Array.prototype.forEach)
+{
+ Array.prototype.forEach = function(fun /*, thisp*/)
+ {
+ var len = this.length >>> 0;
+ if (typeof fun != "function")
+ throw new TypeError();
+
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ {
+ if (i in this)
+ fun.call(thisp, this[i], i, this);
+ }
+ };
+}
+
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt)
+ {
+ var len = this.length >>> 0;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0)
+ ? Math.ceil(from)
+ : Math.floor(from);
+ if (from < 0)
+ from += len;
+
+ for (; from < len; from++)
+ {
+ if (from in this &&
+ this[from] === elt)
+ return from;
+ }
+ return -1;
+ };
+}
90 vendor/couchapp/_attachments/jquery.couch.app.util.js
@@ -0,0 +1,90 @@
+$.log = function(m) {
+ if (window && window.console && window.console.log) {
+ window.console.log(arguments.length == 1 ? m : arguments);
+ }
+};
+
+// http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery/1186309#1186309
+$.fn.serializeObject = function() {
+ var o = {};
+ var a = this.serializeArray();
+ $.each(a, function() {
+ if (o[this.name]) {
+ if (!o[this.name].push) {
+ o[this.name] = [o[this.name]];
+ }
+ o[this.name].push(this.value || '');
+ } else {
+ o[this.name] = this.value || '';
+ }
+ });
+ return o;
+};
+
+// todo remove this crap
+function escapeHTML(st) {
+ return(
+ st && st.replace(/&/g,'&amp;').
+ replace(/>/g,'&gt;').
+ replace(/</g,'&lt;').
+ replace(/"/g,'&quot;')
+ );
+};
+
+function safeHTML(st, len) {
+ return st ? escapeHTML(st.substring(0,len)) : '';
+}
+
+// todo this should take a replacement template
+$.linkify = function(body) {
+ return body.replace(/((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi,function(a) {
+ return '<a target="_blank" href="'+a+'">'+a+'</a>';
+ }).replace(/\@([\w\-]+)/g,function(user,name) {
+ return '<a href="#/mentions/'+encodeURIComponent(name.toLowerCase())+'">'+user+'</a>';
+ }).replace(/\#([\w\-\.]+)/g,function(word,tag) {
+ return '<a href="#/tags/'+encodeURIComponent(tag.toLowerCase())+'">'+word+'</a>';
+ });
+};
+
+$.fn.prettyDate = function() {
+ $(this).each(function() {
+ var string, title = $(this).attr("title");
+ if (title) {
+ string = $.prettyDate(title);
+ } else {
+ string = $.prettyDate($(this).text());
+ }
+ $(this).text(string);
+ });
+};
+
+$.prettyDate = function(time){
+
+ var date = new Date(time.replace(/-/g,"/").replace("T", " ").replace("Z", " +0000").replace(/(\d*\:\d*:\d*)\.\d*/g,"$1")),
+ diff = (((new Date()).getTime() - date.getTime()) / 1000),
+ day_diff = Math.floor(diff / 86400);
+
+ if (isNaN(day_diff)) return time;
+
+ return day_diff < 1 && (
+ diff < 60 && "just now" ||
+ diff < 120 && "1 minute ago" ||
+ diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
+ diff < 7200 && "1 hour ago" ||
+ diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
+ day_diff == 1 && "yesterday" ||
+ day_diff < 21 && day_diff + " days ago" ||
+ day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
+ time;
+ // day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" ||
+ // Math.ceil( day_diff / 365 ) + " years ago";
+};
+
+$.argsToArray = function(args) {
+ if (!args.callee) return args;
+ var array = [];
+ for (var i=0; i < args.length; i++) {
+ array.push(args[i]);
+ };
+ return array;
+}
399 vendor/couchapp/_attachments/jquery.evently.js
@@ -0,0 +1,399 @@
+// $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/
+function $$(node) {
+ var data = $(node).data("$$");
+ if (data) {
+ return data;
+ } else {
+ data = {};
+ $(node).data("$$", data);
+ return data;
+ }
+};
+
+(function($) {
+ // utility functions used in the implementation
+
+ function forIn(obj, fun) {
+ var name;
+ for (name in obj) {
+ if (obj.hasOwnProperty(name)) {
+ fun(name, obj[name]);
+ }
+ }
+ };
+ $.forIn = forIn;
+ function funViaString(fun, hint) {
+ if (fun && fun.match && fun.match(/^function/)) {
+ eval("var f = "+fun);
+ if (typeof f == "function") {
+ return function() {
+ try {
+ return f.apply(this, arguments);
+ } catch(e) {
+ // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION
+ $.log({"message": "Error in evently function.", "error": e,
+ "src" : fun, "hint":hint});
+ throw(e);
+ }
+ };
+ }
+ }
+ return fun;
+ };
+
+ function runIfFun(me, fun, args) {
+ // if the field is a function, call it, bound to the widget
+ var f = funViaString(fun, me);
+ if (typeof f == "function") {
+ return f.apply(me, args);
+ } else {
+ return fun;
+ }
+ }
+
+ $.evently = {
+ connect : function(source, target, events) {
+ events.forEach(function(ev) {
+ $(source).bind(ev, function() {
+ var args = $.makeArray(arguments);
+ // remove the original event to keep from stacking args extra deep
+ // it would be nice if jquery had a way to pass the original
+ // event to the trigger method.
+ args.shift();
+ $(target).trigger(ev, args);
+ return false;
+ });
+ });
+ },
+ paths : [],
+ changesDBs : {},
+ changesOpts : {}
+ };
+
+ function extractFrom(name, evs) {
+ return evs[name];
+ };
+
+ function extractEvents(name, ddoc) {
+ // extract events from ddoc.evently and ddoc.vendor.*.evently
+ var events = [true, {}]
+ , vendor = ddoc.vendor || {}
+ , evently = ddoc.evently || {}
+ ;
+ $.forIn(vendor, function(k, v) {
+ if (v.evently && v.evently[name]) {
+ events.push(v.evently[name]);
+ }
+ });
+ if (evently[name]) {events.push(evently[name]);}
+ return $.extend.apply(null, events);
+ }
+
+ function extractPartials(ddoc) {
+ var partials = [true, {}]
+ , vendor = ddoc.vendor || {}
+ , evently = ddoc.evently || {}
+ ;
+ $.forIn(vendor, function(k, v) {
+ if (v.evently && v.evently._partials) {
+ partials.push(v.evently._partials);
+ }
+ });
+ if (evently._partials) {partials.push(evently._partials);}
+ return $.extend.apply(null, partials);
+ };
+
+ function applyCommon(events) {
+ if (events._common) {
+ $.forIn(events, function(k, v) {
+ events[k] = $.extend(true, {}, events._common, v);
+ });
+ delete events._common;
+ return events;
+ } else {
+ return events;
+ }
+ }
+
+ $.fn.evently = function(events, app, args) {
+ var elem = $(this);
+ // store the app on the element for later use
+ if (app) {
+ $$(elem).app = app;
+ }
+
+ if (typeof events == "string") {
+ events = extractEvents(events, app.ddoc);
+ }
+ events = applyCommon(events);
+ $$(elem).evently = events;
+ if (app && app.ddoc) {
+ $$(elem).partials = extractPartials(app.ddoc);
+ }
+ // setup the handlers onto elem
+ forIn(events, function(name, h) {
+ eventlyHandler(elem, name, h, args);
+ });
+
+ if (events._init) {
+ elem.trigger("_init", args);
+ }
+
+ if (app && events._changes) {
+ $("body").bind("evently-changes-"+app.db.name, function() {
+ elem.trigger("_changes");
+ });
+ followChanges(app);
+ elem.trigger("_changes");
+ }
+ };
+
+ // eventlyHandler applies the user's handler (h) to the
+ // elem, bound to trigger based on name.
+ function eventlyHandler(elem, name, h, args) {
+ if ($.evently.log) {
+ elem.bind(name, function() {
+ $.log(elem, name);
+ });
+ }
+ if (h.path) {
+ elem.pathbinder(name, h.path);
+ }
+ var f = funViaString(h, name);
+ if (typeof f == "function") {
+ elem.bind(name, {args:args}, f);
+ } else if (typeof f == "string") {
+ elem.bind(name, {args:args}, function() {
+ $(this).trigger(f, arguments);
+ return false;
+ });
+ } else if ($.isArray(h)) {
+ // handle arrays recursively
+ for (var i=0; i < h.length; i++) {
+ eventlyHandler(elem, name, h[i], args);
+ }
+ } else {
+ // an object is using the evently / mustache template system
+ if (h.fun) {
+ throw("e.fun has been removed, please rename to e.before")
+ }
+ // templates, selectors, etc are intepreted
+ // when our named event is triggered.
+ elem.bind(name, {args:args}, function() {
+ renderElement($(this), h, arguments);
+ return false;
+ });
+ }
+ };
+
+ $.fn.replace = function(elem) {
+ // $.log("Replace", this)
+ $(this).empty().append(elem);
+ };
+
+ // todo: ability to call this
+ // to render and "prepend/append/etc" a new element to the host element (me)
+ // as well as call this in a way that replaces the host elements content
+ // this would be easy if there is a simple way to get at the element we just appended
+ // (as html) so that we can attache the selectors
+ function renderElement(me, h, args, qrun, arun) {
+ // if there's a query object we run the query,
+ // and then call the data function with the response.
+ if (h.before && (!qrun || !arun)) {
+ funViaString(h.before, me).apply(me, args);
+ }
+ if (h.async && !arun) {
+ runAsync(me, h, args)
+ } else if (h.query && !qrun) {
+ // $.log("query before renderElement", arguments)
+ runQuery(me, h, args)
+ } else {
+ // $.log("renderElement")
+ // $.log(me, h, args, qrun)
+ // otherwise we just render the template with the current args
+ var selectors = runIfFun(me, h.selectors, args);
+ var act = (h.render || "replace").replace(/\s/g,"");
+ var app = $$(me).app;
+ if (h.mustache) {
+ // $.log("rendering", h.mustache)
+ var newElem = mustachioed(me, h, args);
+ me[act](newElem);
+ }
+ if (selectors) {
+ if (act == "replace") {
+ var s = me;
+ } else {
+ var s = newElem;
+ }
+ forIn(selectors, function(selector, handlers) {
+ // $.log("selector", selector);
+ // $.log("selected", $(selector, s));
+ $(selector, s).evently(handlers, app, args);
+ // $.log("applied", selector);
+ });
+ }
+ if (h.after) {
+ runIfFun(me, h.after, args);
+ }
+ }
+ };
+
+ // todo this should return the new element
+ function mustachioed(me, h, args) {
+ var partials = $$(me).partials;
+ return $($.mustache(
+ runIfFun(me, h.mustache, args),
+ runIfFun(me, h.data, args),
+ runIfFun(me, $.extend(true, partials, h.partials), args)));
+ };
+
+ function runAsync(me, h, args) {
+ // the callback is the first argument
+ funViaString(h.async, me).apply(me, [function() {
+ renderElement(me, h,
+ $.argsToArray(arguments).concat($.argsToArray(args)), false, true);
+ }].concat($.argsToArray(args)));
+ };
+
+
+ function runQuery(me, h, args) {
+ // $.log("runQuery: args", args)
+ var app = $$(me).app;
+ var qu = runIfFun(me, h.query, args);
+ var qType = qu.type;
+ var viewName = qu.view;
+ var userSuccess = qu.success;
+ // $.log("qType", qType)
+
+ var q = {};
+ forIn(qu, function(k, v) {
+ if (["type", "view"].indexOf(k) == -1) {
+ q[k] = v;
+ }
+ });
+
+ if (qType == "newRows") {
+ q.success = function(resp) {
+ // $.log("runQuery newRows success", resp.rows.length, me, resp)
+ resp.rows.reverse().forEach(function(row) {
+ renderElement(me, h, [row].concat($.argsToArray(args)), true)
+ });
+ if (userSuccess) userSuccess(resp);
+ };
+ newRows(me, app, viewName, q);
+ } else {
+ q.success = function(resp) {
+ // $.log("runQuery success", resp)
+ renderElement(me, h, [resp].concat($.argsToArray(args)), true);
+ userSuccess && userSuccess(resp);
+ };
+ // $.log(app)
+ app.view(viewName, q);
+ }
+ }
+
+ // this is for the items handler
+ // var lastViewId, highKey, inFlight;
+ // this needs to key per elem
+ function newRows(elem, app, view, opts) {
+ // $.log("newRows", arguments);
+ // on success we'll set the top key
+ var thisViewId, successCallback = opts.success, full = false;
+ function successFun(resp) {
+ // $.log("newRows success", resp)
+ $$(elem).inFlight = false;
+ var JSONhighKey = JSON.stringify($$(elem).highKey);
+ resp.rows = resp.rows.filter(function(r) {
+ return JSON.stringify(r.key) != JSONhighKey;
+ });
+ if (resp.rows.length > 0) {
+ if (opts.descending) {
+ $$(elem).highKey = resp.rows[0].key;
+ } else {
+ $$(elem).highKey = resp.rows[resp.rows.length -1].key;
+ }
+ };
+ if (successCallback) {successCallback(resp, full)};
+ };
+ opts.success = successFun;
+
+ if (opts.descending) {
+ thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : "");
+ } else {
+ thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : "");
+ }
+ // $.log(["thisViewId",thisViewId])
+ // for query we'll set keys
+ if (thisViewId == $$(elem).lastViewId) {
+ // we only want the rows newer than changesKey
+ var hk = $$(elem).highKey;
+ if (hk !== undefined) {
+ if (opts.descending) {
+ opts.endkey = hk;
+ // opts.inclusive_end = false;
+ } else {
+ opts.startkey = hk;
+ }
+ }
+ // $.log("add view rows", opts)
+ if (!$$(elem).inFlight) {
+ $$(elem).inFlight = true;
+ app.view(view, opts);
+ }
+ } else {
+ // full refresh
+ // $.log("new view stuff")
+ full = true;
+ $$(elem).lastViewId = thisViewId;
+ $$(elem).highKey = undefined;
+ $$(elem).inFlight = true;
+ app.view(view, opts);
+ }
+ };
+
+ // only start one changes listener per db
+ function followChanges(app) {
+ var dbName = app.db.name, changeEvent = function(resp) {
+ $("body").trigger("evently-changes-"+dbName, [resp]);
+ };
+ if (!$.evently.changesDBs[dbName]) {
+ if (app.db.changes) {
+ // new api in jquery.couch.js 1.0
+ app.db.changes(null, $.evently.changesOpts).onChange(changeEvent);
+ } else {
+ // in case you are still on CouchDB 0.11 ;) deprecated.
+ connectToChanges(app, changeEvent);
+ }
+ $.evently.changesDBs[dbName] = true;
+ }
+ }
+ $.evently.followChanges = followChanges;
+ // deprecated. use db.changes() from jquery.couch.js
+ // this does not have an api for closing changes request.
+ function connectToChanges(app, fun, update_seq) {
+ function changesReq(seq) {
+ var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq;
+ if ($.evently.changesOpts.include_docs) {
+ url = url + "&include_docs=true";
+ }
+ $.ajax({
+ url: url,
+ contentType: "application/json",
+ dataType: "json",
+ complete: function(req) {
+ var resp = $.httpData(req, "json");
+ fun(resp);
+ connectToChanges(app, fun, resp.last_seq);
+ }
+ });
+ };
+ if (update_seq) {
+ changesReq(update_seq);
+ } else {
+ app.db.info({success: function(db_info) {
+ changesReq(db_info.update_seq);
+ }});
+ }
+ };
+
+})(jQuery);
346 vendor/couchapp/_attachments/jquery.mustache.js
@@ -0,0 +1,346 @@
+/*
+Shameless port of a shameless port
+@defunkt => @janl => @aq
+
+See http://github.com/defunkt/mustache for more info.
+*/
+
+;(function($) {
+
+/*
+ mustache.js — Logic-less templates in JavaScript
+
+ See http://mustache.github.com/ for more info.
+*/
+
+var Mustache = function() {
+ var Renderer = function() {};
+
+ Renderer.prototype = {
+ otag: "{{",
+ ctag: "}}",
+ pragmas: {},
+ buffer: [],
+ pragmas_implemented: {
+ "IMPLICIT-ITERATOR": true
+ },
+ context: {},
+
+ render: function(template, context, partials, in_recursion) {
+ // reset buffer & set context
+ if(!in_recursion) {
+ this.context = context;
+ this.buffer = []; // TODO: make this non-lazy
+ }
+
+ // fail fast
+ if(!this.includes("", template)) {
+ if(in_recursion) {
+ return template;
+ } else {
+ this.send(template);
+ return;
+ }
+ }
+
+ template = this.render_pragmas(template);
+ var html = this.render_section(template, context, partials);
+ if(in_recursion) {
+ return this.render_tags(html, context, partials, in_recursion);
+ }
+
+ this.render_tags(html, context, partials, in_recursion);
+ },
+
+ /*
+ Sends parsed lines
+ */
+ send: function(line) {
+ if(line != "") {
+ this.buffer.push(line);
+ }
+ },
+
+ /*
+ Looks for %PRAGMAS
+ */
+ render_pragmas: function(template) {
+ // no pragmas
+ if(!this.includes("%", template)) {
+ return template;
+ }
+
+ var that = this;
+ var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
+ this.ctag);
+ return template.replace(regex, function(match, pragma, options) {
+ if(!that.pragmas_implemented[pragma]) {
+ throw({message:
+ "This implementation of mustache doesn't understand the '" +
+ pragma + "' pragma"});
+ }
+ that.pragmas[pragma] = {};
+ if(options) {
+ var opts = options.split("=");
+ that.pragmas[pragma][opts[0]] = opts[1];
+ }
+ return "";
+ // ignore unknown pragmas silently
+ });
+ },
+
+ /*
+ Tries to find a partial in the curent scope and render it
+ */
+ render_partial: function(name, context, partials) {
+ name = this.trim(name);
+ if(!partials || partials[name] === undefined) {
+ throw({message: "unknown_partial '" + name + "'"});
+ }
+ if(typeof(context[name]) != "object") {
+ return this.render(partials[name], context, partials, true);
+ }
+ return this.render(partials[name], context[name], partials, true);
+ },
+
+ /*
+ Renders inverted (^) and normal (#) sections
+ */
+ render_section: function(template, context, partials) {
+ if(!this.includes("#", template) && !this.includes("^", template)) {
+ return template;
+ }
+
+ var that = this;
+ // CSW - Added "+?" so it finds the tighest bound, not the widest
+ var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
+ "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
+ "\\s*", "mg");
+
+ // for each {{#foo}}{{/foo}} section do...
+ return template.replace(regex, function(match, type, name, content) {
+ var value = that.find(name, context);
+ if(type == "^") { // inverted section
+ if(!value || that.is_array(value) && value.length === 0) {
+ // false or empty list, render it
+ return that.render(content, context, partials, true);
+ } else {
+ return "";
+ }
+ } else if(type == "#") { // normal section
+ if(that.is_array(value)) { // Enumerable, Let's loop!
+ return that.map(value, function(row) {
+ return that.render(content, that.create_context(row),
+ partials, true);
+ }).join("");
+ } else if(that.is_object(value)) { // Object, Use it as subcontext!
+ return that.render(content, that.create_context(value),
+ partials, true);
+ } else if(typeof value === "function") {
+ // higher order section
+ return value.call(context, content, function(text) {
+ return that.render(text, context, partials, true);
+ });
+ } else if(value) { // boolean section
+ return that.render(content, context, partials, true);
+ } else {
+ return "";
+ }
+ }
+ });
+ },
+
+ /*
+ Replace {{foo}} and friends with values from our view
+ */
+ render_tags: function(template, context, partials, in_recursion) {
+ // tit for tat
+ var that = this;
+
+ var new_regex = function() {
+ return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
+ that.ctag + "+", "g");
+ };
+
+ var regex = new_regex();
+ var tag_replace_callback = function(match, operator, name) {
+ switch(operator) {
+ case "!": // ignore comments
+ return "";
+ case "=": // set new delimiters, rebuild the replace regexp
+ that.set_delimiters(name);
+ regex = new_regex();
+ return "";
+ case ">": // render partial
+ return that.render_partial(name, context, partials);
+ case "{": // the triple mustache is unescaped
+ return that.find(name, context);
+ default: // escape the value
+ return that.escape(that.find(name, context));
+ }
+ };
+ var lines = template.split("\n");
+ for(var i = 0; i < lines.length; i++) {
+ lines[i] = lines[i].replace(regex, tag_replace_callback, this);
+ if(!in_recursion) {
+ this.send(lines[i]);
+ }
+ }
+
+ if(in_recursion) {
+ return lines.join("\n");
+ }
+ },
+
+ set_delimiters: function(delimiters) {
+ var dels = delimiters.split(" ");
+ this.otag = this.escape_regex(dels[0]);
+ this.ctag = this.escape_regex(dels[1]);
+ },
+
+ escape_regex: function(text) {
+ // thank you Simon Willison
+ if(!arguments.callee.sRE) {
+ var specials = [
+ '/', '.', '*', '+', '?', '|',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+ arguments.callee.sRE = new RegExp(
+ '(\\' + specials.join('|\\') + ')', 'g'
+ );
+ }
+ return text.replace(arguments.callee.sRE, '\\$1');
+ },
+
+ /*
+ find `name` in current `context`. That is find me a value
+ from the view object
+ */
+ find: function(name, context) {
+ name = this.trim(name);
+
+ // Checks whether a value is thruthy or false or 0
+ function is_kinda_truthy(bool) {
+ return bool === false || bool === 0 || bool;
+ }
+
+ var value;
+ if(is_kinda_truthy(context[name])) {
+ value = context[name];
+ } else if(is_kinda_truthy(this.context[name])) {
+ value = this.context[name];
+ }
+
+ if(typeof value === "function") {
+ return value.apply(context);
+ }
+ if(value !== undefined) {
+ return value;
+ }
+ // silently ignore unkown variables
+ return "";
+ },
+
+ // Utility methods
+
+ /* includes tag */
+ includes: function(needle, haystack) {
+ return haystack.indexOf(this.otag + needle) != -1;
+ },
+
+ /*
+ Does away with nasty characters
+ */
+ escape: function(s) {
+ s = String(s === null ? "" : s);
+ return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) {
+ switch(s) {
+ case "&": return "&amp;";
+ case "\\": return "\\\\";
+ case '"': return '\"';
+ case "<": return "&lt;";
+ case ">": return "&gt;";
+ default: return s;
+ }
+ });
+ },
+
+ // by @langalex, support for arrays of strings
+ create_context: function(_context) {
+ if(this.is_object(_context)) {
+ return _context;
+ } else {
+ var iterator = ".";
+ if(this.pragmas["IMPLICIT-ITERATOR"]) {
+ iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
+ }
+ var ctx = {};
+ ctx[iterator] = _context;
+ return ctx;
+ }
+ },
+
+ is_object: function(a) {
+ return a && typeof a == "object";
+ },
+
+ is_array: function(a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+ },
+
+ /*
+ Gets rid of leading and trailing whitespace
+ */
+ trim: function(s) {
+ return s.replace(/^\s*|\s*$/g, "");
+ },
+
+ /*
+ Why, why, why? Because IE. Cry, cry cry.
+ */
+ map: function(array, fn) {
+ if (typeof array.map == "function") {
+ return array.map(fn);
+ } else {
+ var r = [];
+ var l = array.length;
+ for(var i = 0; i < l; i++) {
+ r.push(fn(array[i]));
+ }
+ return r;
+ }
+ }
+ };
+
+ return({
+ name: "mustache.js",
+ version: "0.3.1-dev",
+
+ /*
+ Turns a template and view into HTML
+ */
+ to_html: function(template, view, partials, send_fun) {
+ var renderer = new Renderer();
+ if(send_fun) {
+ renderer.send = send_fun;
+ }
+ renderer.render(template, view, partials);
+ if(!send_fun) {
+ return renderer.buffer.join("\n");
+ }
+ },
+ escape : function(text) {
+ return new Renderer().escape(text);
+ }
+ });
+}();
+
+ $.mustache = function(template, view, partials) {
+ return Mustache.to_html(template, view, partials);
+ };
+
+ $.mustache.escape = function(text) {
+ return Mustache.escape(text);
+ };
+
+})(jQuery);
174 vendor/couchapp/_attachments/jquery.pathbinder.js
@@ -0,0 +1,174 @@
+(function($) {
+ // functions for handling the path
+ // thanks sammy.js
+ var PATH_REPLACER = "([^\/]+)",
+ PATH_NAME_MATCHER = /:([\w\d]+)/g,
+ QUERY_STRING_MATCHER = /\?([^#]*)$/,
+ SPLAT_MATCHER = /(\*)/,
+ SPLAT_REPLACER = "(.+)",
+ _currentPath,
+ _lastPath,
+ _pathInterval;
+
+ function hashChanged() {
+ _currentPath = getPath();
+ // if path is actually changed from what we thought it was, then react
+ if (_lastPath != _currentPath) {
+ _lastPath = _currentPath;
+ return triggerOnPath(_currentPath);
+ }
+ }
+
+ $.pathbinder = {
+ changeFuns : [],
+ paths : [],
+ begin : function(defaultPath) {
+ // this should trigger the defaultPath if there's not a path in the URL
+ // otherwise it should trigger the URL's path
+ $(function() {
+ var loadPath = getPath();
+ if (loadPath) {
+ triggerOnPath(loadPath);
+ } else {
+ goPath(defaultPath);
+ triggerOnPath(defaultPath);
+ }
+ })
+ },
+ go : function(path) {
+ goPath(path);
+ triggerOnPath(path);
+ },
+ currentPath : function() {
+ return getPath();
+ },
+ onChange : function (fun) {
+ $.pathbinder.changeFuns.push(fun);
+ }
+ };
+
+ function pollPath(every) {
+ function hashCheck() {
+ _currentPath = getPath();
+ // path changed if _currentPath != _lastPath
+ if (_lastPath != _currentPath) {
+ setTimeout(function() {
+ $(window).trigger('hashchange');
+ }, 1);
+ }
+ };
+ hashCheck();
+ _pathInterval = setInterval(hashCheck, every);
+ $(window).bind('unload', function() {
+ clearInterval(_pathInterval);
+ });
+ }
+
+ function triggerOnPath(path) {
+ path = path.replace(/^#/,'');
+ $.pathbinder.changeFuns.forEach(function(fun) {fun(path)});
+ var pathSpec, path_params, params = {}, param_name, param;
+ for (var i=0; i < $.pathbinder.paths.length; i++) {
+ pathSpec = $.pathbinder.paths[i];
+ // $.log("pathSpec", pathSpec);
+ if ((path_params = pathSpec.matcher.exec(path)) !== null) {
+ // $.log("path_params", path_params);
+ path_params.shift();
+ for (var j=0; j < path_params.length; j++) {
+ param_name = pathSpec.param_names[j];
+ param = decodeURIComponent(path_params[j]);
+ if (param_name) {
+ params[param_name] = param;
+ } else {
+ if (!params.splat) params.splat = [];
+ params.splat.push(param);
+ }
+ };
+ pathSpec.callback(params);
+ // return true; // removed this to allow for multi match
+ }
+ };
+ };
+
+ // bind the event
+ $(function() {
+ if ('onhashchange' in window) {
+ // we have a native event
+ } else {
+ pollPath(10);
+ }
+ // setTimeout(hashChanged,50);
+ $(window).bind('hashchange', hashChanged);
+ });
+
+ function registerPath(pathSpec) {
+ $.pathbinder.paths.push(pathSpec);
+ };
+
+ function setPath(pathSpec, params) {
+ var newPath = $.mustache(pathSpec.template, params);
+ goPath(newPath);
+ };
+
+ function goPath(newPath) {
+ if (newPath) {
+ // $.log("goPath", newPath)
+ window.location = '#'+newPath;
+ }
+ _lastPath = getPath();
+ };
+
+ function getPath() {
+ var matches = window.location.toString().match(/^[^#]*(#.+)$/);
+ return matches ? matches[1] : '';
+ };
+
+ function makePathSpec(path, callback) {
+ var param_names = [];
+ var template = "";
+
+ PATH_NAME_MATCHER.lastIndex = 0;
+
+ while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) {
+ param_names.push(path_match[1]);
+ }
+
+ return {
+ param_names : param_names,
+ matcher : new RegExp("^" + path.replace(
+ PATH_NAME_MATCHER, PATH_REPLACER).replace(
+ SPLAT_MATCHER, SPLAT_REPLACER) + "/?$"),
+ template : path.replace(PATH_NAME_MATCHER, function(a, b) {
+ return '{{'+b+'}}';
+ }).replace(SPLAT_MATCHER, '{{splat}}'),
+ callback : callback
+ };
+ };
+
+ $.fn.pathbinder = function(name, paths, options) {
+ options = options || {};
+ var self = $(this), pathList = paths.split(/\n/);
+ $.each(pathList, function() {
+ var path = this;
+ if (path) {
+ // $.log("bind path", path);
+ var pathSpec = makePathSpec(path, function(params) {
+ // $.log("path cb", name, path, self)
+ // $.log("trigger path: "+path+" params: ", params);
+ self.trigger(name, [params]);
+ });
+ // set the path when the event triggered through other means
+ if (options.bindPath) {
+ self.bind(name, function(ev, params) {
+ params = params || {};
+ // $.log("set path", name, pathSpec)
+ setPath(pathSpec, params);
+ });
+ }
+ // trigger when the path matches
+ registerPath(pathSpec);
+ }
+ });
+ };
+})(jQuery);
+
17 vendor/couchapp/_attachments/loader.js
@@ -0,0 +1,17 @@
+
+function couchapp_load(scripts) {
+ for (var i=0; i < scripts.length; i++) {
+ document.write('<script src="'+scripts[i]+'"><\/script>')
+ };
+};
+
+couchapp_load([
+ "/_utils/script/sha1.js",
+ "/_utils/script/json2.js",
+ "/_utils/script/jquery.js",
+ "/_utils/script/jquery.couch.js",
+ "vendor/couchapp/jquery.couch.app.js",
+ "vendor/couchapp/jquery.couch.app.util.js",
+ "vendor/couchapp/jquery.mustache.js",
+ "vendor/couchapp/jquery.evently.js"
+]);
22 vendor/couchapp/evently/README.md
@@ -0,0 +1,22 @@
+## Starting the Document this code challenge
+
+I need help on this code. I only have so many hours in the day. Please be liberal about patching and hacking (and sharing code!) so we can all benefit.
+
+Docs patches are deeply appreciated. For now you can just stick Markdown files in the Docs directory.
+
+# Evently
+
+These are some vendor Evently widgets that are running on the CouchApp system.
+
+## Account
+ This is how you signup, login and logout without worry about the code.
+ Todo, we could have this work against remote APIs like that Facebook stuff or whatever.
+
+
+## Profile
+ Use this to load the local users profile for the logged in user. Useful if you're going to be posting new messages. Most applications end up customizing `profile.profileReady` to render the primary data-entry form. This gets you benefits like refreshing on login / logout, etc, automatically.
+
+
+## Docs
+ This needs to be moved to it's own app.
+ I have this vision of a docs app designed for offline editing, that involves each Markdown paragraph being it's own document, with automatic use of Bespin for code samples. Any help on this would be thanked much.
16 vendor/couchapp/evently/account/_init.js
@@ -0,0 +1,16 @@
+function() {
+ var elem = $(this);
+ $$(this).userCtx = null;
+ $.couch.session({
+ success : function(r) {
+ var userCtx = r.userCtx;
+ if (userCtx.name) {
+ elem.trigger("loggedIn", [r]);
+ } else if (userCtx.roles.indexOf("_admin") != -1) {
+ elem.trigger("adminParty");
+ } else {
+ elem.trigger("loggedOut");
+ };
+ }
+ });
+}
1  vendor/couchapp/evently/account/adminParty/mustache.html
@@ -0,0 +1 @@
+<p><strong>Admin party, everyone is admin!</strong> Fix this in <a href="/_utils/index.html">Futon</a> before proceeding.</p>
10 vendor/couchapp/evently/account/doLogin.js
@@ -0,0 +1,10 @@
+function(e, name, pass) {
+ var elem = $(this);
+ $.couch.login({
+ name : name,
+ password : pass,
+ success : function(r) {
+ elem.trigger("_init")
+ }
+ });
+}
8 vendor/couchapp/evently/account/doLogout.js
@@ -0,0 +1,8 @@
+function() {
+ var elem = $(this);
+ $.couch.logout({
+ success : function() {
+ elem.trigger("_init");
+ }
+ });
+}
10 vendor/couchapp/evently/account/doSignup.js
@@ -0,0 +1,10 @@
+function(e, name, pass) {
+ var elem = $(this);
+ $.couch.signup({
+ name : name
+ }, pass, {
+ success : function() {
+ elem.trigger("doLogin", [name, pass]);
+ }
+ });
+}
4 vendor/couchapp/evently/account/loggedIn/after.js
@@ -0,0 +1,4 @@
+function(e, r) {
+ $$(this).userCtx = r.userCtx;
+ $$(this).info = r.info;
+};
7 vendor/couchapp/evently/account/loggedIn/data.js
@@ -0,0 +1,7 @@
+function(e, r) {
+ return {
+ name : r.userCtx.name,
+ uri_name : encodeURIComponent(r.userCtx.name),
+ auth_db : encodeURIComponent(r.info.authentication_db)
+ };
+}
4 vendor/couchapp/evently/account/loggedIn/mustache.html
@@ -0,0 +1,4 @@
+<span>Welcome
+<a target="_new" href="/_utils/document.html?{{auth_db}}/org.couchdb.user%3A{{uri_name}}">{{name}}</a>!
+<a href="#logout">Logout?</a>
+</span>
3  vendor/couchapp/evently/account/loggedIn/selectors.json
@@ -0,0 +1,3 @@
+{
+ "a[href=#logout]" : {"click" : ["doLogout"]}
+}
1  vendor/couchapp/evently/account/loggedOut/mustache.html
@@ -0,0 +1 @@
+<a href="#signup">Signup</a> or <a href="#login">Login</a>
4 vendor/couchapp/evently/account/loggedOut/selectors.json
@@ -0,0 +1,4 @@
+{
+ "a[href=#signup]" : {"click" : ["signupForm"]},
+ "a[href=#login]" : {"click" : ["loginForm"]}
+}
3  vendor/couchapp/evently/account/loginForm/after.js
@@ -0,0 +1,3 @@
+function() {
+ $("input[name=name]", this).focus();
+}
6 vendor/couchapp/evently/account/loginForm/mustache.html
@@ -0,0 +1,6 @@
+<form>
+ <label for="name">Name</label> <input type="text" name="name" value="" autocapitalize="off" autocorrect="off">
+ <label for="password">Password</label> <input type="password" name="password" value="">
+ <input type="submit" value="Login">
+ <a href="#signup">or Signup</a>
+</form>
6 vendor/couchapp/evently/account/loginForm/selectors/form/submit.js
@@ -0,0 +1,6 @@
+function(e) {
+ var name = $('input[name=name]', this).val(),
+ pass = $('input[name=password]', this).val();
+ $(this).trigger('doLogin', [name, pass]);
+ return false;
+}
3  vendor/couchapp/evently/account/signupForm/after.js
@@ -0,0 +1,3 @@
+function() {
+ $("input[name=name]", this).focus();
+}
6 vendor/couchapp/evently/account/signupForm/mustache.html
@@ -0,0 +1,6 @@
+<form>
+ <label for="name">Name</label> <input type="text" name="name" value="" autocapitalize="off" autocorrect="off">
+ <label for="password">Password</label> <input type="password" name="password" value="">
+ <input type="submit" value="Signup">
+ <a href="#login">or Login</a>
+</form>
6 vendor/couchapp/evently/account/signupForm/selectors/form/submit.js
@@ -0,0 +1,6 @@
+function(e) {
+ var name = $('input[name=name]', this).val(),
+ pass = $('input[name=password]', this).val();
+ $(this).trigger('doSignup', [name, pass]);
+ return false;
+}
21 vendor/couchapp/evently/profile/loggedIn.js
@@ -0,0 +1,21 @@
+function(e, r) {
+ var userCtx = r.userCtx;
+ var widget = $(this);
+ // load the profile from the user doc
+ var db = $.couch.db(r.info.authentication_db);
+ var userDocId = "org.couchdb.user:"+userCtx.name;
+ db.openDoc(userDocId, {
+ success : function(userDoc) {
+ var profile = userDoc["couch.app.profile"];
+ if (profile) {
+ // we copy the name to the profile so it can be used later
+ // without publishing the entire userdoc (roles, pass, etc)
+ profile.name = userDoc.name;
+ $$(widget).profile = profile;
+ widget.trigger("profileReady", [profile]);
+ } else {
+ widget.trigger("noProfile", [userCtx]);
+ }
+ }
+ });
+}
3  vendor/couchapp/evently/profile/loggedOut/after.js
@@ -0,0 +1,3 @@
+function() {
+ $$(this).profile = null;
+};
1  vendor/couchapp/evently/profile/loggedOut/mustache.html
@@ -0,0 +1 @@
+<p>Please log in to see your profile.</p>
3  vendor/couchapp/evently/profile/noProfile/data.js
@@ -0,0 +1,3 @@
+function(e, userCtx) {
+ return userCtx;
+}
11 vendor/couchapp/evently/profile/noProfile/mustache.html
@@ -0,0 +1,11 @@
+<form>
+ <p>Hello {{name}}, Please setup your user profile.</p>
+ <label for="nickname">Nickname
+ <input type="text" name="nickname" value=""></label>
+ <label for="email">Email (<em>for <a href="http://gravatar.com">Gravatar</a></em>)
+ <input type="text" name="email" value=""></label>
+ <label for="url">URL
+ <input type="text" name="url" value=""></label>
+ <input type="submit" value="Go &rarr;">
+ <input type="hidden" name="userCtxName" value="{{name}}" id="userCtxName">
+</form>
36 vendor/couchapp/evently/profile/noProfile/selectors/form/submit.js
@@ -0,0 +1,36 @@
+function() {
+ var md5 = $$(this).app.require("vendor/couchapp/lib/md5");
+
+ // TODO this can be cleaned up with docForm?
+ // it still needs the workflow to edit an existing profile
+ var name = $("input[name=userCtxName]",this).val();
+ var newProfile = {
+ rand : Math.random().toString(),
+ nickname : $("input[name=nickname]",this).val(),
+ email : $("input[name=email]",this).val(),
+ url : $("input[name=url]",this).val()
+ }, widget = $(this);
+
+ // setup gravatar_url
+ if (md5) {
+ newProfile.gravatar_url = 'http://www.gravatar.com/avatar/'+md5.hex(newProfile.email || newProfile.rand)+'.jpg?s=40&d=identicon';
+ }
+
+ // store the user profile on the user account document
+ $.couch.userDb(function(db) {
+ var userDocId = "org.couchdb.user:"+name;
+ db.openDoc(userDocId, {
+ success : function(userDoc) {
+ userDoc["couch.app.profile"] = newProfile;
+ db.saveDoc(userDoc, {
+ success : function() {
+ newProfile.name = userDoc.name;
+ $$(widget).profile = newProfile;
+ widget.trigger("profileReady", [newProfile]);
+ }
+ });
+ }
+ });
+ });
+ return false;
+}
3  vendor/couchapp/evently/profile/profileReady/after.js
@@ -0,0 +1,3 @@
+function(e, p) {
+ $$(this).profile = p;
+};
3  vendor/couchapp/evently/profile/profileReady/data.js
@@ -0,0 +1,3 @@
+function(e, p) {
+ return p
+}
8 vendor/couchapp/evently/profile/profileReady/mustache.html
@@ -0,0 +1,8 @@
+<div class="avatar">
+ {{#gravatar_url}}<img src="{{gravatar_url}}"/>{{/gravatar_url}}
+ <div class="name">
+ {{nickname}}
+ </div>
+</div>
+<p>Hello {{nickname}}!</p>
+<div style="clear:left;"></div>
39 vendor/couchapp/lib/atom.js
@@ -0,0 +1,39 @@
+// atom feed generator
+// requries E4X support.
+
+function f(n) { // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+}
+
+function rfc3339(date) {
+ return date.getUTCFullYear() + '-' +
+ f(date.getUTCMonth() + 1) + '-' +
+ f(date.getUTCDate()) + 'T' +
+ f(date.getUTCHours()) + ':' +
+ f(date.getUTCMinutes()) + ':' +
+ f(date.getUTCSeconds()) + 'Z';
+};
+
+exports.header = function(data) {
+ var f = <feed xmlns="http://www.w3.org/2005/Atom"/>;
+ f.title = data.title;
+ f.id = data.feed_id;
+ f.link.@href = data.feed_link;
+ f.link.@rel = "self";
+ f.generator = "CouchApp on CouchDB";
+ f.updated = rfc3339(data.updated);
+ return f.toXMLString().replace(/\<\/feed\>/,'');
+};
+
+exports.entry = function(data) {
+ var entry = <entry/>;
+ entry.id = data.entry_id;
+ entry.title = data.title;
+ entry.content = data.content;
+ entry.content.@type = (data.content_type || 'html');
+ entry.updated = rfc3339(data.updated);
+ entry.author = <author><name>{data.author}</name></author>;
+ entry.link.@href = data.alternate;
+ entry.link.@rel = "alternate";
+ return entry;
+}
25 vendor/couchapp/lib/cache.js
@@ -0,0 +1,25 @@
+exports.get = function(db, docid, setFun, getFun) {
+ db.openDoc(docid, {
+ success : function(doc) {
+ getFun(doc.cache);
+ },
+ error : function() {
+ setFun(function(cache) {
+ db.saveDoc({
+ _id : docid,
+ cache : cache
+ });
+ getFun(cache);
+ });
+ }
+ });
+};
+
+exports.clear = function(db, docid) {
+ db.openDoc(docid, {
+ success : function(doc) {
+ db.removeDoc(doc);
+ },
+ error : function() {}
+ });
+};
20 vendor/couchapp/lib/code.js
@@ -0,0 +1,20 @@
+exports.ddoc = function(ddoc) {
+ // only return the parts of the app that we use
+ var i, j, path, key, obj, ref, out = {},
+ resources = ddoc.couchapp && ddoc.couchapp.load && ddoc.couchapp.load.app || [];
+ for (i=0; i < resources.length; i++) {
+ path = resources[i].split('/');
+ obj = ddoc;
+ ref = out;
+ for (j=0; j < path.length; j++) {
+ key = path[j];
+ ref[key] = ref[key] || {};
+ if (j < path.length - 1) {
+ obj = obj[key];
+ ref = ref[key];
+ }
+ };
+ ref[key] = obj[key];
+ };
+ return out;
+};
121 vendor/couchapp/lib/docform.js
@@ -0,0 +1,121 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// turn the form into deep json
+// field names like 'author-email' get turned into json like
+// {"author":{"email":"quentin@example.com"}}
+// acts on doc by reference, so you can safely pass non-form fields through
+
+function docForm(formSelector, opts) {
+ var localFormDoc = {};
+ opts = opts || {};
+ opts.fields = opts.fields || [];
+
+ // turn the form into deep json
+ // field names like 'author-email' get turned into json like
+ // {"author":{"email":"quentin@example.com"}}
+ function formToDeepJSON(form, fields, doc) {
+ form = $(form);
+ fields.forEach(function(field) {
+ var element = form.find("[name="+field+"]"),
+ parts = field.split('-'),
+ frontObj = doc, frontName = parts.shift();
+
+ if (element.attr('type') === 'checkbox') {
+ var val = element.attr('checked');
+ } else {
+ var val = element.val();
+ if (!val) {
+ if (frontObj[field]) {
+ delete frontObj[field];
+ }
+ return;
+ }
+ }
+
+ while (parts.length > 0) {
+ frontObj[frontName] = frontObj[frontName] || {};
+ frontObj = frontObj[frontName];
+ frontName = parts.shift();
+ }
+ frontObj[frontName] = val;
+ });
+ }
+
+ // Apply the behavior
+ $(formSelector).submit(function(e) {
+ e.preventDefault();
+ if (opts.validate && opts.validate() == false) { return false;}
+ // formToDeepJSON acts on localFormDoc by reference
+ formToDeepJSON(this, opts.fields, localFormDoc);
+ if (opts.beforeSave) {opts.beforeSave(localFormDoc);}
+ db.saveDoc(localFormDoc, {
+ success : function(resp) {
+ if (opts.success) {opts.success(resp, localFormDoc);}
+ }
+ });
+
+ return false;
+ });
+
+ // populate form from an existing doc
+ function docToForm(doc) {
+ var form = $(formSelector);
+ // fills in forms
+ opts.fields.forEach(function(field) {
+ var parts = field.split('-');
+ var run = true, frontObj = doc, frontName = parts.shift();
+ while (frontObj && parts.length > 0) {
+ frontObj = frontObj[frontName];
+ frontName = parts.shift();
+ }
+ if (frontObj && frontObj[frontName]) {
+ var element = form.find("[name="+field+"]");
+ if (element.attr('type') === 'checkbox') {
+ element.attr('checked', frontObj[frontName]);
+ } else {
+ element.val(frontObj[frontName]);
+ }
+ }
+ });
+ }
+
+ if (opts.id) {
+ db.openDoc(opts.id, {
+ attachPrevRev : opts.attachPrevRev,
+ error: function() {
+ if (opts.error) {opts.error.apply(opts, arguments);}
+ },
+ success: function(doc) {
+ if (opts.load || opts.onLoad) {(opts.load || opts.onLoad)(doc);}
+ localFormDoc = doc;
+ docToForm(doc);
+ }});
+ } else if (opts.template) {
+ if (opts.load || opts.onLoad) {(opts.load || opts.onLoad)(opts.template);}
+ localFormDoc = opts.template;
+ docToForm(localFormDoc);
+ }
+ var instance = {
+ deleteDoc : function(opts) {
+ opts = opts || {};
+ if (confirm("Really delete this document?")) {
+ db.removeDoc(localFormDoc, opts);
+ }
+ },
+ localDoc : function() {
+ formToDeepJSON(formSelector, opts.fields, localFormDoc);
+ return localFormDoc;
+ }
+ };
+ return instance;
+}
18 vendor/couchapp/lib/linkup.js
@@ -0,0 +1,18 @@
+// this code makes http://example.com into a link,
+// and also handles @name and #hashtag
+
+// todo add [[wiki_links]]
+
+var mustache = require("vendor/couchapp/lib/mustache");
+exports.encode = function(body, person_prefix, tag_prefix) {
+ body = mustache.escape(body);
+ person_prefix = person_prefix || "http://twitter.com/";
+ tag_prefix = tag_prefix || "http://delicious.com/tag/";
+ return body.replace(/((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi,function(a) {
+ return '<a target="_blank" href="'+a+'">'+a+'</a>';
+ }).replace(/\@([\w\-]+)/g,function(user,name) {
+ return '<a href="'+person_prefix+encodeURIComponent(name)+'">'+user+'</a>';
+ }).replace(/\#([\w\-\.]+)/g,function(word,tag) {
+ return '<a href="'+tag_prefix+encodeURIComponent(tag)+'">'+word+'</a>';
+ });
+};
13 vendor/couchapp/lib/list.js
@@ -0,0 +1,13 @@
+// Helpers for writing server-side _list functions in CouchDB
+exports.withRows = function(fun) {
+ var f = function() {
+ var row = getRow();
+ return row && fun(row);
+ };
+ f.iterator = true;
+ return f;
+}
+
+exports.send = function(chunk) {
+ send(chunk + "\n")
+}
1,300 vendor/couchapp/lib/markdown.js
@@ -0,0 +1,1300 @@
+//
+// showdown.js -- A javascript port of Markdown.
+//
+// Copyright (c) 2007 John Fraser.
+//
+// Original Markdown Copyright (c) 2004-2005 John Gruber
+// <http://daringfireball.net/projects/markdown/>
+//
+// Redistributable under a BSD-style open source license.
+// See license.txt for more information.
+//
+// The full source distribution is at:
+//
+// A A L
+// T C A
+// T K B
+//
+// <http://www.attacklab.net/>
+//
+
+//
+// Wherever possible, Showdown is a straight, line-by-line port
+// of the Perl version of Markdown.
+//
+// This is not a normal parser design; it's basically just a
+// series of string substitutions. It's hard to read and
+// maintain this way, but keeping Showdown close to the original
+