├── .gitignore ├── views ├── tags │ ├── reduce.js │ └── map.js ├── recent-posts │ └── map.js ├── comments │ └── map.js ├── post-page │ └── map.js └── lib │ └── comments.js ├── blog.json ├── couchapp.json ├── evently ├── tagcloud │ └── _init │ │ ├── query.json │ │ ├── mustache.html │ │ └── data.js ├── profile │ ├── loggedOut │ │ └── mustache.html │ └── profileReady │ │ ├── selectors │ │ ├── form │ │ │ └── submit.js │ │ └── #preview │ │ │ └── click.js │ │ └── mustache.html └── account │ └── loggedIn │ ├── mustache.html │ └── data.js ├── vendor ├── couchapp │ ├── evently │ │ ├── account │ │ │ ├── loginForm │ │ │ │ ├── selectors │ │ │ │ │ ├── a[href=#signup].json │ │ │ │ │ └── form │ │ │ │ │ │ └── submit.js │ │ │ │ ├── after.js │ │ │ │ └── mustache.html │ │ │ ├── signupForm │ │ │ │ ├── selectors │ │ │ │ │ ├── a[href=#login].json │ │ │ │ │ └── form │ │ │ │ │ │ └── submit.js │ │ │ │ ├── after.js │ │ │ │ └── mustache.html │ │ │ ├── loggedIn │ │ │ │ ├── selectors.json │ │ │ │ ├── after.js │ │ │ │ ├── mustache.html │ │ │ │ └── data.js │ │ │ ├── loggedOut │ │ │ │ ├── mustache.html │ │ │ │ └── selectors.json │ │ │ ├── adminParty │ │ │ │ └── mustache.html │ │ │ ├── doLogout.js │ │ │ ├── doLogin.js │ │ │ ├── doSignup.js │ │ │ └── _init.js │ │ ├── profile │ │ │ ├── loggedOut │ │ │ │ ├── mustache.html │ │ │ │ └── after.js │ │ │ ├── profileReady │ │ │ │ ├── data.js │ │ │ │ ├── after.js │ │ │ │ └── mustache.html │ │ │ ├── noProfile │ │ │ │ ├── data.js │ │ │ │ ├── mustache.html │ │ │ │ └── selectors │ │ │ │ │ └── form │ │ │ │ │ └── submit.js │ │ │ └── loggedIn.js │ │ └── README.md │ ├── metadata.json │ ├── lib │ │ ├── redirect.js │ │ ├── list.js │ │ ├── cache.js │ │ ├── code.js │ │ ├── linkup.js │ │ ├── atom.js │ │ ├── validate.js │ │ ├── path.js │ │ ├── docform.js │ │ ├── md5.js │ │ ├── mustache.js │ │ └── markdown.js │ └── _attachments │ │ ├── loader.js │ │ ├── jquery.couch.app.util.js │ │ ├── jquery.pathbinder.js │ │ ├── jquery.couch.app.js │ │ ├── jquery.mustache.js │ │ └── jquery.evently.js └── textile │ └── textile.js ├── _attachments ├── images │ └── icon.png ├── script │ ├── app.js │ ├── jquery.scrollTo.js │ └── md5.js ├── THANKS.txt ├── style │ └── screen.css └── LICENSE.txt ├── lib ├── blog.js ├── validate.js └── mustache.js ├── shows ├── post.js └── edit.js ├── templates ├── partials │ ├── comment.html │ ├── header.html │ └── scripts.html ├── post.html ├── index.html └── edit.html ├── THANKS.txt ├── rewrites.json ├── sofa2.txt ├── validate_doc_update.js ├── lists ├── comments.js ├── post.js └── index.js ├── README.md └── helpers └── md5.js /.gitignore: -------------------------------------------------------------------------------- 1 | .couchapprc -------------------------------------------------------------------------------- /views/tags/reduce.js: -------------------------------------------------------------------------------- 1 | _count -------------------------------------------------------------------------------- /blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sofa: CouchApp Blog Engine" 3 | } -------------------------------------------------------------------------------- /couchapp.json: -------------------------------------------------------------------------------- 1 | { 2 | "index" : "_list/index/recent-posts?descending=true&limit=10" 3 | } 4 | -------------------------------------------------------------------------------- /evently/tagcloud/_init/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "view" : "tags", 3 | "group_level" : 1 4 | } 5 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/selectors/a[href=#signup].json: -------------------------------------------------------------------------------- 1 | {"click" : ["signupForm"]} -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/selectors/a[href=#login].json: -------------------------------------------------------------------------------- 1 | {"click" : ["loginForm"]} -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 |

Please log in to see your profile.

