Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

intial checkin

  • Loading branch information...
commit d0d4dd066612154a0aab458c6f0ba797334ee419 0 parents
@jchris authored
Showing with 4,026 additions and 0 deletions.
  1. +1 −0  .couchappignore
  2. +1 −0  .gitignore
  3. +40 −0 README.md
  4. +29 −0 _attachments/index.html
  5. +31 −0 _attachments/node/timeline.js
  6. +75 −0 _attachments/style/main.css
  7. +1 −0  _id
  8. +4 −0 couchapp.json
  9. +11 −0 evently/items/_changes/data.js
  10. +18 −0 evently/items/_changes/mustache.html
  11. +5 −0 evently/items/_changes/query.json
  12. +14 −0 evently/profile/profileReady/mustache.html
  13. +12 −0 evently/profile/profileReady/selectors/form/submit.js
  14. +1 −0  language
  15. +3 −0  vendor/couchapp/README.md
  16. +329 −0 vendor/couchapp/_attachments/jquery.couch.app.js
  17. +83 −0 vendor/couchapp/_attachments/jquery.couch.app.util.js
  18. +363 −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. +1 −0  vendor/couchapp/evently/account/loginForm/selectors/a[href=#signup].json
  37. +6 −0 vendor/couchapp/evently/account/loginForm/selectors/form/submit.js
  38. +3 −0  vendor/couchapp/evently/account/signupForm/after.js
  39. +6 −0 vendor/couchapp/evently/account/signupForm/mustache.html
  40. +1 −0  vendor/couchapp/evently/account/signupForm/selectors/a[href=#login].json
  41. +6 −0 vendor/couchapp/evently/account/signupForm/selectors/form/submit.js
  42. +21 −0 vendor/couchapp/evently/profile/loggedIn.js
  43. +3 −0  vendor/couchapp/evently/profile/loggedOut/after.js
  44. +1 −0  vendor/couchapp/evently/profile/loggedOut/mustache.html
  45. +3 −0  vendor/couchapp/evently/profile/noProfile/data.js
  46. +11 −0 vendor/couchapp/evently/profile/noProfile/mustache.html
  47. +36 −0 vendor/couchapp/evently/profile/noProfile/selectors/form/submit.js
  48. +3 −0  vendor/couchapp/evently/profile/profileReady/after.js
  49. +3 −0  vendor/couchapp/evently/profile/profileReady/data.js
  50. +8 −0 vendor/couchapp/evently/profile/profileReady/mustache.html
  51. +39 −0 vendor/couchapp/lib/atom.js
  52. +25 −0 vendor/couchapp/lib/cache.js
  53. +108 −0 vendor/couchapp/lib/docform.js
  54. +18 −0 vendor/couchapp/lib/linkup.js
  55. +13 −0 vendor/couchapp/lib/list.js
  56. +1,300 −0 vendor/couchapp/lib/markdown.js
  57. +261 −0 vendor/couchapp/lib/md5.js
  58. +339 −0 vendor/couchapp/lib/mustache.js
  59. +83 −0 vendor/couchapp/lib/path.js
  60. +8 −0 vendor/couchapp/lib/redirect.js
  61. +53 −0 vendor/couchapp/lib/validate.js
  62. +5 −0 vendor/couchapp/metadata.json
  63. +5 −0 views/recent-items/map.js
1  .couchappignore
@@ -0,0 +1 @@
+["config.json"]
1  .gitignore
@@ -0,0 +1 @@
+config.json
40 README.md
@@ -0,0 +1,40 @@
+## Generated by 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.
29 _attachments/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>My New CouchApp</title>
+ <link rel="stylesheet" href="style/main.css" type="text/css">
+ </head>
+ <body>
+ <div id="account"></div>
+
+ <h1>Generated CouchApp</h1>
+
+ <div id="profile"></div>
+ <div id="items"></div>
+
+ <div id="sidebar">
+ <p>Edit welcome message.</p>
+ <p>Ideas: You could easily turn this into a photo sharing app, or a grocery list, or a chat room.</p>
+ </div>
+ </body>
+ <script src="vendor/couchapp/loader.js"></script>
+ <script type="text/javascript" charset="utf-8">
+ $.couch.app(function(app) {
+ $("#account").evently("account", app);
+ $("#profile").evently("profile", app);
+ $.evently.connect("#account","#profile", ["loggedIn","loggedOut"]);
+ $("#items").evently("items", app);
+ });
+ </script>
+</html>
31 _attachments/node/timeline.js
@@ -0,0 +1,31 @@
+// based on the 'osb' test from @mikeal's tweetstream
+var tweetstream = require('tweetstream'),
+ fs = require('fs'),
+ path = require('path'),
+ request = require('request'),
+ sys = require('sys');
+
+var config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json')));
+
+
+var stream = tweetstream.createTweetStream({track:config.track,
+ username:config.username,
+ password:config.password});
+
+function addFriend(stream, user) {
+ // automatically friend anyone who mentions the term...
+ stream.rest.friendships.create({id:tweet.user.screen_name, user_id:tweet.user.id, follow:true},
+ function (error, resp, body) {
+ if (body) sys.puts(body);
+ });
+};
+
+stream.addListener("tweet", function (tweet) {
+
+ tweet._id = tweet.id.toString();
+ request({uri:couch, method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(tweet)},
+ function (e, resp, body) {
+ if (resp && resp.statusCode !== 201) sys.puts(sys.inspect(resp), sys.puts(body));
+ else {sys.puts(body)};
+ })
+});
75 _attachments/style/main.css
@@ -0,0 +1,75 @@
+/* add styles here */
+
+body {
+ font:1em Helvetica, sans-serif;
+ padding:4px;
+}
+
+h1 {
+ margin-top:0;
+}
+
+#account {
+ float:right;
+}
+
+#profile {
+ border:4px solid #edd;
+ background:#fee;
+ padding:8px;
+ margin-bottom:8px;
+}
+
+#items {
+ border:4px solid #dde;
+ background:#eef;
+ padding:8px;
+ width:60%;
+ float:left;
+}
+
+#sidebar {
+ border:4px solid #dfd;
+ padding:8px;
+ float:right;
+ width:30%;
+}
+
+#items li {
+ border:4px solid #f5f5ff;
+ background:#fff;
+ padding:8px;
+ margin:4px 0;
+}
+
+form {
+ padding:4px;
+ margin:6px;
+ background-color:#ddd;
+}
+
+div.avatar {
+ padding:2px;
+ padding-bottom:0;
+ margin-right:4px;
+ float:left;
+ font-size:0.78em;
+ width : 60px;
+ height : 60px;
+ text-align: center;
+}
+
+div.avatar .name {
+ padding-top:2px;
+}
+
+div.avatar img {
+ margin:0 auto;
+ padding:0;
+ width : 40px;
+ height : 40px;
+}
+
+#items ul {
+ list-style: none;
+}
1  _id
@@ -0,0 +1 @@
+_design/tweb
4 couchapp.json
@@ -0,0 +1,4 @@
+{
+ "name": "Name of your CouchApp",
+ "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
3  vendor/couchapp/README.md
@@ -0,0 +1,3 @@
+## CouchApp - more than just a filesystem mapper
+
+This is where documentation will go for the client and server JavaScript parts of CouchApp.
329 vendor/couchapp/_attachments/jquery.couch.app.js
@@ -0,0 +1,329 @@
+// 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) {
+ this.doc_id = "_design/"+name;
+ this.view = function(view, opts) {
+ db.view(name+'/'+view, opts);
+ };
+ this.list = function(list, view, opts) {
+ db.list(name+'/'+list, view, opts);
+ };
+ }
+
+ $.couch.app = $.couch.app || function(appFun, opts) {
+ opts = opts || {};
+ $(function() {
+ var urlPrefix = opts.urlPrefix || "";
+ $.couch.urlPrefix = urlPrefix;
+
+ var index = urlPrefix.split('/').length;
+ var fragments = unescape(document.location.href).split('/');
+ var dbname = opts.db || fragments[index + 2];
+ var dname = opts.design || fragments[index + 4];
+
+ var db = $.couch.db(dbname);
+ var design = new Design(db, dname);
+
+ // docForm applies CouchDB behavior to HTML forms.
+ // todo make this a couch.app plugin
+ 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+"]");
+ if (element.attr('type') === 'checkbox') {
+ var val = element.attr('checked');
+ } else {
+ var val = element.val();
+ if (!val) return;
+ }
+ var parts = field.split('-');
+ var frontObj = doc, frontName = parts.shift();
+ 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;
+ }
+
+ function resolveModule(names, parent, current) {
+ if (names.length === 0) {
+ if (typeof current != "string") {
+ throw ["error","invalid_require_path",
+ 'Must require a JavaScript string, not: '+(typeof current)];
+ }
+ return [current, parent];
+ }
+ // we need to traverse the path
+ var n = names.shift();
+ if (n == '..') {
+ if (!(parent && parent.parent)) {
+ throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)];
+ }
+ return resolveModule(names, parent.parent.parent, parent.parent);
+ } else if (n == '.') {
+ if (!parent) {
+ throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)];
+ }
+ return resolveModule(names, parent.parent, parent);
+ }
+ if (!current[n]) {
+ throw ["error", "invalid_require_path", 'Object has no property "'+n+'". '+JSON.stringify(current)];
+ }
+ var p = current;
+ current = current[n];
+ current.parent = p;
+ return resolveModule(names, p, current);
+ }
+
+ var p = document.location.pathname.split('/');
+ p.shift();
+ var qs = document.location.search.replace(/^\?/,'').split('&');
+ var q = {};
+ qs.forEach(function(param) {
+ var ps = param.split('=');
+ var k = decodeURIComponent(ps[0]);
+ var v = decodeURIComponent(ps[1]);
+ if (["startkey", "endkey", "key"].indexOf(k) != -1) {
+ q[k] = JSON.parse(v);
+ } else {
+ q[k] = v;
+ }
+ });
+ var mockReq = {
+ path : p,
+ query : q
+ };
+
+ var appExports = $.extend({
+ db : db,
+ design : design,
+ view : design.view,
+ list : design.list,
+ docForm : docForm,
+ req : mockReq
+ }, $.couch.app.app);
+
+ function handleDDoc(ddoc) {
+ var moduleCache = [];
+
+ function getCachedModule(name, parent) {
+ var key, i, len = moduleCache.length;
+ for (i=0;i<len;++i) {
+ key = moduleCache[i].key;
+ if (key[0] === name && key[1] === parent) {
+ return moduleCache[i].module;
+ }
+ }
+
+ return null;
+ }
+
+ function setCachedModule(name, parent, module) {
+ moduleCache.push({ key: [name, parent], module: module });
+ }
+
+ if (ddoc) {
+ var require = function(name, parent) {
+ var cachedModule = getCachedModule(name, parent);
+ if (cachedModule !== null) {
+ return cachedModule;
+ }
+
+ var exports = {};
+ var resolved = resolveModule(name.split('/'), parent, ddoc);
+ var source = resolved[0];
+ parent = resolved[1];
+ var s = "var func = function (exports, require) { " + source + " };";
+ try {
+ eval(s);
+ func.apply(ddoc, [exports, function(name) {return require(name, parent, source)}]);
+ } catch(e) {
+ throw ["error","compilation_error","Module require('"+name+"') raised error "+e.toSource()];
+ }
+
+ setCachedModule(name, parent, exports);
+
+ return exports;
+ }
+ appExports.ddoc = ddoc;
+ appExports.require = require;
+ }
+ // todo make app-exports the this in the execution context?
+ appFun.apply(appExports, [appExports]);
+ }
+
+ if ($.couch.app.ddocs[design.doc_id]) {
+ 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];
+ db.openDoc(design.doc_id, {
+ success : function(doc) {
+ $.couch.app.ddocs[design.doc_id] = doc;
+ $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) {
+ h(doc);
+ });
+ $.couch.app.ddoc_handlers[design.doc_id] = null;
+ },
+ error : function() {
+ $.couch.app.ddoc_handlers[design.doc_id].forEach(function(h) {
+ 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;
+ };
+}
83 vendor/couchapp/_attachments/jquery.couch.app.util.js
@@ -0,0 +1,83 @@
+$.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() {
+ $(this).text($.prettyDate($(this).text()));
+ });
+};
+
+$.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" ||
+ 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;
+}
363 vendor/couchapp/_attachments/jquery.evently.js
@@ -0,0 +1,363 @@
+// $$ 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) {
+ 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});
+ 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);
+ 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, {}];
+ $.forIn(ddoc.vendor, function(k, v) {
+ if (v.evently && v.evently[name]) {
+ events.push(v.evently[name]);
+ }
+ });
+ if (ddoc.evently[name]) {events.push(ddoc.evently[name]);}
+ return $.extend.apply(null, 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);
+ }
+
+ $$(elem).evently = events;
+ // setup the handlers onto elem
+ forIn(events, function(name, h) {
+ eventlyHandler(elem, name, h, args);
+ });
+
+ if (events._init) {
+ // $.log("ev _init", elem);
+ elem.trigger("_init", args);
+ }
+
+ if (app && events._changes) {
+ $("body").bind("evently-changes-"+app.db.name, function() {
+ // we want to unbind this function when the element is deleted.
+ // maybe jquery 1.4.2 has this covered?
+ // $.log('changes', elem);
+ 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 (h.path) {
+ elem.pathbinder(name, h.path);
+ }
+ var f = funViaString(h);
+ 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) {
+ elem.bind(name, {args:args}, funViaString(h.fun));
+ }
+ // 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).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);
+ // funViaString(h.after).apply(me, args);
+ }
+ }
+ };
+
+ // todo this should return the new element
+ function mustachioed(me, h, args) {
+ return $($.mustache(
+ runIfFun(me, h.mustache, args),
+ runIfFun(me, h.data, args),
+ runIfFun(me, h.partials, args)));
+ };
+
+ function runAsync(me, h, args) {
+ // the callback is the first argument
+ funViaString(h.async).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) {
+ 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="">
+ <label for="password">Password</label> <input type="password" name="password" value="">
+ <input type="submit" value="Login">
+ <a href="#signup">or Signup</a>
+</form>
1  vendor/couchapp/evently/account/loginForm/selectors/a[href=#signup].json
@@ -0,0 +1 @@
+{"click" : ["signupForm"]}
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="">
+ <label for="password">Password</label> <input type="password" name="password" value="">
+ <input type="submit" value="Signup">
+ <a href="#login">or Login</a>
+</form>
1  vendor/couchapp/evently/account/signupForm/selectors/a[href=#login].json
@@ -0,0 +1 @@
+{"click" : ["loginForm"]}
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() {}
+ });
+};
108 vendor/couchapp/lib/docform.js
@@ -0,0 +1,108 @@
+// 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 formToDeepJSON(form, fields, doc) {
+ form = $(form);
+ fields.forEach(function(field) {
+ var val = form.find("[name="+field+"]").val();
+ if (!val) {return;}
+ var parts = field.split('-');
+ var frontObj = doc, frontName = parts.shift();
+ while (parts.length > 0) {
+ frontObj[frontName] = frontObj[frontName] || {};
+ frontObj = frontObj[frontName];
+ frontName = parts.shift();
+ }
+ frontObj[frontName] = val;
+ });
+}
+
+function onSubmit(form, db, doc, opts) {
+ formToDeepJSON(form, opts.fields, doc);
+ if (opts.beforeSave) {opts.beforeSave(doc);}
+ db.saveDoc(localFormDoc, {
+ success : function(resp) {
+ if (opts.success) {opts.success(resp, doc);}
+ }
+ });
+};
+
+function applyFields(form, doc) {
+
+};
+exports.applyFields = applyFields;
+
+// docForm applies CouchDB behavior to HTML forms.
+// todo make this a couch.app plugin
+function docForm(formSelector, opts) {
+ var localFormDoc = {};
+ opts = opts || {};
+ opts.fields = opts.fields || [];
+
+ // Apply the behavior
+ $(formSelector).submit(function(e) {
+
+
+ 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]) {
+ form.find("[name="+field+"]").val(frontObj[frontName]);
+ }
+ });
+ }
+
+ if (opts.id) {
+ db.openDoc(opts.id, {
+ success: function(doc) {
+ if (opts.onLoad) {opts.onLoad(doc);}
+ localFormDoc = doc;
+ docToForm(doc);
+ }});
+ } else if (opts.template) {
+ if (opts.onLoad) {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.toLowerCase())+'">'+user+'</a>';
+ }).replace(/\#([\w\-\.]+)/g,function(word,tag) {
+ return '<a href="'+tag_prefix+encodeURIComponent(tag.toLowerCase())+'">'+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
+// design makes it easier to port new features.
+//
+// More importantly, Showdown behaves like markdown.pl in most
+// edge cases. So web applications can do client-side preview
+// in Javascript, and then build identical HTML on the server.
+//
+// This port needs the new RegExp functionality of ECMA 262,
+// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers
+// should do fine. Even with the new regular expression features,
+// We do a lot of work to emulate Perl's regex functionality.
+// The tricky changes in this file mostly have the "attacklab:"
+// label. Major or self-explanatory changes don't.
+//
+// Smart diff tools like Araxis Merge will be able to match up
+// this file with markdown.pl in a useful way. A little tweaking
+// helps: in a copy of markdown.pl, replace "#" with "//" and
+// replace "$text" with "text". Be sure to ignore whitespace
+// and line endings.
+//
+
+
+//
+// Showdown usage:
+//
+// var text = "Markdown *rocks*.";
+//
+// var markdown = require("markdown");
+// var html = markdown.encode(text);
+//
+// print(html);
+//
+// Note: move the sample code to the bottom of this
+// file before uncommenting it.
+//
+
+
+//
+// Globals:
+//
+
+// Global hashes, used by various utility routines
+var g_urls;
+var g_titles;
+var g_html_blocks;
+
+// Used to track when we're inside an ordered or unordered list
+// (see _ProcessListItems() for details):
+var g_list_level = 0;
+
+
+exports.makeHtml = function(text) {
+//
+// Main function. The order in which other subs are called here is
+// essential. Link and image substitutions need to happen before
+// _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the <a>
+// and <img> tags get encoded.
+//
+
+ // Clear the global hashes. If we don't clear these, you get conflicts
+ // from other articles when generating a page which contains more than
+ // one article (e.g. an index page that shows the N most recent
+ // articles):
+ g_urls = new Array();
+ g_titles = new Array();
+ g_html_blocks = new Array();
+
+ // attacklab: Replace ~ with ~T
+ // This lets us use tilde as an escape char to avoid md5 hashes
+ // The choice of character is arbitray; anything that isn't
+ // magic in Markdown will work.
+ text = text.replace(/~/g,"~T");
+
+ // attacklab: Replace $ with ~D
+ // RegExp interprets $ as a special character
+ // when it's in a replacement string
+ text = text.replace(/\$/g,"~D");
+
+ // Standardize line endings
+ text = text.replace(/\r\n/g,"\n"); // DOS to Unix
+ text = text.replace(/\r/g,"\n"); // Mac to Unix
+
+ // Make sure text begins and ends with a couple of newlines:
+ text = "\n\n" + text + "\n\n";
+
+ // Convert all tabs to spaces.
+ text = _Detab(text);
+
+ // Strip any lines consisting only of spaces and tabs.
+ // This makes subsequent regexen easier to write, because we can
+ // match consecutive blank lines with /\n+/ instead of something
+ // contorted like /[ \t]*\n+/ .
+ text = text.replace(/^[ \t]+$/mg,"");
+
+ // Turn block-level HTML blocks into hash entries
+ text = _HashHTMLBlocks(text);
+