diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6ab99d --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +## Generated CouchApp + +This repo is a snapshot of the development going on in [The CouchApp generate command.](http://github.com/couchapp/couchapp/tree/master/templates/app/) + +It's easier for me to receive patches against couchapp/couchapp. This repo is intended primarily so you can deploy new versions of the example app without needing to upgrade CouchApp. + +Install with + + couchapp push . http://localhost:5984/proto + +or (if you have security turned on) + + couchapp push . http://myname:mypass@localhost:5984/proto + \ No newline at end of file diff --git a/_attachments/couchcampfire.jpg b/_attachments/couchcampfire.jpg new file mode 100644 index 0000000..10c0692 Binary files /dev/null and b/_attachments/couchcampfire.jpg differ diff --git a/_attachments/couchcoon.jpg b/_attachments/couchcoon.jpg new file mode 100644 index 0000000..64a80e5 Binary files /dev/null and b/_attachments/couchcoon.jpg differ diff --git a/_attachments/index.html b/_attachments/index.html index 7b20699..4569861 100644 --- a/_attachments/index.html +++ b/_attachments/index.html @@ -5,17 +5,66 @@ +
+ + diff --git a/_attachments/style/main.css b/_attachments/style/main.css index 2e1421c..bd171c0 100644 --- a/_attachments/style/main.css +++ b/_attachments/style/main.css @@ -1,12 +1,22 @@ /* add styles here */ body { + background: black; + color: #DA0; font:1em Helvetica, sans-serif; padding:4px; } +#header { + background: url('../couchcampfire.jpg') no-repeat;; +} +a, a:hover, a:visited { + color: #A70; +} + h1 { margin-top:0; + color: orange; } #account { @@ -14,38 +24,51 @@ h1 { } #profile { - border:4px solid #edd; - background:#fee; - padding:8px; - margin-bottom:8px; + font-size: .8em; +} + +#profile form { + float:left; } #items { - border:4px solid #dde; - background:#eef; + border: 4px solid #600; + background: #300; padding:8px; width:60%; float:left; } +#items img.attachment { + width:500px; +} + #sidebar { - border:4px solid #dfd; + border:4px solid #600; + background: #300; padding:8px; float:right; - width:30%; + width:35%; } #items li { - border:4px solid #f5f5ff; - background:#fff; + border:4px solid orange; + background: black; padding:8px; margin:4px 0; } +#items li.url { + border: 0px; + padding: 0px; + margin: 0px; +} + form { padding:4px; margin:6px; - background-color:#ddd; + background-color:#300; + border: 4px solid #600; } div.avatar { @@ -73,3 +96,7 @@ div.avatar img { #items ul { list-style: none; } + +textarea { + width: 95%; +} \ No newline at end of file diff --git a/evently/items/_changes/.data.js.swp b/evently/items/_changes/.data.js.swp deleted file mode 100644 index 4ad1676..0000000 Binary files a/evently/items/_changes/.data.js.swp and /dev/null differ diff --git a/evently/items/_changes/.mustache.html.swp b/evently/items/_changes/.mustache.html.swp deleted file mode 100644 index 91394ef..0000000 Binary files a/evently/items/_changes/.mustache.html.swp and /dev/null differ diff --git a/evently/items/_changes/data.js b/evently/items/_changes/data.js index 06214ff..f4bd1b7 100644 --- a/evently/items/_changes/data.js +++ b/evently/items/_changes/data.js @@ -1,13 +1,16 @@ function(data) { - + var dbname = $$(this).app.db.name; // $.log(data); var items = []; for (var i=0; i < data.rows.length; i++) { - var r = data.rows[i].value, item = {}; + var r = data.rows[i].value, item = {}, v=r; item.bio = r.bio || ""; + item.name = v.name; + item.company = v.company; + item.email = v.email; + item.id = data.rows[i].id; + item.dbname = dbname; item.urls = []; - item.name = data.rows[i].id; - if (r.url) { item.urls.push({url:r.url, label:"www"}); } diff --git a/evently/items/_changes/mustache.html b/evently/items/_changes/mustache.html index df09d70..80eb7b8 100644 --- a/evently/items/_changes/mustache.html +++ b/evently/items/_changes/mustache.html @@ -5,15 +5,20 @@

CouchCampers

{{#attachments}} {{/attachments}} - {{name}}
- {{bio}}
+ {{name}} + {{company}}
+ {{#email}}{{email}}{{/email}} +

{{bio}}


+ edit in Futon
{{/items}} diff --git a/evently/profile/loggedOut/mustache.html b/evently/profile/loggedOut/mustache.html new file mode 100644 index 0000000..1618ecf --- /dev/null +++ b/evently/profile/loggedOut/mustache.html @@ -0,0 +1 @@ +

Please log in to create your profile. Signup is super easy, all you need is a name and password, none of the email verification or whatever. Cheers!

\ No newline at end of file diff --git a/evently/profile/profileReady/mustache.html b/evently/profile/profileReady/mustache.html index 1ad2bc0..ab0515b 100644 --- a/evently/profile/profileReady/mustache.html +++ b/evently/profile/profileReady/mustache.html @@ -1,5 +1,3 @@ -

Most applications will customize this template (ddoc.evently.profile.profileReady.mustache) for user input.

-
{{#gravatar_url}}{{/gravatar_url}}
@@ -8,7 +6,25 @@
- +

New profile saved by {{nickname}}.

+

+

+

+

+

+
+ URLs +

+

+

+

+
+

+

+

If you want to edit this information later, there is an "Edit in Futon" link on each listed profile.

\ No newline at end of file diff --git a/evently/profile/profileReady/selectors/form/submit.js b/evently/profile/profileReady/selectors/form/submit.js index bf18dd8..a97860c 100644 --- a/evently/profile/profileReady/selectors/form/submit.js +++ b/evently/profile/profileReady/selectors/form/submit.js @@ -1,13 +1,39 @@ -function() { - var form = this; +function(e) { + e.preventDefault(); + var form = $(this); + var f = form.serializeObject(); var doc = { created_at : new Date(), profile : $$("#profile").profile, - message : $("[name=message]", form).val() + bio : f.bio, + name : f.name, + company : f.company, + urls : { + www : f["url-www"], + sourcecode : f["url-sourcecode"], + twitter : f["url-twitter"], + couchdb : f["url-couchdb"] + }, + email : f.email }; - $$(this).app.db.saveDoc(doc, { + var db = $$(this).app.db; + + db.saveDoc(doc, { success : function() { - $("[name=message]", form).val(""); + $("input[name='_rev']", form).val(doc._rev); + var as = $("input[name='_attachments']", form).val(); + if (as) { + $("[name=name]", form).val("Uploading file..."); + // thank you cmlenz for Futon's original upload code + form.ajaxSubmit({ + url: db.uri + $.couch.encodeDocId(doc._id), + success: function(resp) { + form[0].reset(); + } + }); + } else { + form[0].reset(); + } } }); return false; diff --git a/migrate.js b/migrate.js new file mode 100644 index 0000000..98b4068 --- /dev/null +++ b/migrate.js @@ -0,0 +1,40 @@ +var couchdb = require("couchdb") + , client = couchdb.createClient(5984, "localhost", "xxx", "xxx") + , db = client.db("profiles") + ; + +db.allDocs(function(er, view) { + view.rows.forEach(function(r) { + console.log(r.id) + db.getDoc(r.id, {attachments:true}, function(er, doc) { + if (er) { + console.log("getDoc", er) + } else { + console.log(doc._id) + var rev = doc._rev; + var name = doc._id; + if (name.length < 30) { + delete doc._rev; + delete doc._id; + doc.name = name; + db.removeDoc(name, rev, function(er) { + if (er) { + console.log("error deleting", name, er) + } else { + console.log("deleted", name) + } + }) + // console.log("update", doc) + // db.saveDoc(doc, function(er, resp) { + // if (er) { + // console.log(er) + // } else { + // + // } + // }) + } + } + }); + }); + +}); \ No newline at end of file diff --git a/vendor/couchapp/README.md b/vendor/couchapp/README.md deleted file mode 100644 index 2606c1d..0000000 --- a/vendor/couchapp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## CouchApp - more than just a filesystem mapper - -This is where documentation will go for the client and server JavaScript parts of CouchApp. \ No newline at end of file diff --git a/vendor/couchapp/_attachments/docs.css b/vendor/couchapp/_attachments/docs.css deleted file mode 100644 index 94b19d6..0000000 --- a/vendor/couchapp/_attachments/docs.css +++ /dev/null @@ -1,53 +0,0 @@ -body { - font:1em Helvetica, sans-serif; - margin:0; - padding:4px; -} - -h1 { - margin:0.5em; -} - -h2 { - color:#222; -} - -pre { - padding:4px; - margin:4px; - background:#bbb; -} - -#content { - padding:4px; - margin:2px; -} - -#sidebar { - float:right; - width:34%; -} - -#docs { - -moz-box-shadow:0 0 2em #000; - -webkit-box-shadow:0 0 2em #000; - width:58%; - padding:8px; - margin:4px; -} - -.example { - background:#ffd; - padding:4px; - margin:4px; - position:absolute; -} - -textarea.code { - width:100%; -} - -.edit { - float:right; - font-size:0.8em; -} diff --git a/vendor/couchapp/_attachments/docs.html b/vendor/couchapp/_attachments/docs.html deleted file mode 100644 index 77220b7..0000000 --- a/vendor/couchapp/_attachments/docs.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - Evently and CouchApp Docs - - - - -
- -
-
-
-
- - - - - - - - - - - - - diff --git a/vendor/couchapp/_attachments/docs.js b/vendor/couchapp/_attachments/docs.js deleted file mode 100644 index 7b0e4d0..0000000 --- a/vendor/couchapp/_attachments/docs.js +++ /dev/null @@ -1,8 +0,0 @@ -$.log = function() { - // console.log(arguments) -}; - -$.couch.app(function(app) { - $("#docs").evently(app.ddoc.vendor.couchapp.evently.docs, app); - $.pathbinder.begin("/"); -}); \ No newline at end of file diff --git a/vendor/couchapp/_attachments/jquery.couch.app.js b/vendor/couchapp/_attachments/jquery.couch.app.js index da5a7fc..fc500a2 100644 --- a/vendor/couchapp/_attachments/jquery.couch.app.js +++ b/vendor/couchapp/_attachments/jquery.couch.app.js @@ -19,9 +19,14 @@ // }); (function($) { - - function Design(db, name) { + + function Design(db, name, code) { this.doc_id = "_design/"+name; + if (code) { + this.code_path = this.doc_id + "/" + code; + } else { + this.code_path = this.doc_id; + } this.view = function(view, opts) { db.view(name+'/'+view, opts); }; @@ -30,193 +35,131 @@ }; } - $.couch.app = $.couch.app || function(appFun, opts) { - opts = opts || {}; - $(function() { - var dbname = opts.db || document.location.href.split('/')[3]; - var dname = opts.design || unescape(document.location.href).split('/')[5]; - var db = $.couch.db(dbname); - var design = new Design(db, dname); - - // docForm applies CouchDB behavior to HTML forms. - // todo make this a couch.app plugin - function docForm(formSelector, opts) { - var localFormDoc = {}; - opts = opts || {}; - opts.fields = opts.fields || []; + function docForm() { alert("docForm has been moved to vendor/couchapp/lib/docForm.js, use app.require to load") }; - // turn the form into deep json - // field names like 'author-email' get turned into json like - // {"author":{"email":"quentin@example.com"}} - function formToDeepJSON(form, fields, doc) { - form = $(form); - opts.fields.forEach(function(field) { - var element = form.find("[name="+field+"]"); - if (element.attr('type') === 'checkbox') { - var val = element.attr('checked'); - } else { - var val = element.val(); - if (!val) return; - } - var parts = field.split('-'); - var frontObj = doc, frontName = parts.shift(); - while (parts.length > 0) { - frontObj[frontName] = frontObj[frontName] || {}; - frontObj = frontObj[frontName]; - frontName = parts.shift(); - } - frontObj[frontName] = val; - }); - } - - // Apply the behavior - $(formSelector).submit(function(e) { - e.preventDefault(); - // formToDeepJSON acts on localFormDoc by reference - formToDeepJSON(this, opts.fields, localFormDoc); - if (opts.beforeSave) {opts.beforeSave(localFormDoc);} - db.saveDoc(localFormDoc, { - success : function(resp) { - if (opts.success) {opts.success(resp, localFormDoc);} - } - }); - - return false; - }); + function resolveModule(path, names, parents, current) { + parents = parents || []; + if (names.length === 0) { + if (typeof current != "string") { + throw ["error","invalid_require_path", + 'Must require a JavaScript string, not: '+(typeof current)]; + } + return [current, parents]; + } + var n = names.shift(); + if (n == '..') { + parents.pop(); + var pp = parents.pop(); + if (!pp) { + throw ["error", "invalid_require_path", path]; + } + return resolveModule(path, names, parents, pp); + } else if (n == '.') { + var p = parents.pop(); + if (!p) { + throw ["error", "invalid_require_path", path]; + } + return resolveModule(path, names, parents, p); + } else { + parents = []; + } + if (!current[n]) { + throw ["error", "invalid_require_path", path]; + } + parents.push(current); + return resolveModule(path, names, parents, current[n]); + } - // populate form from an existing doc - function docToForm(doc) { - var form = $(formSelector); - // fills in forms - opts.fields.forEach(function(field) { - var parts = field.split('-'); - var run = true, frontObj = doc, frontName = parts.shift(); - while (frontObj && parts.length > 0) { - frontObj = frontObj[frontName]; - frontName = parts.shift(); - } - if (frontObj && frontObj[frontName]) { - var element = form.find("[name="+field+"]"); - if (element.attr('type') === 'checkbox') { - element.attr('checked', frontObj[frontName]); - } else { - element.val(frontObj[frontName]); - } - } - }); - } - - if (opts.id) { - db.openDoc(opts.id, { - attachPrevRev : opts.attachPrevRev, - success: function(doc) { - if (opts.onLoad) {opts.onLoad(doc);} - localFormDoc = doc; - docToForm(doc); - }}); - } else if (opts.template) { - if (opts.onLoad) {opts.onLoad(opts.template);} - localFormDoc = opts.template; - docToForm(localFormDoc); + function makeRequire(ddoc) { + var moduleCache = []; + function getCachedModule(name, parents) { + var key, i, len = moduleCache.length; + for (i=0;i'+a+''; }).replace(/\@([\w\-]+)/g,function(user,name) { - return ''+user+''; + return ''+user+''; }).replace(/\#([\w\-\.]+)/g,function(word,tag) { - return ''+word+''; + return ''+word+''; }); }; $.fn.prettyDate = function() { $(this).each(function() { - $(this).text($.prettyDate($(this).text())); + var string, title = $(this).attr("title"); + if (title) { + string = $.prettyDate(title); + } else { + string = $.prettyDate($(this).text()); + } + $(this).text(string); }); }; @@ -52,8 +75,9 @@ $.prettyDate = function(time){ day_diff == 1 && "yesterday" || day_diff < 21 && day_diff + " days ago" || day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" || - day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || - Math.ceil( day_diff / 365 ) + " years ago"; + time; + // day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || + // Math.ceil( day_diff / 365 ) + " years ago"; }; $.argsToArray = function(args) { diff --git a/vendor/couchapp/_attachments/jquery.evently.js b/vendor/couchapp/_attachments/jquery.evently.js index 322f311..5128a20 100644 --- a/vendor/couchapp/_attachments/jquery.evently.js +++ b/vendor/couchapp/_attachments/jquery.evently.js @@ -22,7 +22,7 @@ function $$(node) { } }; $.forIn = forIn; - function funViaString(fun) { + function funViaString(fun, hint) { if (fun && fun.match && fun.match(/^function/)) { eval("var f = "+fun); if (typeof f == "function") { @@ -31,7 +31,8 @@ function $$(node) { return f.apply(this, arguments); } catch(e) { // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION - // $.log({"message": "Error in evently function.", "error": e, "src" : fun}); + $.log({"message": "Error in evently function.", "error": e, + "src" : fun, "hint":hint}); throw(e); } }; @@ -42,7 +43,7 @@ function $$(node) { function runIfFun(me, fun, args) { // if the field is a function, call it, bound to the widget - var f = funViaString(fun); + var f = funViaString(fun, me); if (typeof f == "function") { return f.apply(me, args); } else { @@ -65,7 +66,8 @@ function $$(node) { }); }, paths : [], - changesDBs : {} + changesDBs : {}, + changesOpts : {} }; function extractFrom(name, evs) { @@ -74,43 +76,71 @@ function $$(node) { function extractEvents(name, ddoc) { // extract events from ddoc.evently and ddoc.vendor.*.evently - var events = [true, {}]; - $.forIn(ddoc.vendor, function(k, v) { + var events = [true, {}] + , vendor = ddoc.vendor || {} + , evently = ddoc.evently || {} + ; + $.forIn(vendor, function(k, v) { if (v.evently && v.evently[name]) { events.push(v.evently[name]); } }); - if (ddoc.evently[name]) {events.push(ddoc.evently[name]);} + if (evently[name]) {events.push(evently[name]);} return $.extend.apply(null, events); } + function extractPartials(ddoc) { + var partials = [true, {}] + , vendor = ddoc.vendor || {} + , evently = ddoc.evently || {} + ; + $.forIn(vendor, function(k, v) { + if (v.evently && v.evently._partials) { + partials.push(v.evently._partials); + } + }); + if (evently._partials) {partials.push(evently._partials);} + return $.extend.apply(null, partials); + }; + + function applyCommon(events) { + if (events._common) { + $.forIn(events, function(k, v) { + events[k] = $.extend(true, {}, events._common, v); + }); + delete events._common; + return events; + } else { + return events; + } + } + $.fn.evently = function(events, app, args) { var elem = $(this); // store the app on the element for later use if (app) { - $$(elem).app = app; + $$(elem).app = app; } if (typeof events == "string") { events = extractEvents(events, app.ddoc); } - + events = applyCommon(events); $$(elem).evently = events; + if (app && app.ddoc) { + $$(elem).partials = extractPartials(app.ddoc); + } // setup the handlers onto elem forIn(events, function(name, h) { eventlyHandler(elem, name, h, args); }); if (events._init) { - // $.log("ev _init", elem); elem.trigger("_init", args); } if (app && events._changes) { - $("body").bind("evently.changes."+app.db.name, function() { - // we want to unbind this function when the element is deleted. - // maybe jquery 1.4.2 has this covered? - // $.log('changes', elem); + $("body").bind("evently-changes-"+app.db.name, function() { elem.trigger("_changes"); }); followChanges(app); @@ -121,10 +151,15 @@ function $$(node) { // eventlyHandler applies the user's handler (h) to the // elem, bound to trigger based on name. function eventlyHandler(elem, name, h, args) { + if ($.evently.log) { + elem.bind(name, function() { + $.log(elem, name); + }); + } if (h.path) { elem.pathbinder(name, h.path); } - var f = funViaString(h); + var f = funViaString(h, name); if (typeof f == "function") { elem.bind(name, {args:args}, f); } else if (typeof f == "string") { @@ -140,7 +175,7 @@ function $$(node) { } else { // an object is using the evently / mustache template system if (h.fun) { - elem.bind(name, {args:args}, funViaString(h.fun)); + throw("e.fun has been removed, please rename to e.before") } // templates, selectors, etc are intepreted // when our named event is triggered. @@ -165,7 +200,7 @@ function $$(node) { // if there's a query object we run the query, // and then call the data function with the response. if (h.before && (!qrun || !arun)) { - funViaString(h.before).apply(me, args); + funViaString(h.before, me).apply(me, args); } if (h.async && !arun) { runAsync(me, h, args) @@ -199,22 +234,22 @@ function $$(node) { } if (h.after) { runIfFun(me, h.after, args); - // funViaString(h.after).apply(me, args); } } }; // todo this should return the new element function mustachioed(me, h, args) { + var partials = $$(me).partials; return $($.mustache( runIfFun(me, h.mustache, args), runIfFun(me, h.data, args), - runIfFun(me, h.partials, args))); + runIfFun(me, $.extend(true, partials, h.partials), args))); }; function runAsync(me, h, args) { // the callback is the first argument - funViaString(h.async).apply(me, [function() { + funViaString(h.async, me).apply(me, [function() { renderElement(me, h, $.argsToArray(arguments).concat($.argsToArray(args)), false, true); }].concat($.argsToArray(args))); @@ -232,7 +267,9 @@ function $$(node) { var q = {}; forIn(qu, function(k, v) { - q[k] = v; + if (["type", "view"].indexOf(k) == -1) { + q[k] = v; + } }); if (qType == "newRows") { @@ -316,34 +353,47 @@ function $$(node) { // only start one changes listener per db function followChanges(app) { - var dbName = app.db.name; + var dbName = app.db.name, changeEvent = function(resp) { + $("body").trigger("evently-changes-"+dbName, [resp]); + }; if (!$.evently.changesDBs[dbName]) { - connectToChanges(app, function() { - $("body").trigger("evently.changes."+dbName); - }); + if (app.db.changes) { + // new api in jquery.couch.js 1.0 + app.db.changes(null, $.evently.changesOpts).onChange(changeEvent); + } else { + // in case you are still on CouchDB 0.11 ;) deprecated. + connectToChanges(app, changeEvent); + } $.evently.changesDBs[dbName] = true; } } - - function connectToChanges(app, fun) { - function resetHXR(x) { - x.abort(); - connectToChanges(app, fun); + $.evently.followChanges = followChanges; + // deprecated. use db.changes() from jquery.couch.js + // this does not have an api for closing changes request. + function connectToChanges(app, fun, update_seq) { + function changesReq(seq) { + var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq; + if ($.evently.changesOpts.include_docs) { + url = url + "&include_docs=true"; + } + $.ajax({ + url: url, + contentType: "application/json", + dataType: "json", + complete: function(req) { + var resp = $.httpData(req, "json"); + fun(resp); + connectToChanges(app, fun, resp.last_seq); + } + }); }; - app.db.info({success: function(db_info) { - var c_xhr = jQuery.ajaxSettings.xhr(); - c_xhr.open("GET", app.db.uri+"_changes?feed=continuous&since="+db_info.update_seq, true); - c_xhr.send(""); - // todo use a timeout to prevent rapid triggers - var t; - c_xhr.onreadystatechange = function() { - clearTimeout(t); - t = setTimeout(fun, 100); - }; - setTimeout(function() { - resetHXR(c_xhr); - }, 1000 * 60); - }}); + if (update_seq) { + changesReq(update_seq); + } else { + app.db.info({success: function(db_info) { + changesReq(db_info.update_seq); + }}); + } }; })(jQuery); diff --git a/vendor/couchapp/_attachments/jquery.mustache.js b/vendor/couchapp/_attachments/jquery.mustache.js index 701ed0d..5aa67de 100644 --- a/vendor/couchapp/_attachments/jquery.mustache.js +++ b/vendor/couchapp/_attachments/jquery.mustache.js @@ -8,13 +8,9 @@ See http://github.com/defunkt/mustache for more info. ;(function($) { /* - Shamless port of http://github.com/defunkt/mustache - by Jan Lehnardt , Alexander Lang , - Sebastian Cohnen + mustache.js — Logic-less templates in JavaScript - Thanks @defunkt for the awesome code. - - See http://github.com/defunkt/mustache for more info. + See http://mustache.github.com/ for more info. */ var Mustache = function() { @@ -24,16 +20,45 @@ var Mustache = function() { otag: "{{", ctag: "}}", pragmas: {}, + buffer: [], + pragmas_implemented: { + "IMPLICIT-ITERATOR": true + }, + context: {}, + + render: function(template, context, partials, in_recursion) { + // reset buffer & set context + if(!in_recursion) { + this.context = context; + this.buffer = []; // TODO: make this non-lazy + } - render: function(template, context, partials) { // fail fast - if(template.indexOf(this.otag) == -1) { - return template; + if(!this.includes("", template)) { + if(in_recursion) { + return template; + } else { + this.send(template); + return; + } } template = this.render_pragmas(template); var html = this.render_section(template, context, partials); - return this.render_tags(html, context, partials); + if(in_recursion) { + return this.render_tags(html, context, partials, in_recursion); + } + + this.render_tags(html, context, partials, in_recursion); + }, + + /* + Sends parsed lines + */ + send: function(line) { + if(line != "") { + this.buffer.push(line); + } }, /* @@ -41,56 +66,86 @@ var Mustache = function() { */ render_pragmas: function(template) { // no pragmas - if(template.indexOf(this.otag + "%") == -1) { + if(!this.includes("%", template)) { return template; } var that = this; - var regex = new RegExp(this.otag + "%(.+)" + this.ctag); - return template.replace(regex, function(match, pragma) { - that.pragmas[pragma] = true; + var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + + this.ctag); + return template.replace(regex, function(match, pragma, options) { + if(!that.pragmas_implemented[pragma]) { + throw({message: + "This implementation of mustache doesn't understand the '" + + pragma + "' pragma"}); + } + that.pragmas[pragma] = {}; + if(options) { + var opts = options.split("="); + that.pragmas[pragma][opts[0]] = opts[1]; + } return ""; // ignore unknown pragmas silently }); }, - /* - Tries to find a partial in the global scope and render it + /* + Tries to find a partial in the curent scope and render it */ render_partial: function(name, context, partials) { - if(typeof(context[name]) != "object") { - throw({message: "subcontext for '" + name + "' is not an object"}); + name = this.trim(name); + if(!partials || partials[name] === undefined) { + throw({message: "unknown_partial '" + name + "'"}); } - if(!partials || !partials[name]) { - throw({message: "unknown_partial"}); + if(typeof(context[name]) != "object") { + return this.render(partials[name], context, partials, true); } - return this.render(partials[name], context[name], partials); + return this.render(partials[name], context[name], partials, true); }, /* - Renders boolean and enumerable sections + Renders inverted (^) and normal (#) sections */ render_section: function(template, context, partials) { - if(template.indexOf(this.otag + "#") == -1) { + if(!this.includes("#", template) && !this.includes("^", template)) { return template; } + var that = this; // CSW - Added "+?" so it finds the tighest bound, not the widest - var regex = new RegExp(this.otag + "\\#(.+)" + this.ctag + - "\\s*([\\s\\S]+?)" + this.otag + "\\/\\1" + this.ctag + "\\s*", "mg"); + var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + + "\\s*", "mg"); // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function(match, name, content) { + return template.replace(regex, function(match, type, name, content) { var value = that.find(name, context); - if(that.is_array(value)) { // Enumerable, Let's loop! - return that.map(value, function(row) { - return that.render(content, that.merge(context, - that.create_context(row)), partials); - }).join(''); - } else if(value) { // boolean section - return that.render(content, context, partials); - } else { - return ""; + if(type == "^") { // inverted section + if(!value || that.is_array(value) && value.length === 0) { + // false or empty list, render it + return that.render(content, context, partials, true); + } else { + return ""; + } + } else if(type == "#") { // normal section + if(that.is_array(value)) { // Enumerable, Let's loop! + return that.map(value, function(row) { + return that.render(content, that.create_context(row), + partials, true); + }).join(""); + } else if(that.is_object(value)) { // Object, Use it as subcontext! + return that.render(content, that.create_context(value), + partials, true); + } else if(typeof value === "function") { + // higher order section + return value.call(context, content, function(text) { + return that.render(text, context, partials, true); + }); + } else if(value) { // boolean section + return that.render(content, context, partials, true); + } else { + return ""; + } } }); }, @@ -98,41 +153,43 @@ var Mustache = function() { /* Replace {{foo}} and friends with values from our view */ - render_tags: function(template, context, partials) { - var lines = template.split("\n"); + render_tags: function(template, context, partials, in_recursion) { + // tit for tat + var that = this; var new_regex = function() { - return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\/#]+?)\\1?" + + return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + that.ctag + "+", "g"); }; - // tit for tat - var that = this; - var regex = new_regex(); - for (var i=0; i < lines.length; i++) { - lines[i] = lines[i].replace(regex, function (match,operator,name) { - switch(operator) { - case "!": // ignore comments - return match; - case "=": // set new delimiters, rebuild the replace regexp - that.set_delimiters(name); - regex = new_regex(); - // redo the line in order to get tags with the new delimiters - // on the same line - i--; - return ""; - case ">": // render partial - return that.render_partial(name, context, partials); - case "{": // the triple mustache is unescaped - return that.find(name, context); - return ""; - default: // escape the value - return that.escape(that.find(name, context)); - } - },this); + var tag_replace_callback = function(match, operator, name) { + switch(operator) { + case "!": // ignore comments + return ""; + case "=": // set new delimiters, rebuild the replace regexp + that.set_delimiters(name); + regex = new_regex(); + return ""; + case ">": // render partial + return that.render_partial(name, context, partials); + case "{": // the triple mustache is unescaped + return that.find(name, context); + default: // escape the value + return that.escape(that.find(name, context)); + } }; - return lines.join("\n"); + var lines = template.split("\n"); + for(var i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(regex, tag_replace_callback, this); + if(!in_recursion) { + this.send(lines[i]); + } + } + + if(in_recursion) { + return lines.join("\n"); + } }, set_delimiters: function(delimiters) { @@ -152,20 +209,33 @@ var Mustache = function() { '(\\' + specials.join('|\\') + ')', 'g' ); } - return text.replace(arguments.callee.sRE, '\\$1'); + return text.replace(arguments.callee.sRE, '\\$1'); }, /* - find `name` in current `context`. That is find me a value + find `name` in current `context`. That is find me a value from the view object */ find: function(name, context) { name = this.trim(name); - if(typeof context[name] === "function") { - return context[name].apply(context); + + // Checks whether a value is thruthy or false or 0 + function is_kinda_truthy(bool) { + return bool === false || bool === 0 || bool; + } + + var value; + if(is_kinda_truthy(context[name])) { + value = context[name]; + } else if(is_kinda_truthy(this.context[name])) { + value = this.context[name]; + } + + if(typeof value === "function") { + return value.apply(context); } - if(context[name] !== undefined) { - return context[name]; + if(value !== undefined) { + return value; } // silently ignore unkown variables return ""; @@ -173,82 +243,68 @@ var Mustache = function() { // Utility methods + /* includes tag */ + includes: function(needle, haystack) { + return haystack.indexOf(this.otag + needle) != -1; + }, + /* Does away with nasty characters */ escape: function(s) { - return s.toString().replace(/[&"<>\\]/g, function(s) { + s = String(s === null ? "" : s); + return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { switch(s) { - case "&": return "&"; - case "\\": return "\\\\";; - case '"': return '\"';; - case "<": return "<"; - case ">": return ">"; - default: return s; + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return s; } }); }, - /* - Merges all properties of object `b` into object `a`. - `b.property` overwrites a.property` - */ - merge: function(a, b) { - var _new = {}; - for(var name in a) { - if(a.hasOwnProperty(name)) { - _new[name] = a[name]; - } - }; - for(var name in b) { - if(b.hasOwnProperty(name)) { - _new[name] = b[name]; - } - }; - return _new; - }, - // by @langalex, support for arrays of strings create_context: function(_context) { if(this.is_object(_context)) { return _context; - } else if(this.pragmas["JSTACHE-ENABLE-STRING-ARRAYS"]) { - return {'.': _context}; + } else { + var iterator = "."; + if(this.pragmas["IMPLICIT-ITERATOR"]) { + iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; } }, is_object: function(a) { - return a && typeof a == 'object' + return a && typeof a == "object"; }, - /* - Thanks Doug Crockford - JavaScript — The Good Parts lists an alternative that works better with - frames. Frames can suck it, we use the simple version. - */ is_array: function(a) { - return (a && - typeof a === 'object' && - a.constructor === Array); + return Object.prototype.toString.call(a) === '[object Array]'; }, /* Gets rid of leading and trailing whitespace */ trim: function(s) { - return s.replace(/^\s*|\s*$/g, ''); + return s.replace(/^\s*|\s*$/g, ""); }, /* Why, why, why? Because IE. Cry, cry cry. - */ + */ map: function(array, fn) { if (typeof array.map == "function") { - return array.map(fn) + return array.map(fn); } else { var r = []; var l = array.length; - for(i=0;i'; - }).join('')); + for (var i=0; i < scripts.length; i++) { + document.write('