-------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/data.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | return p 3 | } 4 | -------------------------------------------------------------------------------- /_attachments/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchris/sofa/HEAD/_attachments/images/icon.png -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/loggedOut/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $$(this).profile = null; 3 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/data.js: -------------------------------------------------------------------------------- 1 | function(e, userCtx) { 2 | return userCtx; 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/after.js: -------------------------------------------------------------------------------- 1 | function(e, p) { 2 | $$(this).profile = p; 3 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "a[href=#logout]" : {"click" : ["doLogout"]} 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 | Signup or Login -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("input[name=name]", this).focus(); 3 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/after.js: -------------------------------------------------------------------------------- 1 | function() { 2 | $("input[name=name]", this).focus(); 3 | } -------------------------------------------------------------------------------- /evently/profile/loggedOut/mustache.html: -------------------------------------------------------------------------------- 1 |
2 |

Please login (above) to leave comments.

3 |
4 | -------------------------------------------------------------------------------- /evently/tagcloud/_init/mustache.html: -------------------------------------------------------------------------------- 1 | {{#tags}} 2 | {{tag}} 3 | {{/tags}} -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/after.js: -------------------------------------------------------------------------------- 1 | function(e, r) { 2 | $$(this).userCtx = r.userCtx; 3 | $$(this).info = r.info; 4 | }; -------------------------------------------------------------------------------- /views/recent-posts/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.type == "post") { 3 | emit(new Date(doc.created_at), doc); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /vendor/couchapp/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "couchapp", 3 | "description": "official couchapp vendor", 4 | "fetch_uri": "git://github.com/couchapp/couchapp.git" 5 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedOut/selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "a[href=#signup]" : {"click" : ["signupForm"]}, 3 | "a[href=#login]" : {"click" : ["loginForm"]} 4 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/adminParty/mustache.html: -------------------------------------------------------------------------------- 1 |

Admin party, everyone is admin! Fix this in Futon before proceeding.

-------------------------------------------------------------------------------- /lib/blog.js: -------------------------------------------------------------------------------- 1 | exports.slugifyString = function(string) { 2 | return string.replace(/\W/g,'-'). 3 | replace(/\-*$/,'').replace(/^\-*/,''). 4 | replace(/\-{2,}/,'-'); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/redirect.js: -------------------------------------------------------------------------------- 1 | exports.permanent = function(redirect) { 2 | return { 3 | code : 301, 4 | headers : { 5 | "Location" : redirect 6 | } 7 | }; 8 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doLogout.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var elem = $(this); 3 | $.couch.logout({ 4 | success : function() { 5 | elem.trigger("_init"); 6 | } 7 | }); 8 | } -------------------------------------------------------------------------------- /views/comments/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | var comments = require("views/lib/comments"); 3 | 4 | if (doc.type == "comment") { 5 | emit(new Date(doc.created_at), comments.withGravatar(doc)); 6 | } 7 | }; -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/mustache.html: -------------------------------------------------------------------------------- 1 | Welcome 2 | {{name}}! 3 | Logout? 4 | -------------------------------------------------------------------------------- /_attachments/script/app.js: -------------------------------------------------------------------------------- 1 | $.couch.app(function(app) { 2 | $('.date').prettyDate(); 3 | 4 | $("#account").evently($.extend(true, 5 | app.ddoc.vendor.couchapp.evently.account, 6 | app.ddoc.evently.account), app); 7 | }); -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loggedIn/data.js: -------------------------------------------------------------------------------- 1 | function(e, r) { 2 | return { 3 | name : r.userCtx.name, 4 | uri_name : encodeURIComponent(r.userCtx.name), 5 | auth_db : encodeURIComponent(r.info.authentication_db) 6 | }; 7 | } -------------------------------------------------------------------------------- /shows/post.js: -------------------------------------------------------------------------------- 1 | function(doc, req) { 2 | var path = require("vendor/couchapp/lib/path").init(req); 3 | var redirect = require("vendor/couchapp/lib/redirect"); 4 | return redirect.permanent(path.list('post','post-page', {startkey : [doc._id]})); 5 | } 6 | -------------------------------------------------------------------------------- /templates/partials/comment.html: -------------------------------------------------------------------------------- 1 |
  • 2 |

    3 | by {{#url}}{{/url}}{{name}}{{#url}}{{/url}}, 4 | {{created_at}} 5 |

    6 | 7 |

    {{{html}}}

    8 |
  • -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doLogin.js: -------------------------------------------------------------------------------- 1 | function(e, name, pass) { 2 | var elem = $(this); 3 | $.couch.login({ 4 | name : name, 5 | password : pass, 6 | success : function(r) { 7 | elem.trigger("_init") 8 | } 9 | }); 10 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/doSignup.js: -------------------------------------------------------------------------------- 1 | function(e, name, pass) { 2 | var elem = $(this); 3 | $.couch.signup({ 4 | name : name 5 | }, pass, { 6 | success : function() { 7 | elem.trigger("doLogin", [name, pass]); 8 | } 9 | }); 10 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var name = $('input[name=name]', this).val(), 3 | pass = $('input[name=password]', this).val(); 4 | $(this).trigger('doLogin', [name, pass]); 5 | return false; 6 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function(e) { 2 | var name = $('input[name=name]', this).val(), 3 | pass = $('input[name=password]', this).val(); 4 | $(this).trigger('doSignup', [name, pass]); 5 | return false; 6 | } -------------------------------------------------------------------------------- /views/tags/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if(doc.type == "post" && doc.tags && doc.tags.length) { 3 | for(var idx in doc.tags) { 4 | if (doc.tags[idx]){ 5 | emit([doc.tags[idx].toLowerCase(), doc.created_at], doc); 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/profileReady/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | {{#gravatar_url}}{{/gravatar_url}} 3 |
    4 | {{nickname}} 5 |
    6 |
    7 |

    Hello {{nickname}}!

    8 |
    -------------------------------------------------------------------------------- /views/post-page/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | var comments = require("views/lib/comments"); 3 | 4 | if (doc.type == "post") { 5 | emit([doc._id], doc); 6 | } else if (doc.type == "comment") { 7 | emit([doc.post_id, doc.created_at], comments.withGravatar(doc)); 8 | } 9 | }; -------------------------------------------------------------------------------- /_attachments/THANKS.txt: -------------------------------------------------------------------------------- 1 | Sofa THANKS 2 | ===================== 3 | 4 | A number of people have contributed to Sofa by reporting problems, 5 | suggesting improvements, submitting changes or asking hard question 6 | 7 | Some of these people are: 8 | 9 | * Andy Wenk -------------------------------------------------------------------------------- /evently/account/loggedIn/mustache.html: -------------------------------------------------------------------------------- 1 | Welcome 2 | {{name}}! 3 | Logout? 4 | {{#postPath}} 5 | {{postMessage}} 6 | {{/postPath}} 7 | -------------------------------------------------------------------------------- /templates/partials/header.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /THANKS.txt: -------------------------------------------------------------------------------- 1 | Sofa THANKS 2 | ===================== 3 | 4 | A number of people have contributed to Sofa by reporting problems, 5 | suggesting improvements, submitting changes or asking hard questions. 6 | 7 | Some of these people are: 8 | 9 | * Andy Wenk 10 | * Jan Lehnardt 11 | * You -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/loginForm/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | or Signup 6 |
    7 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/signupForm/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | or Login 6 |
    7 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/list.js: -------------------------------------------------------------------------------- 1 | // Helpers for writing server-side _list functions in CouchDB 2 | exports.withRows = function(fun) { 3 | var f = function() { 4 | var row = getRow(); 5 | return row && fun(row); 6 | }; 7 | f.iterator = true; 8 | return f; 9 | } 10 | 11 | exports.send = function(chunk) { 12 | send(chunk + "\n") 13 | } -------------------------------------------------------------------------------- /vendor/couchapp/evently/account/_init.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var elem = $(this); 3 | $$(this).userCtx = null; 4 | $.couch.session({ 5 | success : function(r) { 6 | var userCtx = r.userCtx; 7 | if (userCtx.name) { 8 | elem.trigger("loggedIn", [r]); 9 | } else if (userCtx.roles.indexOf("_admin") != -1) { 10 | elem.trigger("adminParty"); 11 | } else { 12 | elem.trigger("loggedOut"); 13 | }; 14 | } 15 | }); 16 | } -------------------------------------------------------------------------------- /rewrites.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from" : "", 4 | "to" : "_list/index/recent-posts", 5 | "query" : { 6 | "descending" : "true", 7 | "limit" : "10" 8 | } 9 | }, 10 | { 11 | "from" : "script/*", 12 | "to" : "script/*" 13 | }, 14 | { 15 | "from" : "style/*", 16 | "to" : "style/*" 17 | }, 18 | { 19 | "from" : "vendor/*", 20 | "to" : "vendor/*" 21 | }, 22 | { 23 | "from" : ":db/*", 24 | "to" : "../../*" 25 | } 26 | ] -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/mustache.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Hello {{name}}, Please setup your user profile.

    3 | 5 | 7 | 9 | 10 | 11 |
    -------------------------------------------------------------------------------- /sofa2.txt: -------------------------------------------------------------------------------- 1 | backport Mustache changes 2 | test suite 3 | link code 4 | ability to share link code between client and server 5 | add a name key to the rewriter paths 6 | 7 | docform as it's own plugin 8 | 9 | prune dead code 10 | 11 | 12 | 13 | 14 | screens 15 | recent posts 16 | posts by tag 17 | permalink page 18 | edit page 19 | recent comments 20 | 21 | 22 | 23 | 24 | relaxville 25 | # copy couchapp to sofa -> 26 | urls for links (sammy.js) 27 | login / logout 28 | (test suite report?) 29 | couchapp github project 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /evently/profile/profileReady/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var f = $(this), app = $$(this).app; 3 | var newComment = { 4 | type : "comment", 5 | post_id : app.post_id, 6 | format : "markdown", 7 | comment : $("[name=comment]", f).val(), 8 | commenter : $$("#profile").profile, 9 | created_at : new Date() 10 | }; 11 | app.db.saveDoc(newComment, { 12 | success : function(resp) { 13 | $("#new-comment").html('

    Success! Your comment has posted.

    Refresh the page to see it.

    '); 14 | } 15 | }); 16 | 17 | return false; 18 | }; 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/partials/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/loader.js: -------------------------------------------------------------------------------- 1 | 2 | function couchapp_load(scripts) { 3 | for (var i=0; i < scripts.length; i++) { 4 | document.write(' 34 | -------------------------------------------------------------------------------- /shows/edit.js: -------------------------------------------------------------------------------- 1 | function(doc, req) { 2 | var ddoc = this; 3 | var Mustache = require("lib/mustache"); 4 | var path = require("vendor/couchapp/lib/path").init(req); 5 | 6 | var indexPath = path.list('index','recent-posts',{descending:true, limit:10}); 7 | var feedPath = path.list('index','recent-posts',{descending:true, limit:10, format:"atom"}); 8 | var commentsFeed = path.list('comments','comments',{descending:true, limit:10, format:"atom"}); 9 | 10 | var data = { 11 | header : { 12 | index : indexPath, 13 | blogName : ddoc.blog.title, 14 | feedPath : feedPath, 15 | commentsFeed : commentsFeed 16 | }, 17 | scripts : {}, 18 | pageTitle : doc ? "Edit: "+doc.title : "Create a new post", 19 | assets : path.asset() 20 | }; 21 | 22 | if (doc) { 23 | data.doc = JSON.stringify(doc); 24 | data.title = doc.title; 25 | data.body = doc.body; 26 | data.tags = doc.tags.join(", "); 27 | } else { 28 | data.doc = JSON.stringify({ 29 | type : "post", 30 | format : "markdown" 31 | }); 32 | } 33 | 34 | return Mustache.to_html(ddoc.templates.edit, data, ddoc.templates.partials); 35 | } 36 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/README.md: -------------------------------------------------------------------------------- 1 | ## Starting the Document this code challenge 2 | 3 | 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. 4 | 5 | Docs patches are deeply appreciated. For now you can just stick Markdown files in the Docs directory. 6 | 7 | # Evently 8 | 9 | These are some vendor Evently widgets that are running on the CouchApp system. 10 | 11 | ## Account 12 | This is how you signup, login and logout without worry about the code. 13 | Todo, we could have this work against remote APIs like that Facebook stuff or whatever. 14 | 15 | 16 | ## Profile 17 | 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. 18 | 19 | 20 | ## Docs 21 | This needs to be moved to it's own app. 22 | 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. 23 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/atom.js: -------------------------------------------------------------------------------- 1 | // atom feed generator 2 | // requries E4X support. 3 | 4 | function f(n) { // Format integers to have at least two digits. 5 | return n < 10 ? '0' + n : n; 6 | } 7 | 8 | function rfc3339(date) { 9 | return date.getUTCFullYear() + '-' + 10 | f(date.getUTCMonth() + 1) + '-' + 11 | f(date.getUTCDate()) + 'T' + 12 | f(date.getUTCHours()) + ':' + 13 | f(date.getUTCMinutes()) + ':' + 14 | f(date.getUTCSeconds()) + 'Z'; 15 | }; 16 | 17 | exports.header = function(data) { 18 | var f = ; 19 | f.title = data.title; 20 | f.id = data.feed_id; 21 | f.link.@href = data.feed_link; 22 | f.link.@rel = "self"; 23 | f.generator = "CouchApp on CouchDB"; 24 | f.updated = rfc3339(data.updated); 25 | return f.toXMLString().replace(/\<\/feed\>/,''); 26 | }; 27 | 28 | exports.entry = function(data) { 29 | var entry = ; 30 | entry.id = data.entry_id; 31 | entry.title = data.title; 32 | entry.content = data.content; 33 | entry.content.@type = (data.content_type || 'html'); 34 | entry.updated = rfc3339(data.updated); 35 | entry.author = {data.author}; 36 | entry.link.@href = data.alternate; 37 | entry.link.@rel = "alternate"; 38 | return entry; 39 | } 40 | -------------------------------------------------------------------------------- /vendor/couchapp/evently/profile/noProfile/selectors/form/submit.js: -------------------------------------------------------------------------------- 1 | function() { 2 | var md5 = $$(this).app.require("vendor/couchapp/lib/md5"); 3 | 4 | // TODO this can be cleaned up with docForm? 5 | // it still needs the workflow to edit an existing profile 6 | var name = $("input[name=userCtxName]",this).val(); 7 | var newProfile = { 8 | rand : Math.random().toString(), 9 | nickname : $("input[name=nickname]",this).val(), 10 | email : $("input[name=email]",this).val(), 11 | url : $("input[name=url]",this).val() 12 | }, widget = $(this); 13 | 14 | // setup gravatar_url 15 | if (md5) { 16 | newProfile.gravatar_url = 'http://www.gravatar.com/avatar/'+md5.hex(newProfile.email || newProfile.rand)+'.jpg?s=40&d=identicon'; 17 | } 18 | 19 | // store the user profile on the user account document 20 | $.couch.userDb(function(db) { 21 | var userDocId = "org.couchdb.user:"+name; 22 | db.openDoc(userDocId, { 23 | success : function(userDoc) { 24 | userDoc["couch.app.profile"] = newProfile; 25 | db.saveDoc(userDoc, { 26 | success : function() { 27 | newProfile.name = userDoc.name; 28 | $$(widget).profile = newProfile; 29 | widget.trigger("profileReady", [newProfile]); 30 | } 31 | }); 32 | } 33 | }); 34 | }); 35 | return false; 36 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 8 | 9 | 10 | {{>header}} 11 |
    12 |
    13 |

    Recently...

    14 |
      15 | {{#posts}} 16 |
    • 17 |

      {{ title }}

      18 | {{#has_tags}} 19 |
      20 | {{#tags}} 21 | {{tag}} 22 | {{/tags}} 23 |
      24 | {{/has_tags}} 25 |
      26 | {{ date }} 27 | by {{author}} 28 |
      29 |
    • 30 | {{/posts}} 31 |
    32 |
    33 | older posts 34 | 5 35 | 10 36 | 25 37 |
    38 |
    39 | 40 | {{>scripts}} 41 | 49 | -------------------------------------------------------------------------------- /validate_doc_update.js: -------------------------------------------------------------------------------- 1 | function (newDoc, oldDoc, userCtx, secObj) { 2 | var v = require("lib/validate").init(newDoc, oldDoc, userCtx, secObj); 3 | 4 | v.isAuthor = function() { 5 | return v.isAdmin() || userCtx.roles.indexOf("author") != -1; 6 | }; 7 | 8 | // admins or owner can always delete 9 | if (v.isAdmin()) return true; 10 | if (((oldDoc && (oldDoc.author == userCtx.name))) && newDoc._deleted) return true; 11 | 12 | v.unchanged("type"); 13 | v.unchanged("author"); 14 | v.unchanged("created_at"); 15 | 16 | if (newDoc.created_at) v.dateFormat("created_at"); 17 | 18 | // docs with authors can only be saved by their author 19 | // admin can author anything... 20 | if (!v.isAdmin() && newDoc.author && newDoc.author != userCtx.name) { 21 | v.unauthorized("Only "+newDoc.author+" may edit this document."); 22 | } 23 | 24 | if (newDoc.type == 'post') { 25 | if (!v.isAuthor()) { 26 | v.unauthorized("Only authors may edit posts."); 27 | } 28 | v.require("created_at", "author", "body", "format", "title"); 29 | } else if (newDoc.type == 'comment') { 30 | v.require("created_at", "post_id", "comment", "format", "commenter"); 31 | v.assert((newDoc.commenter.name || newDoc.commenter.nickname) && (typeof newDoc.commenter.email != "undefined"), 32 | "Comments must include name and email."); 33 | if (newDoc.commenter.url) { 34 | v.assert(newDoc.commenter.url.match(/^https?:\/\/[^.]*\..*/), 35 | "Commenter URL must start with http://."); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | // a library for validations 2 | // over time we expect to extract more helpers and move them here. 3 | exports.init = function(newDoc, oldDoc, userCtx, secObj) { 4 | var v = {}; 5 | 6 | v.forbidden = function(message) { 7 | throw({forbidden : message}); 8 | }; 9 | 10 | v.unauthorized = function(message) { 11 | throw({unauthorized : message}); 12 | }; 13 | 14 | v.assert = function(should, message) { 15 | if (!should) v.forbidden(message); 16 | } 17 | 18 | v.isAdmin = function() { 19 | return userCtx.roles.indexOf('_admin') != -1 20 | }; 21 | 22 | v.require = function() { 23 | for (var i=0; i < arguments.length; i++) { 24 | var field = arguments[i]; 25 | message = "The '"+field+"' field is required."; 26 | if (typeof newDoc[field] == "undefined") v.forbidden(message); 27 | }; 28 | }; 29 | 30 | v.unchanged = function(field) { 31 | if (oldDoc && oldDoc[field] != newDoc[field]) 32 | v.forbidden("You may not change the '"+field+"' field."); 33 | }; 34 | 35 | v.matches = function(field, regex, message) { 36 | if (!newDoc[field].match(regex)) { 37 | message = message || "Format of '"+field+"' field is invalid."; 38 | v.forbidden(message); 39 | } 40 | }; 41 | 42 | // this ensures that the date will be UTC, parseable, and collate correctly 43 | v.dateFormat = function(field) { 44 | message = "Sorry, '"+field+"' is not a valid date format. Try: 2010-02-24T17:00:03.432Z"; 45 | v.matches(field, /\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}(\.\d*)?Z/, message); 46 | } 47 | 48 | return v; 49 | }; 50 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/validate.js: -------------------------------------------------------------------------------- 1 | // a library for validations 2 | // over time we expect to extract more helpers and move them here. 3 | exports.init = function(newDoc, oldDoc, userCtx, secObj) { 4 | var v = {}; 5 | 6 | v.forbidden = function(message) { 7 | throw({forbidden : message}); 8 | }; 9 | 10 | v.unauthorized = function(message) { 11 | throw({unauthorized : message}); 12 | }; 13 | 14 | v.assert = function(should, message) { 15 | if (!should) v.forbidden(message); 16 | } 17 | 18 | v.isAdmin = function() { 19 | return userCtx.roles.indexOf('_admin') != -1 20 | }; 21 | 22 | v.isRole = function(role) { 23 | return userCtx.roles.indexOf(role) != -1 24 | }; 25 | 26 | v.require = function() { 27 | for (var i=0; i < arguments.length; i++) { 28 | var field = arguments[i]; 29 | message = "The '"+field+"' field is required."; 30 | if (typeof newDoc[field] == "undefined") v.forbidden(message); 31 | }; 32 | }; 33 | 34 | v.unchanged = function(field) { 35 | if (oldDoc && oldDoc[field] != newDoc[field]) 36 | v.forbidden("You may not change the '"+field+"' field."); 37 | }; 38 | 39 | v.matches = function(field, regex, message) { 40 | if (!newDoc[field].match(regex)) { 41 | message = message || "Format of '"+field+"' field is invalid."; 42 | v.forbidden(message); 43 | } 44 | }; 45 | 46 | // this ensures that the date will be UTC, parseable, and collate correctly 47 | v.dateFormat = function(field) { 48 | message = "Sorry, '"+field+"' is not a valid date format. Try: 2010-02-24T17:00:03.432Z"; 49 | v.matches(field, /\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}(\.\d*)?Z/, message); 50 | } 51 | 52 | return v; 53 | }; 54 | -------------------------------------------------------------------------------- /_attachments/script/jquery.scrollTo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.ScrollTo - Easy element scrolling using jQuery. 3 | * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 4 | * Dual licensed under MIT and GPL. 5 | * Date: 9/11/2008 6 | * @author Ariel Flesler 7 | * @version 1.4 8 | * 9 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html 10 | */ 11 | ;(function(h){var m=h.scrollTo=function(b,c,g){h(window).scrollTo(b,c,g)};m.defaults={axis:'y',duration:1};m.window=function(b){return h(window).scrollable()};h.fn.scrollable=function(){return this.map(function(){var b=this.parentWindow||this.defaultView,c=this.nodeName=='#document'?b.frameElement||b:this,g=c.contentDocument||(c.contentWindow||c).document,i=c.setInterval;return c.nodeName=='IFRAME'||i&&h.browser.safari?g.body:i?g.documentElement:this})};h.fn.scrollTo=function(r,j,a){if(typeof j=='object'){a=j;j=0}if(typeof a=='function')a={onAfter:a};a=h.extend({},m.defaults,a);j=j||a.speed||a.duration;a.queue=a.queue&&a.axis.length>1;if(a.queue)j/=2;a.offset=n(a.offset);a.over=n(a.over);return this.scrollable().each(function(){var k=this,o=h(k),d=r,l,e={},p=o.is('html,body');switch(typeof d){case'number':case'string':if(/^([+-]=)?\d+(px)?$/.test(d)){d=n(d);break}d=h(d,this);case'object':if(d.is||d.style)l=(d=h(d)).offset()}h.each(a.axis.split(''),function(b,c){var g=c=='x'?'Left':'Top',i=g.toLowerCase(),f='scroll'+g,s=k[f],t=c=='x'?'Width':'Height',v=t.toLowerCase();if(l){e[f]=l[i]+(p?0:s-o.offset()[i]);if(a.margin){e[f]-=parseInt(d.css('margin'+g))||0;e[f]-=parseInt(d.css('border'+g+'Width'))||0}e[f]+=a.offset[i]||0;if(a.over[i])e[f]+=d[v]()*a.over[i]}else e[f]=d[i];if(/^\d+$/.test(e[f]))e[f]=e[f]<=0?0:Math.min(e[f],u(t));if(!b&&a.queue){if(s!=e[f])q(a.onAfterFirst);delete e[f]}});q(a.onAfter);function q(b){o.animate(e,j,a.easing,b&&function(){b.call(this,r,a)})};function u(b){var c='scroll'+b,g=k.ownerDocument;return p?Math.max(g.documentElement[c],g.body[c]):k[c]}}).end()};function n(b){return typeof b=='object'?b:{top:b,left:b}}})(jQuery); -------------------------------------------------------------------------------- /lists/comments.js: -------------------------------------------------------------------------------- 1 | function(head, req) { 2 | var ddoc = this; 3 | var Mustache = require("lib/mustache"); 4 | var List = require("vendor/couchapp/lib/list"); 5 | var path = require("vendor/couchapp/lib/path").init(req); 6 | var Atom = require("vendor/couchapp/lib/atom"); 7 | 8 | var indexPath = path.list('index','recent-posts',{descending:true, limit:10}); 9 | var feedPath = path.list('index','recent-posts',{descending:true, limit:10, format:"atom"}); 10 | var commentsFeed = path.list('comments','comments',{descending:true, limit:10, format:"atom"}); 11 | 12 | // if the client requests an atom feed and not html, 13 | // we run this function to generate the feed. 14 | provides("atom", function() { 15 | var path = require("vendor/couchapp/lib/path").init(req); 16 | var markdown = require("vendor/couchapp/lib/markdown"); 17 | var textile = require("vendor/textile/textile"); 18 | 19 | // we load the first row to find the most recent change date 20 | var row = getRow(); 21 | 22 | // generate the feed header 23 | var feedHeader = Atom.header({ 24 | updated : (row ? new Date(row.value.created_at) : new Date()), 25 | title : ddoc.blog.title + " comments", 26 | feed_id : path.absolute(indexPath), 27 | feed_link : path.absolute(commentsFeed) 28 | }); 29 | 30 | // send the header to the client 31 | send(feedHeader); 32 | 33 | // loop over all rows 34 | if (row) { 35 | do { 36 | var v = row.value; 37 | 38 | // generate the entry for this row 39 | var feedEntry = Atom.entry({ 40 | entry_id : path.absolute('/'+encodeURIComponent(req.info.db_name)+'/'+encodeURIComponent(row.id)), 41 | title : "comment on "+v.post_id, 42 | content : markdown.encode(Mustache.escape(v.comment)), 43 | updated : new Date(v.created_at), 44 | author : v.commenter.nickname || v.commenter.name, 45 | alternate : path.absolute(path.list('post','post-page', {startkey:[v.post_id]})) 46 | }); 47 | // send the entry to client 48 | send(feedEntry); 49 | } while (row = getRow()); 50 | } 51 | 52 | // close the loop after all rows are rendered 53 | return "
    "; 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /lists/post.js: -------------------------------------------------------------------------------- 1 | function(head, req) { 2 | var Mustache = require("lib/mustache"); 3 | var ddoc = this; 4 | var List = require("vendor/couchapp/lib/list"); 5 | var path = require("vendor/couchapp/lib/path").init(req); 6 | var markdown = require("vendor/couchapp/lib/markdown"); 7 | var textile = require("vendor/textile/textile"); 8 | 9 | var indexPath = path.list('index','recent-posts',{descending:true, limit:10}); 10 | var feedPath = path.list('index','recent-posts',{descending:true, limit:10, format:"atom"}); 11 | var commentsFeed = path.list('comments','comments',{descending:true, limit:10, format:"atom"}); 12 | 13 | provides("html", function() { 14 | // get the first row and make sure it's a post 15 | var post = getRow().value; 16 | if (post.type != "post") { 17 | throw(["error", "not_found", "not a post"]); 18 | } else { 19 | if (post.format == "markdown") { 20 | var html = markdown.encode(post.body); 21 | } else if (post.format == "textile") { 22 | var html = textile.encode(post.body); 23 | } else { 24 | var html = Mustache.escape(post.html); 25 | } 26 | 27 | var stash = { 28 | header : { 29 | index : indexPath, 30 | blogName : ddoc.blog.title, 31 | feedPath : feedPath, 32 | commentsFeed : commentsFeed 33 | }, 34 | scripts : {}, 35 | title : post.title, 36 | post_id : post._id, 37 | date : post.created_at, 38 | html : html, 39 | comments : List.withRows(function(row) { 40 | var v = row.value; 41 | if (v.type != "comment") { 42 | return; 43 | } 44 | // keep getting comments until we get to the next post... 45 | return { 46 | comment : { 47 | name : v.commenter.nickname || v.commenter.name, 48 | url : v.commenter.url, 49 | avatar : v.commenter.gravatar_url || 'http://www.gravatar.com/avatar/'+v.commenter.gravatar+'.jpg?s=40&d=identicon', 50 | html : markdown.encode(Mustache.escape(v.comment)), 51 | created_at : v.created_at 52 | } 53 | }; 54 | }) 55 | }; 56 | return Mustache.to_html(ddoc.templates.post, stash, ddoc.templates.partials, List.send); 57 | } 58 | }); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/path.js: -------------------------------------------------------------------------------- 1 | // from couch.js 2 | function encodeOptions(options) { 3 | var buf = []; 4 | if (typeof(options) == "object" && options !== null) { 5 | for (var name in options) { 6 | if (!options.hasOwnProperty(name)) {continue;} 7 | var value = options[name]; 8 | if (name == "key" || name == "startkey" || name == "endkey") { 9 | value = JSON.stringify(value); 10 | } 11 | buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); 12 | } 13 | } 14 | if (!buf.length) { 15 | return ""; 16 | } 17 | return "?" + buf.join("&"); 18 | } 19 | 20 | function concatArgs(array, args) { 21 | for (var i=0; i < args.length; i++) { 22 | array.push(args[i]); 23 | }; 24 | return array; 25 | }; 26 | 27 | function makePath(array) { 28 | var options, path; 29 | 30 | if (typeof array[array.length - 1] != "string") { 31 | // it's a params hash 32 | options = array.pop(); 33 | } 34 | path = array.map(function(item) {return encodeURIComponent(item)}).join('/'); 35 | if (options) { 36 | return path + encodeOptions(options); 37 | } else { 38 | return path; 39 | } 40 | }; 41 | 42 | exports.init = function(req) { 43 | return { 44 | asset : function() { 45 | var p = req.path, parts = ['', p[0], p[1] , p[2]]; 46 | return makePath(concatArgs(parts, arguments)); 47 | }, 48 | show : function() { 49 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_show']; 50 | return makePath(concatArgs(parts, arguments)); 51 | }, 52 | list : function() { 53 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_list']; 54 | return makePath(concatArgs(parts, arguments)); 55 | }, 56 | update : function() { 57 | var p = req.path, parts = ['', p[0], p[1] , p[2], '_update']; 58 | return makePath(concatArgs(parts, arguments)); 59 | }, 60 | limit : function(limit) { 61 | var query = req.query; 62 | var l = query.limit; 63 | query.limit = limit; 64 | var view = req.path[req.path.length - 1]; 65 | var list = req.path[req.path.length - 2]; 66 | var link = this.list(list, view, query); 67 | query.limit = l; 68 | return link; 69 | }, 70 | older : function(key) { 71 | if (!typeof key == "undefined") return null; 72 | var query = req.query; 73 | query.startkey = key; 74 | query.skip=1; 75 | var view = req.path[req.path.length - 1]; 76 | var list = req.path[req.path.length - 2]; 77 | return this.list(list, view, query); 78 | }, 79 | absolute : function(path) { 80 | return 'http://' + req.headers.Host + path; 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sofa: Standalone CouchDB Blog 2 | 3 | Sofa showcases the [potential of pure CouchDB applications](http://jchris.mfdz.com/code/2008/10/standalone_applications_with_co). It should provide an easy way for people to put thier thoughts online, anywhere there's a running Couch. It's just HTML, JavaScript and the magic of CouchDB. 4 | 5 | Currently supports authoring by anyone with the proper roles, and comments from anyone with a user account. 6 | 7 | ## Current News 8 | 9 | Things are moving crazy fast around here right now as I bring this stuff up to ship-shape for the [CouchDB book](http://books.couchdb.org). I'll be renaming methods and stuff (if I find the time), any API feedback will be appreciated. 10 | 11 | ## Install CouchDB 12 | 13 | You'll also need CouchDB (verion 0.11 or newer). Once you have that installed and the tests passing, you can install CouchApp 14 | and the blog software. 15 | 16 | ## Install CouchApp 17 | 18 | CouchApp makes it easy to edit application that are hosted in CouchDB, by keeping a correspondence between a set of files, and a CouchDB design document. You'll use CouchApp to install Sofa in your CouchDB instance. 19 | 20 | sudo easy_install couchapp 21 | 22 | CouchApp is a set of utilities for developing standalone CouchDB applications You can [learn more about the CouchApp project here](http://github.com/couchapp/couchapp/). Also, [`easy_install` has an unpleasant bug on OSX](http://mail.python.org/pipermail/pythonmac-sig/2008-October/020567.html), so you might end up having to work from git source. 23 | 24 | 25 | ### Setup Admin Access 26 | 27 | If you are going to put your blog in public, you'll want to [set up an Admin account (screencast)](http://www.youtube.com/watch?v=oHKvV3Nh-CI). 28 | 29 | 30 | ## Install Sofa 31 | 32 | git clone git://github.com/jchris/sofa.git 33 | cd sofa 34 | couchapp push . http://user:pass@127.0.0.1:5984/myblogdb 35 | 36 | You'll want to edit the HTML and CSS to personalize your site. Don't worry, the markup is pretty basic, so it's easy to rework. Adding new features is just a few lines of JavaScript away. 37 | 38 | Anytime you make edits to the on-disk version of Sofa, and want to see them in your browser, just run `couchapp push . http://127.0.0.1:5984/blogdb` again. **You probably want to setup your `.couchapprc` file.** You should read the CouchApp readme to learn about that. 39 | 40 | You can customize the blog title and other stuff in the `blog.json` file. 41 | 42 | # Relax 43 | 44 | [Visit your new blog.](http://127.0.0.1:5984/blogdb/_design/sofa/_list/index/recent-posts?descending=true&limit=5) 45 | 46 | ## License 47 | 48 | Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 49 | 50 | -------------------------------------------------------------------------------- /_attachments/style/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 1.2em Georgia, serif; 3 | line-height:1.2em; 4 | margin:0; 5 | padding:0; 6 | 7 | } 8 | 9 | #profile { 10 | /* margin:2em;*/ 11 | /* padding:.5em;*/ 12 | } 13 | 14 | #account { 15 | font-size:0.8em; 16 | text-shadow: #fff 0 0 4px; 17 | float:right; 18 | } 19 | 20 | #feeds { 21 | font-size:0.6em; 22 | text-shadow: #fff 0 0 4px; 23 | } 24 | 25 | a { 26 | text-decoration:none; 27 | } 28 | 29 | h1 { 30 | padding:6px; 31 | margin:0; 32 | line-height:1.2em; 33 | } 34 | 35 | h2 { 36 | padding:0; 37 | margin:0; 38 | line-height:1em; 39 | font-size:112%; 40 | } 41 | 42 | #header h2 { 43 | text-shadow: 0 0 4px #000; 44 | } 45 | 46 | #header h2 a { 47 | color:#fff; 48 | } 49 | 50 | #header #login, #header #edit { 51 | float:right; 52 | } 53 | 54 | h3 { 55 | margin-bottom:0; 56 | margin-top:4px; 57 | } 58 | 59 | h4 { 60 | margin:4px; 61 | } 62 | 63 | #header { 64 | background:#9a9; 65 | padding:8px; 66 | border-bottom: 1em solid #b9dab9; 67 | } 68 | 69 | #content { 70 | width:700px; 71 | padding:10px; 72 | background:#f9f9f9; 73 | /* float:left;*/ 74 | margin-top:0; 75 | margin-left:240px; 76 | } 77 | 78 | #tagcloud { 79 | float:left; 80 | width:220px; 81 | margin:2ex 1em; 82 | } 83 | 84 | .tags a, #tagcloud a { 85 | color:#bbb; 86 | } 87 | .tags a:hover, #tagcloud a:hover { 88 | color:#9b9; 89 | } 90 | 91 | .tags, .by { 92 | font-size:80%; 93 | color:#666; 94 | margin-top:0; 95 | } 96 | 97 | 98 | label { 99 | display:block; 100 | } 101 | 102 | #post .body { 103 | padding:5px 15px; 104 | } 105 | 106 | #posts .body { 107 | padding:5px 10px; 108 | } 109 | .body img { 110 | border:2px solid #fff; 111 | margin-left:133px; 112 | } 113 | 114 | #posts li { 115 | margin-bottom:16px; 116 | } 117 | 118 | p.post { 119 | font-family: serif; 120 | margin:10px; 121 | } 122 | 123 | #comments { 124 | font-size:80%; 125 | background:#fdfdfd; 126 | padding:12px; 127 | margin:8px; 128 | border:4px #fff; 129 | } 130 | 131 | #comments ul { 132 | padding:0; 133 | } 134 | 135 | #comments li { 136 | border:4px solid #ebebeb; 137 | background:#fbfbfb; 138 | margin:20px 0; 139 | padding:3px 3px 3px 3px; 140 | } 141 | 142 | #comments h4 { 143 | background:#ebebeb; 144 | padding:1px 0 0 5px; 145 | } 146 | 147 | img.gravatar { 148 | width:40px; 149 | height:40px; 150 | float:left; 151 | margin:2px 8px; 152 | border:2px solid #fff; 153 | } 154 | 155 | ul { 156 | margin:0; 157 | list-style: none; 158 | padding:0 20px; 159 | } 160 | 161 | .body ul { 162 | list-style: disc; 163 | } 164 | 165 | .paginate { 166 | text-align:center; 167 | display:block; 168 | width:100%; 169 | font-size:80%; 170 | } 171 | 172 | form { 173 | font-size:0.8em; 174 | padding:8px; 175 | background:#ddd; 176 | } 177 | 178 | 179 | 180 | fieldset { 181 | background:#ebebeb; 182 | } 183 | 184 | fieldset#author { 185 | float:left; 186 | width:20%; 187 | } 188 | 189 | textarea { 190 | padding:2px; 191 | } 192 | 193 | 194 | blockquote { 195 | padding:8px; 196 | background:#444; 197 | color:#fff; 198 | } -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.couch.app.util.js: -------------------------------------------------------------------------------- 1 | $.log = function(m) { 2 | if (window && window.console && window.console.log) { 3 | window.console.log(arguments.length == 1 ? m : arguments); 4 | } 5 | }; 6 | 7 | // http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery/1186309#1186309 8 | $.fn.serializeObject = function() { 9 | var o = {}; 10 | var a = this.serializeArray(); 11 | $.each(a, function() { 12 | if (o[this.name]) { 13 | if (!o[this.name].push) { 14 | o[this.name] = [o[this.name]]; 15 | } 16 | o[this.name].push(this.value || ''); 17 | } else { 18 | o[this.name] = this.value || ''; 19 | } 20 | }); 21 | return o; 22 | }; 23 | 24 | // todo remove this crap 25 | function escapeHTML(st) { 26 | return( 27 | st && st.replace(/&/g,'&'). 28 | replace(/>/g,'>'). 29 | replace(/'+a+''; 42 | }).replace(/\@([\w\-]+)/g,function(user,name) { 43 | return ''+user+''; 44 | }).replace(/\#([\w\-\.]+)/g,function(word,tag) { 45 | return ''+word+''; 46 | }); 47 | }; 48 | 49 | $.fn.prettyDate = function() { 50 | $(this).each(function() { 51 | $(this).text($.prettyDate($(this).text())); 52 | }); 53 | }; 54 | 55 | $.prettyDate = function(time){ 56 | 57 | var date = new Date(time.replace(/-/g,"/").replace("T", " ").replace("Z", " +0000").replace(/(\d*\:\d*:\d*)\.\d*/g,"$1")), 58 | diff = (((new Date()).getTime() - date.getTime()) / 1000), 59 | day_diff = Math.floor(diff / 86400); 60 | 61 | if (isNaN(day_diff)) return time; 62 | 63 | return day_diff < 1 && ( 64 | diff < 60 && "just now" || 65 | diff < 120 && "1 minute ago" || 66 | diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || 67 | diff < 7200 && "1 hour ago" || 68 | diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || 69 | day_diff == 1 && "yesterday" || 70 | day_diff < 21 && day_diff + " days ago" || 71 | day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" || 72 | day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || 73 | Math.ceil( day_diff / 365 ) + " years ago"; 74 | }; 75 | 76 | $.argsToArray = function(args) { 77 | if (!args.callee) return args; 78 | var array = []; 79 | for (var i=0; i < args.length; i++) { 80 | array.push(args[i]); 81 | }; 82 | return array; 83 | } 84 | -------------------------------------------------------------------------------- /vendor/textile/textile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is the orginial function from Stuart Langridge at http://www.kryogenix.org/ 3 | */ 4 | 5 | /* 6 | * This is the update function from Jeff Minard - http://www.jrm.cc/ 7 | */ 8 | function superTextile(s) { 9 | var r = s; 10 | // quick tags first 11 | qtags = [['\\*', 'strong'], 12 | ['\\?\\?', 'cite'], 13 | ['\\+', 'ins'], //fixed 14 | ['~', 'sub'], 15 | ['\\^', 'sup'], // me 16 | ['@', 'code']]; 17 | for (var i=0;i'+'$1'+''); 21 | } 22 | // underscores count as part of a word, so do them separately 23 | re = new RegExp('\\b_(.+?)_\\b','g'); 24 | r = r.replace(re,'$1'); 25 | 26 | //jeff: so do dashes 27 | re = new RegExp('[\\s\\n]-(.+?)-[\\s\\n]','g'); 28 | r = r.replace(re,'$1'); 29 | 30 | // links 31 | re = new RegExp('"\\b(.+?)\\(\\b(.+?)\\b\\)":([^\\s]+)','g'); 32 | r = r.replace(re,'$1'); 33 | re = new RegExp('"(.+?)":([^\\s]+)','g'); 34 | r = r.replace(re,'$1'); 35 | 36 | // images 37 | re = new RegExp('!\\b(.+?)\\(\\b(.+?)\\b\\)!','g'); 38 | r = r.replace(re,'$2'); 39 | re = new RegExp('!\\b(.+?)\\b!','g'); 40 | r = r.replace(re,''); 41 | 42 | // block level formatting 43 | 44 | // Jeff's hack to show single line breaks as they should. 45 | // insert breaks - but you get some....stupid ones 46 | re = new RegExp('(.*)\\n([^#\\*\\n].*)','g'); 47 | r = r.replace(re,'$1
    $2'); 48 | // remove the stupid breaks. 49 | re = new RegExp('\\n
    ','g'); 50 | r = r.replace(re,'\n'); 51 | 52 | lines = r.split('\n'); 53 | nr = ''; 54 | for (i=0;i')+''; 59 | changed = 1; 60 | } 61 | 62 | // jeff adds h#. 63 | if (line.search(/^\s*h[1|2|3|4|5|6]\.\s+/) != -1) { 64 | re = new RegExp('h([1|2|3|4|5|6])\\.(.+)','g'); 65 | line = line.replace(re,'$2'); 66 | changed = 1; 67 | } 68 | 69 | if (line.search(/^\s*\*\s+/) != -1) { line = line.replace(/^\s*\*\s+/,'\t') + ''; changed = 1; } // * for bullet list; make up an liu tag to be fixed later 70 | if (line.search(/^\s*#\s+/) != -1) { line = line.replace(/^\s*#\s+/,'\t') + ''; changed = 1; } // # for numeric list; make up an lio tag to be fixed later 71 | if (!changed && (line.replace(/\s/g,'').length > 0)) {line = '

    '+line+'

    ';} 72 | lines[i] = line + '\n'; 73 | } 74 | 75 | // Second pass to do lists 76 | inlist = 0; 77 | listtype = ''; 78 | for (i=0;i AND 89 | r = r.replace(/li[o|u]>/g,'li>'); 90 | 91 | return r; 92 | } 93 | 94 | exports.encode = superTextile; 95 | -------------------------------------------------------------------------------- /templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ pageTitle }} 5 | 6 | 7 | 8 | {{>header}} 9 |
    10 |
    11 | 12 |
    13 |

    {{pageTitle}}

    14 |

    15 |

    16 |

    17 |

    18 |

    19 | 20 | 21 |

    22 |

    23 | 24 | 25 | 26 |

    27 |
    28 | Markdown help 29 |
    30 |
    31 | 32 | {{>scripts}} 33 | 34 | 104 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/docform.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | // use this file except in compliance with the License. You may obtain a copy 3 | // of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | // License for the specific language governing permissions and limitations under 11 | // the License. 12 | 13 | // turn the form into deep json 14 | // field names like 'author-email' get turned into json like 15 | // {"author":{"email":"quentin@example.com"}} 16 | // acts on doc by reference, so you can safely pass non-form fields through 17 | 18 | function docForm(formSelector, opts) { 19 | var localFormDoc = {}; 20 | opts = opts || {}; 21 | opts.fields = opts.fields || []; 22 | 23 | // turn the form into deep json 24 | // field names like 'author-email' get turned into json like 25 | // {"author":{"email":"quentin@example.com"}} 26 | function formToDeepJSON(form, fields, doc) { 27 | form = $(form); 28 | fields.forEach(function(field) { 29 | var element = form.find("[name="+field+"]"); 30 | if (element.attr('type') === 'checkbox') { 31 | var val = element.attr('checked'); 32 | } else { 33 | var val = element.val(); 34 | if (!val) return; 35 | } 36 | var parts = field.split('-'); 37 | var frontObj = doc, frontName = parts.shift(); 38 | while (parts.length > 0) { 39 | frontObj[frontName] = frontObj[frontName] || {}; 40 | frontObj = frontObj[frontName]; 41 | frontName = parts.shift(); 42 | } 43 | frontObj[frontName] = val; 44 | }); 45 | } 46 | 47 | // Apply the behavior 48 | $(formSelector).submit(function(e) { 49 | e.preventDefault(); 50 | if (opts.validate && opts.validate() == false) { return false;} 51 | // formToDeepJSON acts on localFormDoc by reference 52 | formToDeepJSON(this, opts.fields, localFormDoc); 53 | if (opts.beforeSave) {opts.beforeSave(localFormDoc);} 54 | db.saveDoc(localFormDoc, { 55 | success : function(resp) { 56 | if (opts.success) {opts.success(resp, localFormDoc);} 57 | } 58 | }); 59 | 60 | return false; 61 | }); 62 | 63 | // populate form from an existing doc 64 | function docToForm(doc) { 65 | var form = $(formSelector); 66 | // fills in forms 67 | opts.fields.forEach(function(field) { 68 | var parts = field.split('-'); 69 | var run = true, frontObj = doc, frontName = parts.shift(); 70 | while (frontObj && parts.length > 0) { 71 | frontObj = frontObj[frontName]; 72 | frontName = parts.shift(); 73 | } 74 | if (frontObj && frontObj[frontName]) { 75 | var element = form.find("[name="+field+"]"); 76 | if (element.attr('type') === 'checkbox') { 77 | element.attr('checked', frontObj[frontName]); 78 | } else { 79 | element.val(frontObj[frontName]); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | if (opts.id) { 86 | db.openDoc(opts.id, { 87 | attachPrevRev : opts.attachPrevRev, 88 | error: function() { 89 | if (opts.error) {opts.error.apply(opts, arguments);} 90 | }, 91 | success: function(doc) { 92 | if (opts.load || opts.onLoad) {(opts.load || opts.onLoad)(doc);} 93 | localFormDoc = doc; 94 | docToForm(doc); 95 | }}); 96 | } else if (opts.template) { 97 | if (opts.load || opts.onLoad) {(opts.load || opts.onLoad)(opts.template);} 98 | localFormDoc = opts.template; 99 | docToForm(localFormDoc); 100 | } 101 | var instance = { 102 | deleteDoc : function(opts) { 103 | opts = opts || {}; 104 | if (confirm("Really delete this document?")) { 105 | db.removeDoc(localFormDoc, opts); 106 | } 107 | }, 108 | localDoc : function() { 109 | formToDeepJSON(formSelector, opts.fields, localFormDoc); 110 | return localFormDoc; 111 | } 112 | }; 113 | return instance; 114 | } 115 | -------------------------------------------------------------------------------- /lists/index.js: -------------------------------------------------------------------------------- 1 | function(head, req) { 2 | var ddoc = this; 3 | var Mustache = require("lib/mustache"); 4 | var List = require("vendor/couchapp/lib/list"); 5 | var path = require("vendor/couchapp/lib/path").init(req); 6 | var Atom = require("vendor/couchapp/lib/atom"); 7 | 8 | var indexPath = path.list('index','recent-posts',{descending:true, limit:10}); 9 | var feedPath = path.list('index','recent-posts',{descending:true, limit:10, format:"atom"}); 10 | var commentsFeed = path.list('comments','comments',{descending:true, limit:10, format:"atom"}); 11 | 12 | var path_parts = req.path; 13 | // The provides function serves the format the client requests. 14 | // The first matching format is sent, so reordering functions changes 15 | // thier priority. In this case HTML is the preferred format, so it comes first. 16 | provides("html", function() { 17 | var key = ""; 18 | // render the html head using a template 19 | var stash = { 20 | header : { 21 | index : indexPath, 22 | blogName : ddoc.blog.title, 23 | feedPath : feedPath, 24 | commentsFeed : commentsFeed 25 | }, 26 | scripts : {}, 27 | db : req.path[0], 28 | design : req.path[2], 29 | feedPath : feedPath, 30 | newPostPath : path.show("edit"), 31 | assets : path.asset(), 32 | posts : List.withRows(function(row) { 33 | var post = row.value; 34 | key = row.key; 35 | return { 36 | title : post.title, 37 | author : post.author, 38 | date : post.created_at, 39 | link : path.list('post','post-page', {startkey : [row.id]}), 40 | has_tags : post.tags ? true : false, 41 | tags : post.tags && post.tags.map ? post.tags.map(function(tag) { 42 | var t = tag.toLowerCase(); 43 | return { 44 | tag : tag, 45 | link : path.list("index", "tags", { 46 | descending : true, 47 | reduce : false, 48 | startkey : [t, {}], 49 | endkey : [t] 50 | }) 51 | } 52 | }) : [] 53 | }; 54 | }), 55 | older : function() { 56 | return path.older(key); 57 | }, 58 | "5" : path.limit(5), 59 | "10" : path.limit(10), 60 | "25" : path.limit(25) 61 | }; 62 | return Mustache.to_html(ddoc.templates.index, stash, ddoc.templates.partials, List.send); 63 | }); 64 | 65 | // if the client requests an atom feed and not html, 66 | // we run this function to generate the feed. 67 | provides("atom", function() { 68 | var path = require("vendor/couchapp/lib/path").init(req); 69 | var markdown = require("vendor/couchapp/lib/markdown"); 70 | var textile = require("vendor/textile/textile"); 71 | 72 | // we load the first row to find the most recent change date 73 | var row = getRow(); 74 | 75 | // generate the feed header 76 | var feedHeader = Atom.header({ 77 | updated : (row ? new Date(row.value.created_at) : new Date()), 78 | title : ddoc.blog.title, 79 | feed_id : path.absolute(indexPath), 80 | feed_link : path.absolute(feedPath), 81 | }); 82 | 83 | // send the header to the client 84 | send(feedHeader); 85 | 86 | // loop over all rows 87 | if (row) { 88 | do { 89 | if (row.value.format == "markdown") { 90 | var html = markdown.encode(row.value.body); 91 | } else if (row.value.format == "textile") { 92 | var html = textile.encode(row.value.body); 93 | } else { 94 | var html = Mustache.escape(row.value.html); 95 | } 96 | // generate the entry for this row 97 | var feedEntry = Atom.entry({ 98 | entry_id : path.absolute('/'+encodeURIComponent(req.info.db_name)+'/'+encodeURIComponent(row.id)), 99 | title : row.value.title, 100 | content : html, 101 | updated : new Date(row.value.created_at), 102 | author : row.value.author, 103 | alternate : path.absolute(path.show('post', row.id)) 104 | }); 105 | // send the entry to client 106 | send(feedEntry); 107 | } while (row = getRow()); 108 | } 109 | 110 | // close the loop after all rows are rendered 111 | return ""; 112 | }); 113 | }; -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.pathbinder.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | // functions for handling the path 3 | // thanks sammy.js 4 | var PATH_REPLACER = "([^\/]+)", 5 | PATH_NAME_MATCHER = /:([\w\d]+)/g, 6 | QUERY_STRING_MATCHER = /\?([^#]*)$/, 7 | SPLAT_MATCHER = /(\*)/, 8 | SPLAT_REPLACER = "(.+)", 9 | _currentPath, 10 | _lastPath, 11 | _pathInterval; 12 | 13 | function hashChanged() { 14 | _currentPath = getPath(); 15 | // if path is actually changed from what we thought it was, then react 16 | if (_lastPath != _currentPath) { 17 | _lastPath = _currentPath; 18 | return triggerOnPath(_currentPath); 19 | } 20 | } 21 | 22 | $.pathbinder = { 23 | changeFuns : [], 24 | paths : [], 25 | begin : function(defaultPath) { 26 | // this should trigger the defaultPath if there's not a path in the URL 27 | // otherwise it should trigger the URL's path 28 | $(function() { 29 | var loadPath = getPath(); 30 | if (loadPath) { 31 | triggerOnPath(loadPath); 32 | } else { 33 | goPath(defaultPath); 34 | triggerOnPath(defaultPath); 35 | } 36 | }) 37 | }, 38 | go : function(path) { 39 | goPath(path); 40 | triggerOnPath(path); 41 | }, 42 | currentPath : function() { 43 | return getPath(); 44 | }, 45 | onChange : function (fun) { 46 | $.pathbinder.changeFuns.push(fun); 47 | } 48 | }; 49 | 50 | function pollPath(every) { 51 | function hashCheck() { 52 | _currentPath = getPath(); 53 | // path changed if _currentPath != _lastPath 54 | if (_lastPath != _currentPath) { 55 | setTimeout(function() { 56 | $(window).trigger('hashchange'); 57 | }, 1); 58 | } 59 | }; 60 | hashCheck(); 61 | _pathInterval = setInterval(hashCheck, every); 62 | $(window).bind('unload', function() { 63 | clearInterval(_pathInterval); 64 | }); 65 | } 66 | 67 | function triggerOnPath(path) { 68 | path = path.replace(/^#/,''); 69 | $.pathbinder.changeFuns.forEach(function(fun) {fun(path)}); 70 | var pathSpec, path_params, params = {}, param_name, param; 71 | for (var i=0; i < $.pathbinder.paths.length; i++) { 72 | pathSpec = $.pathbinder.paths[i]; 73 | // $.log("pathSpec", pathSpec); 74 | if ((path_params = pathSpec.matcher.exec(path)) !== null) { 75 | // $.log("path_params", path_params); 76 | path_params.shift(); 77 | for (var j=0; j < path_params.length; j++) { 78 | param_name = pathSpec.param_names[j]; 79 | param = decodeURIComponent(path_params[j]); 80 | if (param_name) { 81 | params[param_name] = param; 82 | } else { 83 | if (!params.splat) params.splat = []; 84 | params.splat.push(param); 85 | } 86 | }; 87 | pathSpec.callback(params); 88 | // return true; // removed this to allow for multi match 89 | } 90 | }; 91 | }; 92 | 93 | // bind the event 94 | $(function() { 95 | if ('onhashchange' in window) { 96 | // we have a native event 97 | } else { 98 | pollPath(10); 99 | } 100 | // setTimeout(hashChanged,50); 101 | $(window).bind('hashchange', hashChanged); 102 | }); 103 | 104 | function registerPath(pathSpec) { 105 | $.pathbinder.paths.push(pathSpec); 106 | }; 107 | 108 | function setPath(pathSpec, params) { 109 | var newPath = $.mustache(pathSpec.template, params); 110 | goPath(newPath); 111 | }; 112 | 113 | function goPath(newPath) { 114 | if (newPath) { 115 | // $.log("goPath", newPath) 116 | window.location = '#'+newPath; 117 | } 118 | _lastPath = getPath(); 119 | }; 120 | 121 | function getPath() { 122 | var matches = window.location.toString().match(/^[^#]*(#.+)$/); 123 | return matches ? matches[1] : ''; 124 | }; 125 | 126 | function makePathSpec(path, callback) { 127 | var param_names = []; 128 | var template = ""; 129 | 130 | PATH_NAME_MATCHER.lastIndex = 0; 131 | 132 | while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) { 133 | param_names.push(path_match[1]); 134 | } 135 | 136 | return { 137 | param_names : param_names, 138 | matcher : new RegExp("^" + path.replace( 139 | PATH_NAME_MATCHER, PATH_REPLACER).replace( 140 | SPLAT_MATCHER, SPLAT_REPLACER) + "/?$"), 141 | template : path.replace(PATH_NAME_MATCHER, function(a, b) { 142 | return '{{'+b+'}}'; 143 | }).replace(SPLAT_MATCHER, '{{splat}}'), 144 | callback : callback 145 | }; 146 | }; 147 | 148 | $.fn.pathbinder = function(name, paths, options) { 149 | options = options || {}; 150 | var self = $(this), pathList = paths.split(/\n/); 151 | $.each(pathList, function() { 152 | var path = this; 153 | if (path) { 154 | // $.log("bind path", path); 155 | var pathSpec = makePathSpec(path, function(params) { 156 | // $.log("path cb", name, path, self) 157 | // $.log("trigger path: "+path+" params: ", params); 158 | self.trigger(name, [params]); 159 | }); 160 | // set the path when the event triggered through other means 161 | if (options.bindPath) { 162 | self.bind(name, function(ev, params) { 163 | params = params || {}; 164 | // $.log("set path", name, pathSpec) 165 | setPath(pathSpec, params); 166 | }); 167 | } 168 | // trigger when the path matches 169 | registerPath(pathSpec); 170 | } 171 | }); 172 | }; 173 | })(jQuery); 174 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.couch.app.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | // use this file except in compliance with the License. You may obtain a copy 3 | // of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | // License for the specific language governing permissions and limitations under 11 | // the License. 12 | 13 | // Usage: The passed in function is called when the page is ready. 14 | // CouchApp passes in the app object, which takes care of linking to 15 | // the proper database, and provides access to the CouchApp helpers. 16 | // $.couch.app(function(app) { 17 | // app.db.view(...) 18 | // ... 19 | // }); 20 | 21 | (function($) { 22 | 23 | function Design(db, name, code) { 24 | this.doc_id = "_design/"+name; 25 | if (code) { 26 | this.code_path = this.doc_id + "/" + code; 27 | } else { 28 | this.code_path = this.doc_id; 29 | } 30 | this.view = function(view, opts) { 31 | db.view(name+'/'+view, opts); 32 | }; 33 | this.list = function(list, view, opts) { 34 | db.list(name+'/'+list, view, opts); 35 | }; 36 | } 37 | 38 | function docForm() { alert("docForm has been moved to vendor/couchapp/lib/docForm.js, use app.require to load") }; 39 | 40 | function resolveModule(names, parent, current) { 41 | if (names.length === 0) { 42 | if (typeof current != "string") { 43 | throw ["error","invalid_require_path", 44 | 'Must require a JavaScript string, not: '+(typeof current)]; 45 | } 46 | return [current, parent]; 47 | } 48 | var n = names.shift(); 49 | if (n == '..') { 50 | if (!(parent && parent.parent)) { 51 | throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)]; 52 | } 53 | return resolveModule(names, parent.parent.parent, parent.parent); 54 | } else if (n == '.') { 55 | if (!parent) { 56 | throw ["error", "invalid_require_path", 'Object has no parent '+JSON.stringify(current)]; 57 | } 58 | return resolveModule(names, parent.parent, parent); 59 | } 60 | if (!current[n]) { 61 | throw ["error", "invalid_require_path", 'Object has no property "'+n+'". '+JSON.stringify(current)]; 62 | } 63 | var p = current; 64 | current = current[n]; 65 | current.parent = p; 66 | return resolveModule(names, p, current); 67 | } 68 | 69 | function makeRequire(ddoc) { 70 | var moduleCache = []; 71 | function getCachedModule(name, parent) { 72 | var key, i, len = moduleCache.length; 73 | for (i=0;i>> 0; 198 | if (typeof fun != "function") 199 | throw new TypeError(); 200 | 201 | var thisp = arguments[1]; 202 | for (var i = 0; i < len; i++) 203 | { 204 | if (i in this) 205 | fun.call(thisp, this[i], i, this); 206 | } 207 | }; 208 | } 209 | 210 | if (!Array.prototype.indexOf) 211 | { 212 | Array.prototype.indexOf = function(elt) 213 | { 214 | var len = this.length >>> 0; 215 | 216 | var from = Number(arguments[1]) || 0; 217 | from = (from < 0) 218 | ? Math.ceil(from) 219 | : Math.floor(from); 220 | if (from < 0) 221 | from += len; 222 | 223 | for (; from < len; from++) 224 | { 225 | if (from in this && 226 | this[from] === elt) 227 | return from; 228 | } 229 | return -1; 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /_attachments/script/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 3 | * Digest Algorithm, as defined in RFC 1321. 4 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for more info. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} 23 | function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} 24 | function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} 25 | function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } 26 | function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } 27 | function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function md5_vm_test() 33 | { 34 | return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; 35 | } 36 | 37 | /* 38 | * Calculate the MD5 of an array of little-endian words, and a bit length 39 | */ 40 | function core_md5(x, len) 41 | { 42 | /* append padding */ 43 | x[len >> 5] |= 0x80 << ((len) % 32); 44 | x[(((len + 64) >>> 9) << 4) + 14] = len; 45 | 46 | var a = 1732584193; 47 | var b = -271733879; 48 | var c = -1732584194; 49 | var d = 271733878; 50 | 51 | for(var i = 0; i < x.length; i += 16) 52 | { 53 | var olda = a; 54 | var oldb = b; 55 | var oldc = c; 56 | var oldd = d; 57 | 58 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 59 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 60 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 61 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 62 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 63 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 64 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 65 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 66 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 67 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 68 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 69 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 70 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 71 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 72 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 73 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 74 | 75 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 76 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 77 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 78 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 79 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 80 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 81 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 82 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 83 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 84 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 85 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 86 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 87 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 88 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 89 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 90 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 91 | 92 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 93 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 94 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 95 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 96 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 97 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 98 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 99 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 100 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 101 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 102 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 103 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 104 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 105 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 106 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 107 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 108 | 109 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 110 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 111 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 112 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 113 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 114 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 115 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 116 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 117 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 118 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 119 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 120 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 121 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 122 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 123 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 124 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 125 | 126 | a = safe_add(a, olda); 127 | b = safe_add(b, oldb); 128 | c = safe_add(c, oldc); 129 | d = safe_add(d, oldd); 130 | } 131 | return Array(a, b, c, d); 132 | 133 | } 134 | 135 | /* 136 | * These functions implement the four basic operations the algorithm uses. 137 | */ 138 | function md5_cmn(q, a, b, x, s, t) 139 | { 140 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 141 | } 142 | function md5_ff(a, b, c, d, x, s, t) 143 | { 144 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 145 | } 146 | function md5_gg(a, b, c, d, x, s, t) 147 | { 148 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 149 | } 150 | function md5_hh(a, b, c, d, x, s, t) 151 | { 152 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 153 | } 154 | function md5_ii(a, b, c, d, x, s, t) 155 | { 156 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 157 | } 158 | 159 | /* 160 | * Calculate the HMAC-MD5, of a key and some data 161 | */ 162 | function core_hmac_md5(key, data) 163 | { 164 | var bkey = str2binl(key); 165 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); 166 | 167 | var ipad = Array(16), opad = Array(16); 168 | for(var i = 0; i < 16; i++) 169 | { 170 | ipad[i] = bkey[i] ^ 0x36363636; 171 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 172 | } 173 | 174 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); 175 | return core_md5(opad.concat(hash), 512 + 128); 176 | } 177 | 178 | /* 179 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 180 | * to work around bugs in some JS interpreters. 181 | */ 182 | function safe_add(x, y) 183 | { 184 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 185 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 186 | return (msw << 16) | (lsw & 0xFFFF); 187 | } 188 | 189 | /* 190 | * Bitwise rotate a 32-bit number to the left. 191 | */ 192 | function bit_rol(num, cnt) 193 | { 194 | return (num << cnt) | (num >>> (32 - cnt)); 195 | } 196 | 197 | /* 198 | * Convert a string to an array of little-endian words 199 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. 200 | */ 201 | function str2binl(str) 202 | { 203 | var bin = Array(); 204 | var mask = (1 << chrsz) - 1; 205 | for(var i = 0; i < str.length * chrsz; i += chrsz) 206 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); 207 | return bin; 208 | } 209 | 210 | /* 211 | * Convert an array of little-endian words to a string 212 | */ 213 | function binl2str(bin) 214 | { 215 | var str = ""; 216 | var mask = (1 << chrsz) - 1; 217 | for(var i = 0; i < bin.length * 32; i += chrsz) 218 | str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); 219 | return str; 220 | } 221 | 222 | /* 223 | * Convert an array of little-endian words to a hex string. 224 | */ 225 | function binl2hex(binarray) 226 | { 227 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 228 | var str = ""; 229 | for(var i = 0; i < binarray.length * 4; i++) 230 | { 231 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + 232 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); 233 | } 234 | return str; 235 | } 236 | 237 | /* 238 | * Convert an array of little-endian words to a base-64 string 239 | */ 240 | function binl2b64(binarray) 241 | { 242 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 243 | var str = ""; 244 | for(var i = 0; i < binarray.length * 4; i += 3) 245 | { 246 | var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) 247 | | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) 248 | | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); 249 | for(var j = 0; j < 4; j++) 250 | { 251 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 252 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 253 | } 254 | } 255 | return str; 256 | } 257 | -------------------------------------------------------------------------------- /helpers/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 3 | * Digest Algorithm, as defined in RFC 1321. 4 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for more info. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} 23 | function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} 24 | function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} 25 | function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } 26 | function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } 27 | function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function md5_vm_test() 33 | { 34 | return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; 35 | } 36 | 37 | /* 38 | * Calculate the MD5 of an array of little-endian words, and a bit length 39 | keep 40 | */ 41 | function core_md5(x, len) 42 | { 43 | /* append padding */ 44 | x[len >> 5] |= 0x80 << ((len) % 32); 45 | x[(((len + 64) >>> 9) << 4) + 14] = len; 46 | 47 | var a = 1732584193; 48 | var b = -271733879; 49 | var c = -1732584194; 50 | var d = 271733878; 51 | 52 | for(var i = 0; i < x.length; i += 16) 53 | { 54 | var olda = a; 55 | var oldb = b; 56 | var oldc = c; 57 | var oldd = d; 58 | 59 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 60 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 61 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 62 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 63 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 64 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 65 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 66 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 67 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 68 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 69 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 70 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 71 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 72 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 73 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 74 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 75 | 76 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 77 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 78 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 79 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 80 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 81 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 82 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 83 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 84 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 85 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 86 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 87 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 88 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 89 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 90 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 91 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 92 | 93 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 94 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 95 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 96 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 97 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 98 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 99 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 100 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 101 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 102 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 103 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 104 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 105 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 106 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 107 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 108 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 109 | 110 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 111 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 112 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 113 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 114 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 115 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 116 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 117 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 118 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 119 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 120 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 121 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 122 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 123 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 124 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 125 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 126 | 127 | a = safe_add(a, olda); 128 | b = safe_add(b, oldb); 129 | c = safe_add(c, oldc); 130 | d = safe_add(d, oldd); 131 | } 132 | return Array(a, b, c, d); 133 | 134 | } 135 | 136 | /* 137 | * These functions implement the four basic operations the algorithm uses. 138 | */ 139 | function md5_cmn(q, a, b, x, s, t) 140 | { 141 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 142 | } 143 | function md5_ff(a, b, c, d, x, s, t) 144 | { 145 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 146 | } 147 | function md5_gg(a, b, c, d, x, s, t) 148 | { 149 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 150 | } 151 | function md5_hh(a, b, c, d, x, s, t) 152 | { 153 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 154 | } 155 | function md5_ii(a, b, c, d, x, s, t) 156 | { 157 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 158 | } 159 | 160 | /* 161 | * Calculate the HMAC-MD5, of a key and some data 162 | */ 163 | function core_hmac_md5(key, data) 164 | { 165 | var bkey = str2binl(key); 166 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); 167 | 168 | var ipad = Array(16), opad = Array(16); 169 | for(var i = 0; i < 16; i++) 170 | { 171 | ipad[i] = bkey[i] ^ 0x36363636; 172 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 173 | } 174 | 175 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); 176 | return core_md5(opad.concat(hash), 512 + 128); 177 | } 178 | 179 | /* 180 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 181 | * to work around bugs in some JS interpreters. 182 | */ 183 | function safe_add(x, y) 184 | { 185 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 186 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 187 | return (msw << 16) | (lsw & 0xFFFF); 188 | } 189 | 190 | /* 191 | * Bitwise rotate a 32-bit number to the left. 192 | */ 193 | function bit_rol(num, cnt) 194 | { 195 | return (num << cnt) | (num >>> (32 - cnt)); 196 | } 197 | 198 | /* 199 | * Convert a string to an array of little-endian words 200 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. 201 | keep 202 | */ 203 | function str2binl(str) 204 | { 205 | var bin = Array(); 206 | var mask = (1 << chrsz) - 1; 207 | for(var i = 0; i < str.length * chrsz; i += chrsz) 208 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); 209 | return bin; 210 | } 211 | 212 | /* 213 | * Convert an array of little-endian words to a string 214 | */ 215 | function binl2str(bin) 216 | { 217 | var str = ""; 218 | var mask = (1 << chrsz) - 1; 219 | for(var i = 0; i < bin.length * 32; i += chrsz) 220 | str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); 221 | return str; 222 | } 223 | 224 | /* 225 | * Convert an array of little-endian words to a hex string. 226 | keep 227 | */ 228 | function binl2hex(binarray) 229 | { 230 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 231 | var str = ""; 232 | for(var i = 0; i < binarray.length * 4; i++) 233 | { 234 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + 235 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); 236 | } 237 | return str; 238 | } 239 | 240 | /* 241 | * Convert an array of little-endian words to a base-64 string 242 | */ 243 | function binl2b64(binarray) 244 | { 245 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 246 | var str = ""; 247 | for(var i = 0; i < binarray.length * 4; i += 3) 248 | { 249 | var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) 250 | | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) 251 | | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); 252 | for(var j = 0; j < 4; j++) 253 | { 254 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 255 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 256 | } 257 | } 258 | return str; 259 | } -------------------------------------------------------------------------------- /vendor/couchapp/lib/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 3 | * Digest Algorithm, as defined in RFC 1321. 4 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for more info. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} 23 | function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} 24 | function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} 25 | function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } 26 | function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } 27 | function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function md5_vm_test() 33 | { 34 | return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; 35 | } 36 | 37 | /* 38 | * Calculate the MD5 of an array of little-endian words, and a bit length 39 | keep 40 | */ 41 | function core_md5(x, len) 42 | { 43 | /* append padding */ 44 | x[len >> 5] |= 0x80 << ((len) % 32); 45 | x[(((len + 64) >>> 9) << 4) + 14] = len; 46 | 47 | var a = 1732584193; 48 | var b = -271733879; 49 | var c = -1732584194; 50 | var d = 271733878; 51 | 52 | for(var i = 0; i < x.length; i += 16) 53 | { 54 | var olda = a; 55 | var oldb = b; 56 | var oldc = c; 57 | var oldd = d; 58 | 59 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 60 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 61 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 62 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 63 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 64 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 65 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 66 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 67 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 68 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 69 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 70 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 71 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 72 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 73 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 74 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 75 | 76 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 77 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 78 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 79 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 80 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 81 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 82 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 83 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 84 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 85 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 86 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 87 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 88 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 89 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 90 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 91 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 92 | 93 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 94 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 95 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 96 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 97 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 98 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 99 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 100 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 101 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 102 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 103 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 104 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 105 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 106 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 107 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 108 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 109 | 110 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 111 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 112 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 113 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 114 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 115 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 116 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 117 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 118 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 119 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 120 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 121 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 122 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 123 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 124 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 125 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 126 | 127 | a = safe_add(a, olda); 128 | b = safe_add(b, oldb); 129 | c = safe_add(c, oldc); 130 | d = safe_add(d, oldd); 131 | } 132 | return Array(a, b, c, d); 133 | 134 | } 135 | 136 | /* 137 | * These functions implement the four basic operations the algorithm uses. 138 | */ 139 | function md5_cmn(q, a, b, x, s, t) 140 | { 141 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 142 | } 143 | function md5_ff(a, b, c, d, x, s, t) 144 | { 145 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 146 | } 147 | function md5_gg(a, b, c, d, x, s, t) 148 | { 149 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 150 | } 151 | function md5_hh(a, b, c, d, x, s, t) 152 | { 153 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 154 | } 155 | function md5_ii(a, b, c, d, x, s, t) 156 | { 157 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 158 | } 159 | 160 | /* 161 | * Calculate the HMAC-MD5, of a key and some data 162 | */ 163 | function core_hmac_md5(key, data) 164 | { 165 | var bkey = str2binl(key); 166 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); 167 | 168 | var ipad = Array(16), opad = Array(16); 169 | for(var i = 0; i < 16; i++) 170 | { 171 | ipad[i] = bkey[i] ^ 0x36363636; 172 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 173 | } 174 | 175 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); 176 | return core_md5(opad.concat(hash), 512 + 128); 177 | } 178 | 179 | /* 180 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 181 | * to work around bugs in some JS interpreters. 182 | */ 183 | function safe_add(x, y) 184 | { 185 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 186 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 187 | return (msw << 16) | (lsw & 0xFFFF); 188 | } 189 | 190 | /* 191 | * Bitwise rotate a 32-bit number to the left. 192 | */ 193 | function bit_rol(num, cnt) 194 | { 195 | return (num << cnt) | (num >>> (32 - cnt)); 196 | } 197 | 198 | /* 199 | * Convert a string to an array of little-endian words 200 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. 201 | keep 202 | */ 203 | function str2binl(str) 204 | { 205 | var bin = Array(); 206 | var mask = (1 << chrsz) - 1; 207 | for(var i = 0; i < str.length * chrsz; i += chrsz) 208 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); 209 | return bin; 210 | } 211 | 212 | /* 213 | * Convert an array of little-endian words to a string 214 | */ 215 | function binl2str(bin) 216 | { 217 | var str = ""; 218 | var mask = (1 << chrsz) - 1; 219 | for(var i = 0; i < bin.length * 32; i += chrsz) 220 | str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); 221 | return str; 222 | } 223 | 224 | /* 225 | * Convert an array of little-endian words to a hex string. 226 | keep 227 | */ 228 | function binl2hex(binarray) 229 | { 230 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 231 | var str = ""; 232 | for(var i = 0; i < binarray.length * 4; i++) 233 | { 234 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + 235 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); 236 | } 237 | return str; 238 | } 239 | 240 | /* 241 | * Convert an array of little-endian words to a base-64 string 242 | */ 243 | function binl2b64(binarray) 244 | { 245 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 246 | var str = ""; 247 | for(var i = 0; i < binarray.length * 4; i += 3) 248 | { 249 | var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) 250 | | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) 251 | | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); 252 | for(var j = 0; j < 4; j++) 253 | { 254 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 255 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 256 | } 257 | } 258 | return str; 259 | } 260 | 261 | exports.hex = hex_md5; 262 | -------------------------------------------------------------------------------- /lib/mustache.js: -------------------------------------------------------------------------------- 1 | /* 2 | Shameless port of http://github.com/defunkt/mustache 3 | by Jan Lehnardt , 4 | Alexander Lang , 5 | Sebastian Cohnen 6 | 7 | Thanks @defunkt for the awesome code. 8 | 9 | See http://github.com/defunkt/mustache for more info. 10 | */ 11 | 12 | var Mustache = function() { 13 | var Renderer = function() {}; 14 | 15 | Renderer.prototype = { 16 | otag: "{{", 17 | ctag: "}}", 18 | pragmas: {}, 19 | buffer: [], 20 | pragmas_parsed: false, 21 | 22 | render: function(template, context, partials, in_recursion) { 23 | // fail fast 24 | if(template.indexOf(this.otag) == -1) { 25 | if(in_recursion) { 26 | return template; 27 | } else { 28 | this.send(template); 29 | } 30 | } 31 | 32 | if(!in_recursion) { 33 | this.buffer = []; 34 | } 35 | 36 | if(!this.pragmas_parsed) { 37 | template = this.render_pragmas(template); 38 | } 39 | var html = this.render_section(template, context, partials); 40 | if(in_recursion) { 41 | return this.render_tags(html, context, partials, in_recursion); 42 | } 43 | 44 | this.render_tags(html, context, partials, in_recursion); 45 | }, 46 | 47 | /* 48 | Sends parsed lines 49 | */ 50 | send: function(line) { 51 | if(line != "") { 52 | this.buffer.push(line); 53 | } 54 | }, 55 | 56 | /* 57 | Looks for %PRAGMAS 58 | */ 59 | render_pragmas: function(template) { 60 | this.pragmas_parsed = true; 61 | // no pragmas 62 | if(template.indexOf(this.otag + "%") == -1) { 63 | return template; 64 | } 65 | 66 | var that = this; 67 | var regex = new RegExp(this.otag + "%([\\w_-]+) ?([\\w]+=[\\w]+)?" 68 | + this.ctag); 69 | return template.replace(regex, function(match, pragma, options) { 70 | that.pragmas[pragma] = {}; 71 | if(options) { 72 | var opts = options.split("="); 73 | that.pragmas[pragma][opts[0]] = opts[1]; 74 | } 75 | return ""; 76 | // ignore unknown pragmas silently 77 | }); 78 | }, 79 | 80 | /* 81 | Tries to find a partial in the global scope and render it 82 | */ 83 | render_partial: function(name, context, partials) { 84 | if(typeof(context[name]) != "object") { 85 | throw({message: "subcontext for '" + name + "' is not an object"}); 86 | } 87 | if(!partials || !partials[name]) { 88 | throw({message: "unknown_partial '" + name + "'"}); 89 | } 90 | return this.render(partials[name], context[name], partials, true); 91 | }, 92 | 93 | /* 94 | Renders boolean and enumerable sections 95 | */ 96 | render_section: function(template, context, partials) { 97 | if(template.indexOf(this.otag + "#") == -1) { 98 | return template; 99 | } 100 | var that = this; 101 | // CSW - Added "+?" so it finds the tighest bound, not the widest 102 | var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + 103 | "\\s*([\\s\\S]+?)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); 104 | 105 | // for each {{#foo}}{{/foo}} section do... 106 | return template.replace(regex, function(match, name, content) { 107 | var value = that.find(name, context); 108 | if(that.is_array(value)) { // Enumerable, Let's loop! 109 | return that.map(value, function(row) { 110 | return that.render(content, that.merge(context, 111 | that.create_context(row)), partials, true); 112 | }).join(""); 113 | } else if (that.is_iterator(value)) { 114 | var result = []; 115 | var row; 116 | while (row = value()) { 117 | result.push(that.render(content, that.merge(context, 118 | that.create_context(row)), partials, true)); 119 | } // fuck buffering, works for now though. 120 | return result.join(''); 121 | } else if(value) { // boolean section 122 | return that.render(content, context, partials, true); 123 | } else { 124 | return ""; 125 | } 126 | }); 127 | }, 128 | 129 | /* 130 | Replace {{foo}} and friends with values from our view 131 | */ 132 | render_tags: function(template, context, partials, in_recursion) { 133 | // tit for tat 134 | var that = this; 135 | 136 | var new_regex = function() { 137 | return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\/#]+?)\\1?" + 138 | that.ctag + "+", "g"); 139 | }; 140 | 141 | var regex = new_regex(); 142 | var lines = template.split("\n"); 143 | for (var i=0; i < lines.length; i++) { 144 | lines[i] = lines[i].replace(regex, function(match, operator, name) { 145 | switch(operator) { 146 | case "!": // ignore comments 147 | return match; 148 | case "=": // set new delimiters, rebuild the replace regexp 149 | that.set_delimiters(name); 150 | regex = new_regex(); 151 | return ""; 152 | case ">": // render partial 153 | return that.render_partial(name, context, partials); 154 | case "{": // the triple mustache is unescaped 155 | return that.find(name, context); 156 | default: // escape the value 157 | return that.escape(that.find(name, context)); 158 | } 159 | }, this); 160 | if(!in_recursion) { 161 | this.send(lines[i]); 162 | } 163 | } 164 | return lines.join("\n"); 165 | }, 166 | 167 | set_delimiters: function(delimiters) { 168 | var dels = delimiters.split(" "); 169 | this.otag = this.escape_regex(dels[0]); 170 | this.ctag = this.escape_regex(dels[1]); 171 | }, 172 | 173 | escape_regex: function(text) { 174 | // thank you Simon Willison 175 | if(!arguments.callee.sRE) { 176 | var specials = [ 177 | '/', '.', '*', '+', '?', '|', 178 | '(', ')', '[', ']', '{', '}', '\\' 179 | ]; 180 | arguments.callee.sRE = new RegExp( 181 | '(\\' + specials.join('|\\') + ')', 'g' 182 | ); 183 | } 184 | return text.replace(arguments.callee.sRE, '\\$1'); 185 | }, 186 | 187 | /* 188 | find `name` in current `context`. That is find me a value 189 | from the view object 190 | */ 191 | find: function(name, context) { 192 | name = this.trim(name); 193 | if(typeof context[name] === "function" && !context[name].iterator) { 194 | return context[name].apply(context); 195 | } 196 | if(context[name] !== undefined) { 197 | return context[name]; 198 | } 199 | // silently ignore unkown variables 200 | return ""; 201 | }, 202 | 203 | // Utility methods 204 | 205 | /* 206 | Does away with nasty characters 207 | */ 208 | escape: function(s) { 209 | return ((s == null) ? "" : s).toString().replace(/[&"<>\\]/g, function(s) { 210 | switch(s) { 211 | case "&": return "&"; 212 | case "\\": return "\\\\";; 213 | case '"': return '\"';; 214 | case "<": return "<"; 215 | case ">": return ">"; 216 | default: return s; 217 | } 218 | }); 219 | }, 220 | 221 | /* 222 | Merges all properties of object `b` into object `a`. 223 | `b.property` overwrites a.property` 224 | */ 225 | merge: function(a, b) { 226 | var _new = {}; 227 | for(var name in a) { 228 | if(a.hasOwnProperty(name)) { 229 | _new[name] = a[name]; 230 | } 231 | }; 232 | for(var name in b) { 233 | if(b.hasOwnProperty(name)) { 234 | _new[name] = b[name]; 235 | } 236 | }; 237 | return _new; 238 | }, 239 | 240 | // by @langalex, support for arrays of strings 241 | create_context: function(_context) { 242 | if(this.is_object(_context)) { 243 | return _context; 244 | } else if(this.pragmas["IMPLICIT-ITERATOR"]) { 245 | var iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator || "."; 246 | var ctx = {}; 247 | ctx[iterator] = _context 248 | return ctx; 249 | } 250 | }, 251 | 252 | is_object: function(a) { 253 | return a && typeof a == "object"; 254 | }, 255 | 256 | is_array: function(a) { 257 | return Object.prototype.toString.call(a) === '[object Array]'; 258 | }, 259 | 260 | is_iterator : function(f) { 261 | return (typeof f === 'function' && f.iterator); 262 | }, 263 | 264 | /* 265 | Gets rid of leading and trailing whitespace 266 | */ 267 | trim: function(s) { 268 | return s.replace(/^\s*|\s*$/g, ""); 269 | }, 270 | 271 | /* 272 | Why, why, why? Because IE. Cry, cry cry. 273 | */ 274 | map: function(array, fn) { 275 | if (typeof array.map == "function") { 276 | return array.map(fn) 277 | } else { 278 | var r = []; 279 | var l = array.length; 280 | for(i=0;i|\\{|%)?([^\\/#\\^]+?)\\1?" + 159 | that.ctag + "+", "g"); 160 | }; 161 | 162 | var regex = new_regex(); 163 | var tag_replace_callback = function(match, operator, name) { 164 | switch(operator) { 165 | case "!": // ignore comments 166 | return ""; 167 | case "=": // set new delimiters, rebuild the replace regexp 168 | that.set_delimiters(name); 169 | regex = new_regex(); 170 | return ""; 171 | case ">": // render partial 172 | return that.render_partial(name, context, partials); 173 | case "{": // the triple mustache is unescaped 174 | return that.find(name, context); 175 | default: // escape the value 176 | return that.escape(that.find(name, context)); 177 | } 178 | }; 179 | var lines = template.split("\n"); 180 | for(var i = 0; i < lines.length; i++) { 181 | lines[i] = lines[i].replace(regex, tag_replace_callback, this); 182 | if(!in_recursion) { 183 | this.send(lines[i]); 184 | } 185 | } 186 | 187 | if(in_recursion) { 188 | return lines.join("\n"); 189 | } 190 | }, 191 | 192 | set_delimiters: function(delimiters) { 193 | var dels = delimiters.split(" "); 194 | this.otag = this.escape_regex(dels[0]); 195 | this.ctag = this.escape_regex(dels[1]); 196 | }, 197 | 198 | escape_regex: function(text) { 199 | // thank you Simon Willison 200 | if(!arguments.callee.sRE) { 201 | var specials = [ 202 | '/', '.', '*', '+', '?', '|', 203 | '(', ')', '[', ']', '{', '}', '\\' 204 | ]; 205 | arguments.callee.sRE = new RegExp( 206 | '(\\' + specials.join('|\\') + ')', 'g' 207 | ); 208 | } 209 | return text.replace(arguments.callee.sRE, '\\$1'); 210 | }, 211 | 212 | /* 213 | find `name` in current `context`. That is find me a value 214 | from the view object 215 | */ 216 | find: function(name, context) { 217 | name = this.trim(name); 218 | 219 | // Checks whether a value is thruthy or false or 0 220 | function is_kinda_truthy(bool) { 221 | return bool === false || bool === 0 || bool; 222 | } 223 | 224 | var value; 225 | if(is_kinda_truthy(context[name])) { 226 | value = context[name]; 227 | } else if(is_kinda_truthy(this.context[name])) { 228 | value = this.context[name]; 229 | } 230 | 231 | if(typeof value === "function") { 232 | return value.apply(context); 233 | } 234 | if(value !== undefined) { 235 | return value; 236 | } 237 | // silently ignore unkown variables 238 | return ""; 239 | }, 240 | 241 | // Utility methods 242 | 243 | /* includes tag */ 244 | includes: function(needle, haystack) { 245 | return haystack.indexOf(this.otag + needle) != -1; 246 | }, 247 | 248 | /* 249 | Does away with nasty characters 250 | */ 251 | escape: function(s) { 252 | s = String(s === null ? "" : s); 253 | return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { 254 | switch(s) { 255 | case "&": return "&"; 256 | case "\\": return "\\\\"; 257 | case '"': return '\"'; 258 | case "<": return "<"; 259 | case ">": return ">"; 260 | default: return s; 261 | } 262 | }); 263 | }, 264 | 265 | // by @langalex, support for arrays of strings 266 | create_context: function(_context) { 267 | if(this.is_object(_context)) { 268 | return _context; 269 | } else { 270 | var iterator = "."; 271 | if(this.pragmas["IMPLICIT-ITERATOR"]) { 272 | iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; 273 | } 274 | var ctx = {}; 275 | ctx[iterator] = _context; 276 | return ctx; 277 | } 278 | }, 279 | 280 | is_object: function(a) { 281 | return a && typeof a == "object"; 282 | }, 283 | 284 | is_array: function(a) { 285 | return Object.prototype.toString.call(a) === '[object Array]'; 286 | }, 287 | 288 | /* 289 | Gets rid of leading and trailing whitespace 290 | */ 291 | trim: function(s) { 292 | return s.replace(/^\s*|\s*$/g, ""); 293 | }, 294 | 295 | /* 296 | Why, why, why? Because IE. Cry, cry cry. 297 | */ 298 | map: function(array, fn) { 299 | if (typeof array.map == "function") { 300 | return array.map(fn); 301 | } else { 302 | var r = []; 303 | var l = array.length; 304 | for(var i = 0; i < l; i++) { 305 | r.push(fn(array[i])); 306 | } 307 | return r; 308 | } 309 | } 310 | }; 311 | 312 | return({ 313 | name: "mustache.js", 314 | version: "0.3.1-dev", 315 | 316 | /* 317 | Turns a template and view into HTML 318 | */ 319 | to_html: function(template, view, partials, send_fun) { 320 | var renderer = new Renderer(); 321 | if(send_fun) { 322 | renderer.send = send_fun; 323 | } 324 | renderer.render(template, view, partials); 325 | if(!send_fun) { 326 | return renderer.buffer.join("\n"); 327 | } 328 | }, 329 | escape : function(text) { 330 | return new Renderer().escape(text); 331 | } 332 | }); 333 | }(); 334 | 335 | exports.name = Mustache.name; 336 | exports.version = Mustache.version; 337 | 338 | exports.to_html = Mustache.to_html; 339 | exports.escape = Mustache.escape; 340 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.mustache.js: -------------------------------------------------------------------------------- 1 | /* 2 | Shameless port of a shameless port 3 | @defunkt => @janl => @aq 4 | 5 | See http://github.com/defunkt/mustache for more info. 6 | */ 7 | 8 | ;(function($) { 9 | 10 | /* 11 | mustache.js — Logic-less templates in JavaScript 12 | 13 | See http://mustache.github.com/ for more info. 14 | */ 15 | 16 | var Mustache = function() { 17 | var Renderer = function() {}; 18 | 19 | Renderer.prototype = { 20 | otag: "{{", 21 | ctag: "}}", 22 | pragmas: {}, 23 | buffer: [], 24 | pragmas_implemented: { 25 | "IMPLICIT-ITERATOR": true 26 | }, 27 | context: {}, 28 | 29 | render: function(template, context, partials, in_recursion) { 30 | // reset buffer & set context 31 | if(!in_recursion) { 32 | this.context = context; 33 | this.buffer = []; // TODO: make this non-lazy 34 | } 35 | 36 | // fail fast 37 | if(!this.includes("", template)) { 38 | if(in_recursion) { 39 | return template; 40 | } else { 41 | this.send(template); 42 | return; 43 | } 44 | } 45 | 46 | template = this.render_pragmas(template); 47 | var html = this.render_section(template, context, partials); 48 | if(in_recursion) { 49 | return this.render_tags(html, context, partials, in_recursion); 50 | } 51 | 52 | this.render_tags(html, context, partials, in_recursion); 53 | }, 54 | 55 | /* 56 | Sends parsed lines 57 | */ 58 | send: function(line) { 59 | if(line != "") { 60 | this.buffer.push(line); 61 | } 62 | }, 63 | 64 | /* 65 | Looks for %PRAGMAS 66 | */ 67 | render_pragmas: function(template) { 68 | // no pragmas 69 | if(!this.includes("%", template)) { 70 | return template; 71 | } 72 | 73 | var that = this; 74 | var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + 75 | this.ctag); 76 | return template.replace(regex, function(match, pragma, options) { 77 | if(!that.pragmas_implemented[pragma]) { 78 | throw({message: 79 | "This implementation of mustache doesn't understand the '" + 80 | pragma + "' pragma"}); 81 | } 82 | that.pragmas[pragma] = {}; 83 | if(options) { 84 | var opts = options.split("="); 85 | that.pragmas[pragma][opts[0]] = opts[1]; 86 | } 87 | return ""; 88 | // ignore unknown pragmas silently 89 | }); 90 | }, 91 | 92 | /* 93 | Tries to find a partial in the curent scope and render it 94 | */ 95 | render_partial: function(name, context, partials) { 96 | name = this.trim(name); 97 | if(!partials || partials[name] === undefined) { 98 | throw({message: "unknown_partial '" + name + "'"}); 99 | } 100 | if(typeof(context[name]) != "object") { 101 | return this.render(partials[name], context, partials, true); 102 | } 103 | return this.render(partials[name], context[name], partials, true); 104 | }, 105 | 106 | /* 107 | Renders inverted (^) and normal (#) sections 108 | */ 109 | render_section: function(template, context, partials) { 110 | if(!this.includes("#", template) && !this.includes("^", template)) { 111 | return template; 112 | } 113 | 114 | var that = this; 115 | // CSW - Added "+?" so it finds the tighest bound, not the widest 116 | var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + 117 | "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + 118 | "\\s*", "mg"); 119 | 120 | // for each {{#foo}}{{/foo}} section do... 121 | return template.replace(regex, function(match, type, name, content) { 122 | var value = that.find(name, context); 123 | if(type == "^") { // inverted section 124 | if(!value || that.is_array(value) && value.length === 0) { 125 | // false or empty list, render it 126 | return that.render(content, context, partials, true); 127 | } else { 128 | return ""; 129 | } 130 | } else if(type == "#") { // normal section 131 | if(that.is_array(value)) { // Enumerable, Let's loop! 132 | return that.map(value, function(row) { 133 | return that.render(content, that.create_context(row), 134 | partials, true); 135 | }).join(""); 136 | } else if(that.is_object(value)) { // Object, Use it as subcontext! 137 | return that.render(content, that.create_context(value), 138 | partials, true); 139 | } else if(typeof value === "function") { 140 | // higher order section 141 | return value.call(context, content, function(text) { 142 | return that.render(text, context, partials, true); 143 | }); 144 | } else if(value) { // boolean section 145 | return that.render(content, context, partials, true); 146 | } else { 147 | return ""; 148 | } 149 | } 150 | }); 151 | }, 152 | 153 | /* 154 | Replace {{foo}} and friends with values from our view 155 | */ 156 | render_tags: function(template, context, partials, in_recursion) { 157 | // tit for tat 158 | var that = this; 159 | 160 | var new_regex = function() { 161 | return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + 162 | that.ctag + "+", "g"); 163 | }; 164 | 165 | var regex = new_regex(); 166 | var tag_replace_callback = function(match, operator, name) { 167 | switch(operator) { 168 | case "!": // ignore comments 169 | return ""; 170 | case "=": // set new delimiters, rebuild the replace regexp 171 | that.set_delimiters(name); 172 | regex = new_regex(); 173 | return ""; 174 | case ">": // render partial 175 | return that.render_partial(name, context, partials); 176 | case "{": // the triple mustache is unescaped 177 | return that.find(name, context); 178 | default: // escape the value 179 | return that.escape(that.find(name, context)); 180 | } 181 | }; 182 | var lines = template.split("\n"); 183 | for(var i = 0; i < lines.length; i++) { 184 | lines[i] = lines[i].replace(regex, tag_replace_callback, this); 185 | if(!in_recursion) { 186 | this.send(lines[i]); 187 | } 188 | } 189 | 190 | if(in_recursion) { 191 | return lines.join("\n"); 192 | } 193 | }, 194 | 195 | set_delimiters: function(delimiters) { 196 | var dels = delimiters.split(" "); 197 | this.otag = this.escape_regex(dels[0]); 198 | this.ctag = this.escape_regex(dels[1]); 199 | }, 200 | 201 | escape_regex: function(text) { 202 | // thank you Simon Willison 203 | if(!arguments.callee.sRE) { 204 | var specials = [ 205 | '/', '.', '*', '+', '?', '|', 206 | '(', ')', '[', ']', '{', '}', '\\' 207 | ]; 208 | arguments.callee.sRE = new RegExp( 209 | '(\\' + specials.join('|\\') + ')', 'g' 210 | ); 211 | } 212 | return text.replace(arguments.callee.sRE, '\\$1'); 213 | }, 214 | 215 | /* 216 | find `name` in current `context`. That is find me a value 217 | from the view object 218 | */ 219 | find: function(name, context) { 220 | name = this.trim(name); 221 | 222 | // Checks whether a value is thruthy or false or 0 223 | function is_kinda_truthy(bool) { 224 | return bool === false || bool === 0 || bool; 225 | } 226 | 227 | var value; 228 | if(is_kinda_truthy(context[name])) { 229 | value = context[name]; 230 | } else if(is_kinda_truthy(this.context[name])) { 231 | value = this.context[name]; 232 | } 233 | 234 | if(typeof value === "function") { 235 | return value.apply(context); 236 | } 237 | if(value !== undefined) { 238 | return value; 239 | } 240 | // silently ignore unkown variables 241 | return ""; 242 | }, 243 | 244 | // Utility methods 245 | 246 | /* includes tag */ 247 | includes: function(needle, haystack) { 248 | return haystack.indexOf(this.otag + needle) != -1; 249 | }, 250 | 251 | /* 252 | Does away with nasty characters 253 | */ 254 | escape: function(s) { 255 | s = String(s === null ? "" : s); 256 | return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { 257 | switch(s) { 258 | case "&": return "&"; 259 | case "\\": return "\\\\"; 260 | case '"': return '\"'; 261 | case "<": return "<"; 262 | case ">": return ">"; 263 | default: return s; 264 | } 265 | }); 266 | }, 267 | 268 | // by @langalex, support for arrays of strings 269 | create_context: function(_context) { 270 | if(this.is_object(_context)) { 271 | return _context; 272 | } else { 273 | var iterator = "."; 274 | if(this.pragmas["IMPLICIT-ITERATOR"]) { 275 | iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; 276 | } 277 | var ctx = {}; 278 | ctx[iterator] = _context; 279 | return ctx; 280 | } 281 | }, 282 | 283 | is_object: function(a) { 284 | return a && typeof a == "object"; 285 | }, 286 | 287 | is_array: function(a) { 288 | return Object.prototype.toString.call(a) === '[object Array]'; 289 | }, 290 | 291 | /* 292 | Gets rid of leading and trailing whitespace 293 | */ 294 | trim: function(s) { 295 | return s.replace(/^\s*|\s*$/g, ""); 296 | }, 297 | 298 | /* 299 | Why, why, why? Because IE. Cry, cry cry. 300 | */ 301 | map: function(array, fn) { 302 | if (typeof array.map == "function") { 303 | return array.map(fn); 304 | } else { 305 | var r = []; 306 | var l = array.length; 307 | for(var i = 0; i < l; i++) { 308 | r.push(fn(array[i])); 309 | } 310 | return r; 311 | } 312 | } 313 | }; 314 | 315 | return({ 316 | name: "mustache.js", 317 | version: "0.3.1-dev", 318 | 319 | /* 320 | Turns a template and view into HTML 321 | */ 322 | to_html: function(template, view, partials, send_fun) { 323 | var renderer = new Renderer(); 324 | if(send_fun) { 325 | renderer.send = send_fun; 326 | } 327 | renderer.render(template, view, partials); 328 | if(!send_fun) { 329 | return renderer.buffer.join("\n"); 330 | } 331 | }, 332 | escape : function(text) { 333 | return new Renderer().escape(text); 334 | } 335 | }); 336 | }(); 337 | 338 | $.mustache = function(template, view, partials) { 339 | return Mustache.to_html(template, view, partials); 340 | }; 341 | 342 | $.mustache.escape = function(text) { 343 | return Mustache.escape(text); 344 | }; 345 | 346 | })(jQuery); 347 | -------------------------------------------------------------------------------- /vendor/couchapp/_attachments/jquery.evently.js: -------------------------------------------------------------------------------- 1 | // $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/ 2 | function $$(node) { 3 | var data = $(node).data("$$"); 4 | if (data) { 5 | return data; 6 | } else { 7 | data = {}; 8 | $(node).data("$$", data); 9 | return data; 10 | } 11 | }; 12 | 13 | (function($) { 14 | // utility functions used in the implementation 15 | 16 | function forIn(obj, fun) { 17 | var name; 18 | for (name in obj) { 19 | if (obj.hasOwnProperty(name)) { 20 | fun(name, obj[name]); 21 | } 22 | } 23 | }; 24 | $.forIn = forIn; 25 | function funViaString(fun) { 26 | if (fun && fun.match && fun.match(/^function/)) { 27 | eval("var f = "+fun); 28 | if (typeof f == "function") { 29 | return function() { 30 | try { 31 | return f.apply(this, arguments); 32 | } catch(e) { 33 | // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION 34 | $.log({"message": "Error in evently function.", "error": e, "src" : fun}); 35 | throw(e); 36 | } 37 | }; 38 | } 39 | } 40 | return fun; 41 | }; 42 | 43 | function runIfFun(me, fun, args) { 44 | // if the field is a function, call it, bound to the widget 45 | var f = funViaString(fun); 46 | if (typeof f == "function") { 47 | return f.apply(me, args); 48 | } else { 49 | return fun; 50 | } 51 | } 52 | 53 | $.evently = { 54 | connect : function(source, target, events) { 55 | events.forEach(function(ev) { 56 | $(source).bind(ev, function() { 57 | var args = $.makeArray(arguments); 58 | // remove the original event to keep from stacking args extra deep 59 | // it would be nice if jquery had a way to pass the original 60 | // event to the trigger method. 61 | args.shift(); 62 | $(target).trigger(ev, args); 63 | return false; 64 | }); 65 | }); 66 | }, 67 | paths : [], 68 | changesDBs : {}, 69 | changesOpts : {} 70 | }; 71 | 72 | function extractFrom(name, evs) { 73 | return evs[name]; 74 | }; 75 | 76 | function extractEvents(name, ddoc) { 77 | // extract events from ddoc.evently and ddoc.vendor.*.evently 78 | var events = [true, {}]; 79 | $.forIn(ddoc.vendor, function(k, v) { 80 | if (v.evently && v.evently[name]) { 81 | events.push(v.evently[name]); 82 | } 83 | }); 84 | if (ddoc.evently[name]) {events.push(ddoc.evently[name]);} 85 | return $.extend.apply(null, events); 86 | } 87 | 88 | $.fn.evently = function(events, app, args) { 89 | var elem = $(this); 90 | // store the app on the element for later use 91 | if (app) { 92 | $$(elem).app = app; 93 | } 94 | 95 | if (typeof events == "string") { 96 | events = extractEvents(events, app.ddoc); 97 | } 98 | 99 | $$(elem).evently = events; 100 | // setup the handlers onto elem 101 | forIn(events, function(name, h) { 102 | eventlyHandler(elem, name, h, args); 103 | }); 104 | 105 | if (events._init) { 106 | // $.log("ev _init", elem); 107 | elem.trigger("_init", args); 108 | } 109 | 110 | if (app && events._changes) { 111 | $("body").bind("evently-changes-"+app.db.name, function() { 112 | // we want to unbind this function when the element is deleted. 113 | // maybe jquery 1.4.2 has this covered? 114 | // $.log('changes', elem); 115 | elem.trigger("_changes"); 116 | }); 117 | followChanges(app); 118 | elem.trigger("_changes"); 119 | } 120 | }; 121 | 122 | // eventlyHandler applies the user's handler (h) to the 123 | // elem, bound to trigger based on name. 124 | function eventlyHandler(elem, name, h, args) { 125 | if (h.path) { 126 | elem.pathbinder(name, h.path); 127 | } 128 | var f = funViaString(h); 129 | if (typeof f == "function") { 130 | elem.bind(name, {args:args}, f); 131 | } else if (typeof f == "string") { 132 | elem.bind(name, {args:args}, function() { 133 | $(this).trigger(f, arguments); 134 | return false; 135 | }); 136 | } else if ($.isArray(h)) { 137 | // handle arrays recursively 138 | for (var i=0; i < h.length; i++) { 139 | eventlyHandler(elem, name, h[i], args); 140 | } 141 | } else { 142 | // an object is using the evently / mustache template system 143 | if (h.fun) { 144 | elem.bind(name, {args:args}, funViaString(h.fun)); 145 | } 146 | // templates, selectors, etc are intepreted 147 | // when our named event is triggered. 148 | elem.bind(name, {args:args}, function() { 149 | renderElement($(this), h, arguments); 150 | return false; 151 | }); 152 | } 153 | }; 154 | 155 | $.fn.replace = function(elem) { 156 | // $.log("Replace", this) 157 | $(this).empty().append(elem); 158 | }; 159 | 160 | // todo: ability to call this 161 | // to render and "prepend/append/etc" a new element to the host element (me) 162 | // as well as call this in a way that replaces the host elements content 163 | // this would be easy if there is a simple way to get at the element we just appended 164 | // (as html) so that we can attache the selectors 165 | function renderElement(me, h, args, qrun, arun) { 166 | // if there's a query object we run the query, 167 | // and then call the data function with the response. 168 | if (h.before && (!qrun || !arun)) { 169 | funViaString(h.before).apply(me, args); 170 | } 171 | if (h.async && !arun) { 172 | runAsync(me, h, args) 173 | } else if (h.query && !qrun) { 174 | // $.log("query before renderElement", arguments) 175 | runQuery(me, h, args) 176 | } else { 177 | // $.log("renderElement") 178 | // $.log(me, h, args, qrun) 179 | // otherwise we just render the template with the current args 180 | var selectors = runIfFun(me, h.selectors, args); 181 | var act = (h.render || "replace").replace(/\s/g,""); 182 | var app = $$(me).app; 183 | if (h.mustache) { 184 | // $.log("rendering", h.mustache) 185 | var newElem = mustachioed(me, h, args); 186 | me[act](newElem); 187 | } 188 | if (selectors) { 189 | if (act == "replace") { 190 | var s = me; 191 | } else { 192 | var s = newElem; 193 | } 194 | forIn(selectors, function(selector, handlers) { 195 | // $.log("selector", selector); 196 | // $.log("selected", $(selector, s)); 197 | $(selector, s).evently(handlers, app, args); 198 | // $.log("applied", selector); 199 | }); 200 | } 201 | if (h.after) { 202 | runIfFun(me, h.after, args); 203 | // funViaString(h.after).apply(me, args); 204 | } 205 | } 206 | }; 207 | 208 | // todo this should return the new element 209 | function mustachioed(me, h, args) { 210 | return $($.mustache( 211 | runIfFun(me, h.mustache, args), 212 | runIfFun(me, h.data, args), 213 | runIfFun(me, h.partials, args))); 214 | }; 215 | 216 | function runAsync(me, h, args) { 217 | // the callback is the first argument 218 | funViaString(h.async).apply(me, [function() { 219 | renderElement(me, h, 220 | $.argsToArray(arguments).concat($.argsToArray(args)), false, true); 221 | }].concat($.argsToArray(args))); 222 | }; 223 | 224 | 225 | function runQuery(me, h, args) { 226 | // $.log("runQuery: args", args) 227 | var app = $$(me).app; 228 | var qu = runIfFun(me, h.query, args); 229 | var qType = qu.type; 230 | var viewName = qu.view; 231 | var userSuccess = qu.success; 232 | // $.log("qType", qType) 233 | 234 | var q = {}; 235 | forIn(qu, function(k, v) { 236 | q[k] = v; 237 | }); 238 | 239 | if (qType == "newRows") { 240 | q.success = function(resp) { 241 | // $.log("runQuery newRows success", resp.rows.length, me, resp) 242 | resp.rows.reverse().forEach(function(row) { 243 | renderElement(me, h, [row].concat($.argsToArray(args)), true) 244 | }); 245 | if (userSuccess) userSuccess(resp); 246 | }; 247 | newRows(me, app, viewName, q); 248 | } else { 249 | q.success = function(resp) { 250 | // $.log("runQuery success", resp) 251 | renderElement(me, h, [resp].concat($.argsToArray(args)), true); 252 | userSuccess && userSuccess(resp); 253 | }; 254 | // $.log(app) 255 | app.view(viewName, q); 256 | } 257 | } 258 | 259 | // this is for the items handler 260 | // var lastViewId, highKey, inFlight; 261 | // this needs to key per elem 262 | function newRows(elem, app, view, opts) { 263 | // $.log("newRows", arguments); 264 | // on success we'll set the top key 265 | var thisViewId, successCallback = opts.success, full = false; 266 | function successFun(resp) { 267 | // $.log("newRows success", resp) 268 | $$(elem).inFlight = false; 269 | var JSONhighKey = JSON.stringify($$(elem).highKey); 270 | resp.rows = resp.rows.filter(function(r) { 271 | return JSON.stringify(r.key) != JSONhighKey; 272 | }); 273 | if (resp.rows.length > 0) { 274 | if (opts.descending) { 275 | $$(elem).highKey = resp.rows[0].key; 276 | } else { 277 | $$(elem).highKey = resp.rows[resp.rows.length -1].key; 278 | } 279 | }; 280 | if (successCallback) {successCallback(resp, full)}; 281 | }; 282 | opts.success = successFun; 283 | 284 | if (opts.descending) { 285 | thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : ""); 286 | } else { 287 | thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : ""); 288 | } 289 | // $.log(["thisViewId",thisViewId]) 290 | // for query we'll set keys 291 | if (thisViewId == $$(elem).lastViewId) { 292 | // we only want the rows newer than changesKey 293 | var hk = $$(elem).highKey; 294 | if (hk !== undefined) { 295 | if (opts.descending) { 296 | opts.endkey = hk; 297 | // opts.inclusive_end = false; 298 | } else { 299 | opts.startkey = hk; 300 | } 301 | } 302 | // $.log("add view rows", opts) 303 | if (!$$(elem).inFlight) { 304 | $$(elem).inFlight = true; 305 | app.view(view, opts); 306 | } 307 | } else { 308 | // full refresh 309 | // $.log("new view stuff") 310 | full = true; 311 | $$(elem).lastViewId = thisViewId; 312 | $$(elem).highKey = undefined; 313 | $$(elem).inFlight = true; 314 | app.view(view, opts); 315 | } 316 | }; 317 | 318 | // only start one changes listener per db 319 | function followChanges(app) { 320 | var dbName = app.db.name, changeEvent = function(resp) { 321 | $("body").trigger("evently-changes-"+dbName, [resp]); 322 | }; 323 | if (!$.evently.changesDBs[dbName]) { 324 | if (app.db.changes) { 325 | // new api in jquery.couch.js 1.0 326 | app.db.changes(null, $.evently.changesOpts).onChange(changeEvent); 327 | } else { 328 | // in case you are still on CouchDB 0.11 ;) deprecated. 329 | connectToChanges(app, changeEvent); 330 | } 331 | $.evently.changesDBs[dbName] = true; 332 | } 333 | } 334 | $.evently.followChanges = followChanges; 335 | // deprecated. use db.changes() from jquery.couch.js 336 | // this does not have an api for closing changes request. 337 | function connectToChanges(app, fun, update_seq) { 338 | function changesReq(seq) { 339 | var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq; 340 | if ($.evently.changesOpts.include_docs) { 341 | url = url + "&include_docs=true"; 342 | } 343 | $.ajax({ 344 | url: url, 345 | contentType: "application/json", 346 | dataType: "json", 347 | complete: function(req) { 348 | var resp = $.httpData(req, "json"); 349 | fun(resp); 350 | connectToChanges(app, fun, resp.last_seq); 351 | } 352 | }); 353 | }; 354 | if (update_seq) { 355 | changesReq(update_seq); 356 | } else { 357 | app.db.info({success: function(db_info) { 358 | changesReq(db_info.update_seq); 359 | }}); 360 | } 361 | }; 362 | 363 | })(jQuery); 364 | -------------------------------------------------------------------------------- /vendor/couchapp/lib/markdown.js: -------------------------------------------------------------------------------- 1 | // 2 | // showdown.js -- A javascript port of Markdown. 3 | // 4 | // Copyright (c) 2007 John Fraser. 5 | // 6 | // Original Markdown Copyright (c) 2004-2005 John Gruber 7 | // 8 | // 9 | // Redistributable under a BSD-style open source license. 10 | // See license.txt for more information. 11 | // 12 | // The full source distribution is at: 13 | // 14 | // A A L 15 | // T C A 16 | // T K B 17 | // 18 | // 19 | // 20 | 21 | // 22 | // Wherever possible, Showdown is a straight, line-by-line port 23 | // of the Perl version of Markdown. 24 | // 25 | // This is not a normal parser design; it's basically just a 26 | // series of string substitutions. It's hard to read and 27 | // maintain this way, but keeping Showdown close to the original 28 | // design makes it easier to port new features. 29 | // 30 | // More importantly, Showdown behaves like markdown.pl in most 31 | // edge cases. So web applications can do client-side preview 32 | // in Javascript, and then build identical HTML on the server. 33 | // 34 | // This port needs the new RegExp functionality of ECMA 262, 35 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 36 | // should do fine. Even with the new regular expression features, 37 | // We do a lot of work to emulate Perl's regex functionality. 38 | // The tricky changes in this file mostly have the "attacklab:" 39 | // label. Major or self-explanatory changes don't. 40 | // 41 | // Smart diff tools like Araxis Merge will be able to match up 42 | // this file with markdown.pl in a useful way. A little tweaking 43 | // helps: in a copy of markdown.pl, replace "#" with "//" and 44 | // replace "$text" with "text". Be sure to ignore whitespace 45 | // and line endings. 46 | // 47 | 48 | 49 | // 50 | // Showdown usage: 51 | // 52 | // var text = "Markdown *rocks*."; 53 | // 54 | // var markdown = require("markdown"); 55 | // var html = markdown.encode(text); 56 | // 57 | // print(html); 58 | // 59 | // Note: move the sample code to the bottom of this 60 | // file before uncommenting it. 61 | // 62 | 63 | 64 | // 65 | // Globals: 66 | // 67 | 68 | // Global hashes, used by various utility routines 69 | var g_urls; 70 | var g_titles; 71 | var g_html_blocks; 72 | 73 | // Used to track when we're inside an ordered or unordered list 74 | // (see _ProcessListItems() for details): 75 | var g_list_level = 0; 76 | 77 | 78 | exports.makeHtml = function(text) { 79 | // 80 | // Main function. The order in which other subs are called here is 81 | // essential. Link and image substitutions need to happen before 82 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 83 | // and tags get encoded. 84 | // 85 | 86 | // Clear the global hashes. If we don't clear these, you get conflicts 87 | // from other articles when generating a page which contains more than 88 | // one article (e.g. an index page that shows the N most recent 89 | // articles): 90 | g_urls = new Array(); 91 | g_titles = new Array(); 92 | g_html_blocks = new Array(); 93 | 94 | // attacklab: Replace ~ with ~T 95 | // This lets us use tilde as an escape char to avoid md5 hashes 96 | // The choice of character is arbitray; anything that isn't 97 | // magic in Markdown will work. 98 | text = text.replace(/~/g,"~T"); 99 | 100 | // attacklab: Replace $ with ~D 101 | // RegExp interprets $ as a special character 102 | // when it's in a replacement string 103 | text = text.replace(/\$/g,"~D"); 104 | 105 | // Standardize line endings 106 | text = text.replace(/\r\n/g,"\n"); // DOS to Unix 107 | text = text.replace(/\r/g,"\n"); // Mac to Unix 108 | 109 | // Make sure text begins and ends with a couple of newlines: 110 | text = "\n\n" + text + "\n\n"; 111 | 112 | // Convert all tabs to spaces. 113 | text = _Detab(text); 114 | 115 | // Strip any lines consisting only of spaces and tabs. 116 | // This makes subsequent regexen easier to write, because we can 117 | // match consecutive blank lines with /\n+/ instead of something 118 | // contorted like /[ \t]*\n+/ . 119 | text = text.replace(/^[ \t]+$/mg,""); 120 | 121 | // Turn block-level HTML blocks into hash entries 122 | text = _HashHTMLBlocks(text); 123 | 124 | // Strip link definitions, store in hashes. 125 | text = _StripLinkDefinitions(text); 126 | 127 | text = _RunBlockGamut(text); 128 | 129 | text = _UnescapeSpecialChars(text); 130 | 131 | // attacklab: Restore dollar signs 132 | text = text.replace(/~D/g,"$$"); 133 | 134 | // attacklab: Restore tildes 135 | text = text.replace(/~T/g,"~"); 136 | return text; 137 | } 138 | 139 | 140 | var _StripLinkDefinitions = function(text) { 141 | // 142 | // Strips link definitions from text, stores the URLs and titles in 143 | // hash references. 144 | // 145 | 146 | // Link defs are in the form: ^[id]: url "optional title" 147 | 148 | /* 149 | var text = text.replace(/ 150 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 151 | [ \t]* 152 | \n? // maybe *one* newline 153 | [ \t]* 154 | ? // url = $2 155 | [ \t]* 156 | \n? // maybe one newline 157 | [ \t]* 158 | (?: 159 | (\n*) // any lines skipped = $3 attacklab: lookbehind removed 160 | ["(] 161 | (.+?) // title = $4 162 | [")] 163 | [ \t]* 164 | )? // title is optional 165 | (?:\n+|$) 166 | /gm, 167 | function(){...}); 168 | */ 169 | var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm, 170 | function (wholeMatch,m1,m2,m3,m4) { 171 | m1 = m1.toLowerCase(); 172 | g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive 173 | if (m3) { 174 | // Oops, found blank lines, so it's not a title. 175 | // Put back the parenthetical statement we stole. 176 | return m3+m4; 177 | } else if (m4) { 178 | g_titles[m1] = m4.replace(/"/g,"""); 179 | } 180 | 181 | // Completely remove the definition from the text 182 | return ""; 183 | } 184 | ); 185 | 186 | return text; 187 | } 188 | 189 | 190 | var _HashHTMLBlocks = function(text) { 191 | // attacklab: Double up blank lines to reduce lookaround 192 | text = text.replace(/\n/g,"\n\n"); 193 | 194 | // Hashify HTML blocks: 195 | // We only want to do this for block-level HTML tags, such as headers, 196 | // lists, and tables. That's because we still want to wrap

    s around 197 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 198 | // phrase emphasis, and spans. The list of tags we're looking for is 199 | // hard-coded: 200 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 201 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 202 | 203 | // First, look for nested blocks, e.g.: 204 | //

    205 | //
    206 | // tags for inner block must be indented. 207 | //
    208 | //
    209 | // 210 | // The outermost tags must start at the left margin for this to match, and 211 | // the inner nested divs must be indented. 212 | // We need to do this before the next, more liberal match, because the next 213 | // match will start at the first `
    ` and stop at the first `
    `. 214 | 215 | // attacklab: This regex can be expensive when it fails. 216 | /* 217 | var text = text.replace(/ 218 | ( // save in $1 219 | ^ // start of line (with /m) 220 | <($block_tags_a) // start tag = $2 221 | \b // word break 222 | // attacklab: hack around khtml/pcre bug... 223 | [^\r]*?\n // any number of lines, minimally matching 224 | // the matching end tag 225 | [ \t]* // trailing spaces/tabs 226 | (?=\n+) // followed by a newline 227 | ) // attacklab: there are sentinel newlines at end of document 228 | /gm,function(){...}}; 229 | */ 230 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,hashElement); 231 | 232 | // 233 | // Now match more liberally, simply from `\n` to `\n` 234 | // 235 | 236 | /* 237 | var text = text.replace(/ 238 | ( // save in $1 239 | ^ // start of line (with /m) 240 | <($block_tags_b) // start tag = $2 241 | \b // word break 242 | // attacklab: hack around khtml/pcre bug... 243 | [^\r]*? // any number of lines, minimally matching 244 | .* // the matching end tag 245 | [ \t]* // trailing spaces/tabs 246 | (?=\n+) // followed by a newline 247 | ) // attacklab: there are sentinel newlines at end of document 248 | /gm,function(){...}}; 249 | */ 250 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm,hashElement); 251 | 252 | // Special case just for
    . It was easier to make a special case than 253 | // to make the other regex more complicated. 254 | 255 | /* 256 | text = text.replace(/ 257 | ( // save in $1 258 | \n\n // Starting after a blank line 259 | [ ]{0,3} 260 | (<(hr) // start tag = $2 261 | \b // word break 262 | ([^<>])*? // 263 | \/?>) // the matching end tag 264 | [ \t]* 265 | (?=\n{2,}) // followed by a blank line 266 | ) 267 | /g,hashElement); 268 | */ 269 | text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); 270 | 271 | // Special case for standalone HTML comments: 272 | 273 | /* 274 | text = text.replace(/ 275 | ( // save in $1 276 | \n\n // Starting after a blank line 277 | [ ]{0,3} // attacklab: g_tab_width - 1 278 | 281 | [ \t]* 282 | (?=\n{2,}) // followed by a blank line 283 | ) 284 | /g,hashElement); 285 | */ 286 | text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); 287 | 288 | // PHP and ASP-style processor instructions ( and <%...%>) 289 | 290 | /* 291 | text = text.replace(/ 292 | (?: 293 | \n\n // Starting after a blank line 294 | ) 295 | ( // save in $1 296 | [ ]{0,3} // attacklab: g_tab_width - 1 297 | (?: 298 | <([?%]) // $2 299 | [^\r]*? 300 | \2> 301 | ) 302 | [ \t]* 303 | (?=\n{2,}) // followed by a blank line 304 | ) 305 | /g,hashElement); 306 | */ 307 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); 308 | 309 | // attacklab: Undo double lines (see comment at top of this function) 310 | text = text.replace(/\n\n/g,"\n"); 311 | return text; 312 | } 313 | 314 | var hashElement = function(wholeMatch,m1) { 315 | var blockText = m1; 316 | 317 | // Undo double lines 318 | blockText = blockText.replace(/\n\n/g,"\n"); 319 | blockText = blockText.replace(/^\n/,""); 320 | 321 | // strip trailing blank lines 322 | blockText = blockText.replace(/\n+$/g,""); 323 | 324 | // Replace the element text with a marker ("~KxK" where x is its key) 325 | blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; 326 | 327 | return blockText; 328 | }; 329 | 330 | var _RunBlockGamut = function(text) { 331 | // 332 | // These are all the transformations that form block-level 333 | // tags like paragraphs, headers, and list items. 334 | // 335 | text = _DoHeaders(text); 336 | 337 | // Do Horizontal Rules: 338 | var key = hashBlock("
    "); 339 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); 340 | text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); 341 | text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); 342 | 343 | text = _DoLists(text); 344 | text = _DoCodeBlocks(text); 345 | text = _DoBlockQuotes(text); 346 | 347 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 348 | // was to escape raw HTML in the original Markdown source. This time, 349 | // we're escaping the markup we've just created, so that we don't wrap 350 | //

    tags around block-level tags. 351 | text = _HashHTMLBlocks(text); 352 | text = _FormParagraphs(text); 353 | 354 | return text; 355 | } 356 | 357 | 358 | var _RunSpanGamut = function(text) { 359 | // 360 | // These are all the transformations that occur *within* block-level 361 | // tags like paragraphs, headers, and list items. 362 | // 363 | 364 | text = _DoCodeSpans(text); 365 | text = _EscapeSpecialCharsWithinTagAttributes(text); 366 | text = _EncodeBackslashEscapes(text); 367 | 368 | // Process anchor and image tags. Images must come first, 369 | // because ![foo][f] looks like an anchor. 370 | text = _DoImages(text); 371 | text = _DoAnchors(text); 372 | 373 | // Make links out of things like `` 374 | // Must come after _DoAnchors(), because you can use < and > 375 | // delimiters in inline links like [this](). 376 | text = _DoAutoLinks(text); 377 | text = _EncodeAmpsAndAngles(text); 378 | text = _DoItalicsAndBold(text); 379 | 380 | // Do hard breaks: 381 | text = text.replace(/ +\n/g,"
    \n"); 382 | 383 | return text; 384 | } 385 | 386 | var _EscapeSpecialCharsWithinTagAttributes = function(text) { 387 | // 388 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 389 | // don't conflict with their use in Markdown for code, italics and strong. 390 | // 391 | 392 | // Build a regex to find HTML tags and comments. See Friedl's 393 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 394 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; 395 | 396 | text = text.replace(regex, function(wholeMatch) { 397 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); 398 | tag = escapeCharacters(tag,"\\`*_"); 399 | return tag; 400 | }); 401 | 402 | return text; 403 | } 404 | 405 | var _DoAnchors = function(text) { 406 | // 407 | // Turn Markdown link shortcuts into XHTML
    tags. 408 | // 409 | // 410 | // First, handle reference-style links: [link text] [id] 411 | // 412 | 413 | /* 414 | text = text.replace(/ 415 | ( // wrap whole match in $1 416 | \[ 417 | ( 418 | (?: 419 | \[[^\]]*\] // allow brackets nested one level 420 | | 421 | [^\[] // or anything else 422 | )* 423 | ) 424 | \] 425 | 426 | [ ]? // one optional space 427 | (?:\n[ ]*)? // one optional newline followed by spaces 428 | 429 | \[ 430 | (.*?) // id = $3 431 | \] 432 | )()()()() // pad remaining backreferences 433 | /g,_DoAnchors_callback); 434 | */ 435 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); 436 | 437 | // 438 | // Next, inline-style links: [link text](url "optional title") 439 | // 440 | 441 | /* 442 | text = text.replace(/ 443 | ( // wrap whole match in $1 444 | \[ 445 | ( 446 | (?: 447 | \[[^\]]*\] // allow brackets nested one level 448 | | 449 | [^\[\]] // or anything else 450 | ) 451 | ) 452 | \] 453 | \( // literal paren 454 | [ \t]* 455 | () // no id, so leave $3 empty 456 | ? // href = $4 457 | [ \t]* 458 | ( // $5 459 | (['"]) // quote char = $6 460 | (.*?) // Title = $7 461 | \6 // matching quote 462 | [ \t]* // ignore any spaces/tabs between closing quote and ) 463 | )? // title is optional 464 | \) 465 | ) 466 | /g,writeAnchorTag); 467 | */ 468 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); 469 | 470 | // 471 | // Last, handle reference-style shortcuts: [link text] 472 | // These must come last in case you've also got [link test][1] 473 | // or [link test](/foo) 474 | // 475 | 476 | /* 477 | text = text.replace(/ 478 | ( // wrap whole match in $1 479 | \[ 480 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 481 | \] 482 | )()()()()() // pad rest of backreferences 483 | /g, writeAnchorTag); 484 | */ 485 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 486 | 487 | return text; 488 | } 489 | 490 | var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 491 | if (m7 == undefined) m7 = ""; 492 | var whole_match = m1; 493 | var link_text = m2; 494 | var link_id = m3.toLowerCase(); 495 | var url = m4; 496 | var title = m7; 497 | 498 | if (url == "") { 499 | if (link_id == "") { 500 | // lower-case and turn embedded newlines into spaces 501 | link_id = link_text.toLowerCase().replace(/ ?\n/g," "); 502 | } 503 | url = "#"+link_id; 504 | 505 | if (g_urls[link_id] != undefined) { 506 | url = g_urls[link_id]; 507 | if (g_titles[link_id] != undefined) { 508 | title = g_titles[link_id]; 509 | } 510 | } 511 | else { 512 | if (whole_match.search(/\(\s*\)$/m)>-1) { 513 | // Special case for explicit empty url 514 | url = ""; 515 | } else { 516 | return whole_match; 517 | } 518 | } 519 | } 520 | 521 | url = escapeCharacters(url,"*_"); 522 | var result = ""; 531 | 532 | return result; 533 | } 534 | 535 | 536 | var _DoImages = function(text) { 537 | // 538 | // Turn Markdown image shortcuts into tags. 539 | // 540 | 541 | // 542 | // First, handle reference-style labeled images: ![alt text][id] 543 | // 544 | 545 | /* 546 | text = text.replace(/ 547 | ( // wrap whole match in $1 548 | !\[ 549 | (.*?) // alt text = $2 550 | \] 551 | 552 | [ ]? // one optional space 553 | (?:\n[ ]*)? // one optional newline followed by spaces 554 | 555 | \[ 556 | (.*?) // id = $3 557 | \] 558 | )()()()() // pad rest of backreferences 559 | /g,writeImageTag); 560 | */ 561 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); 562 | 563 | // 564 | // Next, handle inline images: ![alt text](url "optional title") 565 | // Don't forget: encode * and _ 566 | 567 | /* 568 | text = text.replace(/ 569 | ( // wrap whole match in $1 570 | !\[ 571 | (.*?) // alt text = $2 572 | \] 573 | \s? // One optional whitespace character 574 | \( // literal paren 575 | [ \t]* 576 | () // no id, so leave $3 empty 577 | ? // src url = $4 578 | [ \t]* 579 | ( // $5 580 | (['"]) // quote char = $6 581 | (.*?) // title = $7 582 | \6 // matching quote 583 | [ \t]* 584 | )? // title is optional 585 | \) 586 | ) 587 | /g,writeImageTag); 588 | */ 589 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); 590 | 591 | return text; 592 | } 593 | 594 | var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 595 | var whole_match = m1; 596 | var alt_text = m2; 597 | var link_id = m3.toLowerCase(); 598 | var url = m4; 599 | var title = m7; 600 | 601 | if (!title) title = ""; 602 | 603 | if (url == "") { 604 | if (link_id == "") { 605 | // lower-case and turn embedded newlines into spaces 606 | link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); 607 | } 608 | url = "#"+link_id; 609 | 610 | if (g_urls[link_id] != undefined) { 611 | url = g_urls[link_id]; 612 | if (g_titles[link_id] != undefined) { 613 | title = g_titles[link_id]; 614 | } 615 | } 616 | else { 617 | return whole_match; 618 | } 619 | } 620 | 621 | alt_text = alt_text.replace(/"/g,"""); 622 | url = escapeCharacters(url,"*_"); 623 | var result = "\""" + _RunSpanGamut(m1) + "");}); 651 | 652 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 653 | function(matchFound,m1){return hashBlock("

    " + _RunSpanGamut(m1) + "

    ");}); 654 | 655 | // atx-style headers: 656 | // # Header 1 657 | // ## Header 2 658 | // ## Header 2 with closing hashes ## 659 | // ... 660 | // ###### Header 6 661 | // 662 | 663 | /* 664 | text = text.replace(/ 665 | ^(\#{1,6}) // $1 = string of #'s 666 | [ \t]* 667 | (.+?) // $2 = Header text 668 | [ \t]* 669 | \#* // optional closing #'s (not counted) 670 | \n+ 671 | /gm, function() {...}); 672 | */ 673 | 674 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 675 | function(wholeMatch,m1,m2) { 676 | var h_level = m1.length; 677 | return hashBlock("" + _RunSpanGamut(m2) + ""); 678 | }); 679 | 680 | return text; 681 | } 682 | 683 | // This declaration keeps Dojo compressor from outputting garbage: 684 | var _ProcessListItems; 685 | 686 | var _DoLists = function(text) { 687 | // 688 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 689 | // 690 | 691 | // attacklab: add sentinel to hack around khtml/safari bug: 692 | // http://bugs.webkit.org/show_bug.cgi?id=11231 693 | text += "~0"; 694 | 695 | // Re-usable pattern to match any entirel ul or ol list: 696 | 697 | /* 698 | var whole_list = / 699 | ( // $1 = whole list 700 | ( // $2 701 | [ ]{0,3} // attacklab: g_tab_width - 1 702 | ([*+-]|\d+[.]) // $3 = first list item marker 703 | [ \t]+ 704 | ) 705 | [^\r]+? 706 | ( // $4 707 | ~0 // sentinel for workaround; should be $ 708 | | 709 | \n{2,} 710 | (?=\S) 711 | (?! // Negative lookahead for another list item marker 712 | [ \t]* 713 | (?:[*+-]|\d+[.])[ \t]+ 714 | ) 715 | ) 716 | )/g 717 | */ 718 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 719 | 720 | if (g_list_level) { 721 | text = text.replace(whole_list,function(wholeMatch,m1,m2) { 722 | var list = m1; 723 | var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol"; 724 | 725 | // Turn double returns into triple returns, so that we can make a 726 | // paragraph for the last item in a list, if necessary: 727 | list = list.replace(/\n{2,}/g,"\n\n\n");; 728 | var result = _ProcessListItems(list); 729 | 730 | // Trim any trailing whitespace, to put the closing `` 731 | // up on the preceding line, to get it past the current stupid 732 | // HTML block parser. This is a hack to work around the terrible 733 | // hack that is the HTML block parser. 734 | result = result.replace(/\s+$/,""); 735 | result = "<"+list_type+">" + result + "\n"; 736 | return result; 737 | }); 738 | } else { 739 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 740 | text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) { 741 | var runup = m1; 742 | var list = m2; 743 | 744 | var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol"; 745 | // Turn double returns into triple returns, so that we can make a 746 | // paragraph for the last item in a list, if necessary: 747 | var list = list.replace(/\n{2,}/g,"\n\n\n");; 748 | var result = _ProcessListItems(list); 749 | result = runup + "<"+list_type+">\n" + result + "\n"; 750 | return result; 751 | }); 752 | } 753 | 754 | // attacklab: strip sentinel 755 | text = text.replace(/~0/,""); 756 | 757 | return text; 758 | } 759 | 760 | _ProcessListItems = function(list_str) { 761 | // 762 | // Process the contents of a single ordered or unordered list, splitting it 763 | // into individual list items. 764 | // 765 | // The $g_list_level global keeps track of when we're inside a list. 766 | // Each time we enter a list, we increment it; when we leave a list, 767 | // we decrement. If it's zero, we're not in a list anymore. 768 | // 769 | // We do this because when we're not inside a list, we want to treat 770 | // something like this: 771 | // 772 | // I recommend upgrading to version 773 | // 8. Oops, now this line is treated 774 | // as a sub-list. 775 | // 776 | // As a single paragraph, despite the fact that the second line starts 777 | // with a digit-period-space sequence. 778 | // 779 | // Whereas when we're inside a list (or sub-list), that line will be 780 | // treated as the start of a sub-list. What a kludge, huh? This is 781 | // an aspect of Markdown's syntax that's hard to parse perfectly 782 | // without resorting to mind-reading. Perhaps the solution is to 783 | // change the syntax rules such that sub-lists must start with a 784 | // starting cardinal number; e.g. "1." or "a.". 785 | 786 | g_list_level++; 787 | 788 | // trim trailing blank lines: 789 | list_str = list_str.replace(/\n{2,}$/,"\n"); 790 | 791 | // attacklab: add sentinel to emulate \z 792 | list_str += "~0"; 793 | 794 | /* 795 | list_str = list_str.replace(/ 796 | (\n)? // leading line = $1 797 | (^[ \t]*) // leading whitespace = $2 798 | ([*+-]|\d+[.]) [ \t]+ // list marker = $3 799 | ([^\r]+? // list item text = $4 800 | (\n{1,2})) 801 | (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) 802 | /gm, function(){...}); 803 | */ 804 | list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, 805 | function(wholeMatch,m1,m2,m3,m4){ 806 | var item = m4; 807 | var leading_line = m1; 808 | var leading_space = m2; 809 | 810 | if (leading_line || (item.search(/\n{2,}/)>-1)) { 811 | item = _RunBlockGamut(_Outdent(item)); 812 | } 813 | else { 814 | // Recursion for sub-lists: 815 | item = _DoLists(_Outdent(item)); 816 | item = item.replace(/\n$/,""); // chomp(item) 817 | item = _RunSpanGamut(item); 818 | } 819 | 820 | return "
  • " + item + "
  • \n"; 821 | } 822 | ); 823 | 824 | // attacklab: strip sentinel 825 | list_str = list_str.replace(/~0/g,""); 826 | 827 | g_list_level--; 828 | return list_str; 829 | } 830 | 831 | 832 | var _DoCodeBlocks = function(text) { 833 | // 834 | // Process Markdown `
    ` blocks.
     835 | //  
     836 | 
     837 | 	/*
     838 | 		text = text.replace(text,
     839 | 			/(?:\n\n|^)
     840 | 			(								// $1 = the code block -- one or more lines, starting with a space/tab
     841 | 				(?:
     842 | 					(?:[ ]{4}|\t)			// Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
     843 | 					.*\n+
     844 | 				)+
     845 | 			)
     846 | 			(\n*[ ]{0,3}[^ \t\n]|(?=~0))	// attacklab: g_tab_width
     847 | 		/g,function(){...});
     848 | 	*/
     849 | 
     850 | 	// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
     851 | 	text += "~0";
     852 | 	
     853 | 	text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
     854 | 		function(wholeMatch,m1,m2) {
     855 | 			var codeblock = m1;
     856 | 			var nextChar = m2;
     857 | 		
     858 | 			codeblock = _EncodeCode( _Outdent(codeblock));
     859 | 			codeblock = _Detab(codeblock);
     860 | 			codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
     861 | 			codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
     862 | 
     863 | 			codeblock = "
    " + codeblock + "\n
    "; 864 | 865 | return hashBlock(codeblock) + nextChar; 866 | } 867 | ); 868 | 869 | // attacklab: strip sentinel 870 | text = text.replace(/~0/,""); 871 | 872 | return text; 873 | } 874 | 875 | var hashBlock = function(text) { 876 | text = text.replace(/(^\n+|\n+$)/g,""); 877 | return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n"; 878 | } 879 | 880 | 881 | var _DoCodeSpans = function(text) { 882 | // 883 | // * Backtick quotes are used for spans. 884 | // 885 | // * You can use multiple backticks as the delimiters if you want to 886 | // include literal backticks in the code span. So, this input: 887 | // 888 | // Just type ``foo `bar` baz`` at the prompt. 889 | // 890 | // Will translate to: 891 | // 892 | //

    Just type foo `bar` baz at the prompt.

    893 | // 894 | // There's no arbitrary limit to the number of backticks you 895 | // can use as delimters. If you need three consecutive backticks 896 | // in your code, use four for delimiters, etc. 897 | // 898 | // * You can use spaces to get literal backticks at the edges: 899 | // 900 | // ... type `` `bar` `` ... 901 | // 902 | // Turns to: 903 | // 904 | // ... type `bar` ... 905 | // 906 | 907 | /* 908 | text = text.replace(/ 909 | (^|[^\\]) // Character before opening ` can't be a backslash 910 | (`+) // $2 = Opening run of ` 911 | ( // $3 = The code block 912 | [^\r]*? 913 | [^`] // attacklab: work around lack of lookbehind 914 | ) 915 | \2 // Matching closer 916 | (?!`) 917 | /gm, function(){...}); 918 | */ 919 | 920 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 921 | function(wholeMatch,m1,m2,m3,m4) { 922 | var c = m3; 923 | c = c.replace(/^([ \t]*)/g,""); // leading whitespace 924 | c = c.replace(/[ \t]*$/g,""); // trailing whitespace 925 | c = _EncodeCode(c); 926 | return m1+""+c+""; 927 | }); 928 | 929 | return text; 930 | } 931 | 932 | 933 | var _EncodeCode = function(text) { 934 | // 935 | // Encode/escape certain characters inside Markdown code runs. 936 | // The point is that in code, these characters are literals, 937 | // and lose their special Markdown meanings. 938 | // 939 | // Encode all ampersands; HTML entities are not 940 | // entities within a Markdown code span. 941 | text = text.replace(/&/g,"&"); 942 | 943 | // Do the angle bracket song and dance: 944 | text = text.replace(//g,">"); 946 | 947 | // Now, escape characters that are magic in Markdown: 948 | text = escapeCharacters(text,"\*_{}[]\\",false); 949 | 950 | // jj the line above breaks this: 951 | //--- 952 | 953 | //* Item 954 | 955 | // 1. Subitem 956 | 957 | // special char: * 958 | //--- 959 | 960 | return text; 961 | } 962 | 963 | 964 | var _DoItalicsAndBold = function(text) { 965 | 966 | // must go first: 967 | text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, 968 | "$2"); 969 | 970 | text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, 971 | "$2"); 972 | 973 | return text; 974 | } 975 | 976 | 977 | var _DoBlockQuotes = function(text) { 978 | 979 | /* 980 | text = text.replace(/ 981 | ( // Wrap whole match in $1 982 | ( 983 | ^[ \t]*>[ \t]? // '>' at the start of a line 984 | .+\n // rest of the first line 985 | (.+\n)* // subsequent consecutive lines 986 | \n* // blanks 987 | )+ 988 | ) 989 | /gm, function(){...}); 990 | */ 991 | 992 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 993 | function(wholeMatch,m1) { 994 | var bq = m1; 995 | 996 | // attacklab: hack around Konqueror 3.5.4 bug: 997 | // "----------bug".replace(/^-/g,"") == "bug" 998 | 999 | bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting 1000 | 1001 | // attacklab: clean up hack 1002 | bq = bq.replace(/~0/g,""); 1003 | 1004 | bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines 1005 | bq = _RunBlockGamut(bq); // recurse 1006 | 1007 | bq = bq.replace(/(^|\n)/g,"$1 "); 1008 | // These leading spaces screw with
     content, so we need to fix that:
    1009 | 			bq = bq.replace(
    1010 | 					/(\s*
    [^\r]+?<\/pre>)/gm,
    1011 | 				function(wholeMatch,m1) {
    1012 | 					var pre = m1;
    1013 | 					// attacklab: hack around Konqueror 3.5.4 bug:
    1014 | 					pre = pre.replace(/^  /mg,"~0");
    1015 | 					pre = pre.replace(/~0/g,"");
    1016 | 					return pre;
    1017 | 				});
    1018 | 			
    1019 | 			return hashBlock("
    \n" + bq + "\n
    "); 1020 | }); 1021 | return text; 1022 | } 1023 | 1024 | 1025 | var _FormParagraphs = function(text) { 1026 | // 1027 | // Params: 1028 | // $text - string to process with html

    tags 1029 | // 1030 | 1031 | // Strip leading and trailing lines: 1032 | text = text.replace(/^\n+/g,""); 1033 | text = text.replace(/\n+$/g,""); 1034 | 1035 | var grafs = text.split(/\n{2,}/g); 1036 | var grafsOut = new Array(); 1037 | 1038 | // 1039 | // Wrap

    tags. 1040 | // 1041 | var end = grafs.length; 1042 | for (var i=0; i= 0) { 1047 | grafsOut.push(str); 1048 | } 1049 | else if (str.search(/\S/) >= 0) { 1050 | str = _RunSpanGamut(str); 1051 | str = str.replace(/^([ \t]*)/g,"

    "); 1052 | str += "

    " 1053 | grafsOut.push(str); 1054 | } 1055 | 1056 | } 1057 | 1058 | // 1059 | // Unhashify HTML blocks 1060 | // 1061 | end = grafsOut.length; 1062 | for (var i=0; i= 0) { 1065 | var blockText = g_html_blocks[RegExp.$1]; 1066 | blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs 1067 | grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); 1068 | } 1069 | } 1070 | 1071 | return grafsOut.join("\n\n"); 1072 | } 1073 | 1074 | 1075 | var _EncodeAmpsAndAngles = function(text) { 1076 | // Smart processing for ampersands and angle brackets that need to be encoded. 1077 | 1078 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1079 | // http://bumppo.net/projects/amputator/ 1080 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); 1081 | 1082 | // Encode naked <'s 1083 | text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); 1084 | 1085 | return text; 1086 | } 1087 | 1088 | 1089 | var _EncodeBackslashEscapes = function(text) { 1090 | // 1091 | // Parameter: String. 1092 | // Returns: The string, with after processing the following backslash 1093 | // escape sequences. 1094 | // 1095 | 1096 | // attacklab: The polite way to do this is with the new 1097 | // escapeCharacters() function: 1098 | // 1099 | // text = escapeCharacters(text,"\\",true); 1100 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1101 | // 1102 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1103 | // as an optimization for Firefox. This function gets called a LOT. 1104 | 1105 | text = text.replace(/\\(\\)/g,escapeCharacters_callback); 1106 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); 1107 | return text; 1108 | } 1109 | 1110 | 1111 | var _DoAutoLinks = function(text) { 1112 | 1113 | text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); 1114 | 1115 | // Email addresses: 1116 | 1117 | /* 1118 | text = text.replace(/ 1119 | < 1120 | (?:mailto:)? 1121 | ( 1122 | [-.\w]+ 1123 | \@ 1124 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1125 | ) 1126 | > 1127 | /gi, _DoAutoLinks_callback()); 1128 | */ 1129 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1130 | function(wholeMatch,m1) { 1131 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1132 | } 1133 | ); 1134 | 1135 | return text; 1136 | } 1137 | 1138 | 1139 | var _EncodeEmailAddress = function(addr) { 1140 | // 1141 | // Input: an email address, e.g. "foo@example.com" 1142 | // 1143 | // Output: the email address as a mailto link, with each character 1144 | // of the address encoded as either a decimal or hex entity, in 1145 | // the hopes of foiling most address harvesting spam bots. E.g.: 1146 | // 1147 | // foo 1149 | // @example.com 1150 | // 1151 | // Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1152 | // mailing list: 1153 | // 1154 | 1155 | // attacklab: why can't javascript speak hex? 1156 | function char2hex(ch) { 1157 | var hexDigits = '0123456789ABCDEF'; 1158 | var dec = ch.charCodeAt(0); 1159 | return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15)); 1160 | } 1161 | 1162 | var encode = [ 1163 | function(ch){return "&#"+ch.charCodeAt(0)+";";}, 1164 | function(ch){return "&#x"+char2hex(ch)+";";}, 1165 | function(ch){return ch;} 1166 | ]; 1167 | 1168 | addr = "mailto:" + addr; 1169 | 1170 | addr = addr.replace(/./g, function(ch) { 1171 | if (ch == "@") { 1172 | // this *must* be encoded. I insist. 1173 | ch = encode[Math.floor(Math.random()*2)](ch); 1174 | } else if (ch !=":") { 1175 | // leave ':' alone (to spot mailto: later) 1176 | var r = Math.random(); 1177 | // roughly 10% raw, 45% hex, 45% dec 1178 | ch = ( 1179 | r > .9 ? encode[2](ch) : 1180 | r > .45 ? encode[1](ch) : 1181 | encode[0](ch) 1182 | ); 1183 | } 1184 | return ch; 1185 | }); 1186 | 1187 | addr = "" + addr + ""; 1188 | addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part 1189 | 1190 | return addr; 1191 | } 1192 | 1193 | 1194 | var _UnescapeSpecialChars = function(text) { 1195 | // 1196 | // Swap back in all the special characters we've hidden. 1197 | // 1198 | text = text.replace(/~E(\d+)E/g, 1199 | function(wholeMatch,m1) { 1200 | var charCodeToReplace = parseInt(m1); 1201 | return String.fromCharCode(charCodeToReplace); 1202 | } 1203 | ); 1204 | return text; 1205 | } 1206 | 1207 | 1208 | var _Outdent = function(text) { 1209 | // 1210 | // Remove one level of line-leading tabs or spaces 1211 | // 1212 | 1213 | // attacklab: hack around Konqueror 3.5.4 bug: 1214 | // "----------bug".replace(/^-/g,"") == "bug" 1215 | 1216 | text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width 1217 | 1218 | // attacklab: clean up hack 1219 | text = text.replace(/~0/g,"") 1220 | 1221 | return text; 1222 | } 1223 | 1224 | var _Detab = function(text) { 1225 | // attacklab: Detab's completely rewritten for speed. 1226 | // In perl we could fix it by anchoring the regexp with \G. 1227 | // In javascript we're less fortunate. 1228 | 1229 | // expand first n-1 tabs 1230 | text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width 1231 | 1232 | // replace the nth with two sentinels 1233 | text = text.replace(/\t/g,"~A~B"); 1234 | 1235 | // use the sentinel to anchor our regex so it doesn't explode 1236 | text = text.replace(/~B(.+?)~A/g, 1237 | function(wholeMatch,m1,m2) { 1238 | var leadingText = m1; 1239 | var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width 1240 | 1241 | // there *must* be a better way to do this: 1242 | for (var i=0; i