Browse Source

Merge branch 'release/0.6.0' into develop

Markus Ochel 12 years ago
parent
commit
021bc23765

+ 4 - 0
HISTORY.md

@@ -1,6 +1,10 @@
 History of Changes
 ==================
 
+## v0.6.0 / 2012-06-22
+
+- Implemented CouchDB `_changes` feeds in admin app to sync all connected clients with latest changes live
+
 ## v0.5.3 / 2012-06-21
 
 - Added a new chocolate color to the list of theme colors

+ 0 - 48
admin/controllers/essays.coffee

@@ -3,10 +3,6 @@ $           = Spine.$
 templates   = require('duality/templates')
 utils       = require('lib/utils')
 
-# For importing HTML from old sites
-require('lib/reMarked')
-require('lib/jquery-xdomainajax')
-
 MultiSelectUI = require('controllers/ui/multi-select')
 FileUploadUI  = require('controllers/ui/file-upload')
 PreviewUI     = require('controllers/ui/preview')
@@ -48,7 +44,6 @@ class EssayForm extends Spine.Controller
     'change select[name=site]': 'siteChange'
     'blur input[name=slug]':    'updateSlug'
     'click .fullscreen-button': 'fullscreen'
-    'click .import-button':     'import'
 
   constructor: ->
     super
@@ -151,49 +146,6 @@ class EssayForm extends Spine.Controller
       @fullscreenButton.html "Exit #{@fullscreenButtonText}"
       @previewUI = new PreviewUI field: @formBody
 
-  import: (e) =>
-    # For importing old HTML to Markdown directly from old location
-    e?.preventDefault()
-    url = $.trim prompt("Paste a URL from #{@formSite.val()}", @item.old_url or '')
-    if url
-      $.ajax
-        type: 'GET'
-        url: url
-        success: (res) =>
-          $html = $(res.responseText)
-          $title = $html.find('.post > h2:first > a')
-          $author = $html.find('.post .entry-author > a:first')
-          $date = $html.find('.post .entry-date > .published')
-          $content = $html.find('.post .entry:first')
-          $image = $content.find('img:first')
-          if $content
-            $content.find('.addthis_toolbox, .author-bio').remove()
-            options =
-                link_list:  false    # render links as references, create link list as appendix
-                h1_setext:  true     # underline h1 headers
-                h2_setext:  true     # underline h2 headers
-                h_atx_suf:  true     # header suffixes (###)
-                gfm_code:   false    # render code blocks as via ``` delims
-                li_bullet:  "*"      # list item bullet style
-                hr_char:    "-"      # hr style
-                indnt_str:  "    "   # indentation string
-                bold_char:  "*"      # char used for strong
-                emph_char:  "_"      # char used for em
-                gfm_tbls:   false    # markdown-extra tables
-                tbl_edges:  false    # show side edges on tables
-                hash_lnks:  false    # anchors w/hash hrefs as links
-            reMarker = new reMarked(options)
-            markdown = reMarker.render($content.html())
-            @formBody.val(markdown)
-
-          if not @item.old_url
-            @formTitle.val($title.text()) if $title
-            $slug = @form.find('input[name=slug]')
-            unless slug.val()
-              $slug.val($title.attr('href').replace('www.', '').replace("http://#{@formSite.val().replace('www.', '')}", '')) if $title
-            @formAuthorId.val($author.text()) if $author
-            @form.find('input[name=published_at]').val($date.text()) if $date
-
   save: (e) ->
     e.preventDefault()
     if not navigator.onLine

+ 2 - 0
admin/controllers/index.coffee

@@ -1,6 +1,8 @@
 Spine       = require('spine/core')
 require('spine/route')
 require('spine/manager')
+require('lib/spine-couch-ajax')
+require('lib/spine-couch-changes')
 require('lib/fastclick')
 
 templates   = require('duality/templates')

+ 0 - 48
admin/controllers/scenes.coffee

@@ -3,10 +3,6 @@ $           = Spine.$
 templates   = require('duality/templates')
 utils       = require('lib/utils')
 
-# For importing HTML from old sites
-require('lib/reMarked')
-require('lib/jquery-xdomainajax')
-
 MultiSelectUI = require('controllers/ui/multi-select')
 FileUploadUI  = require('controllers/ui/file-upload')
 PreviewUI     = require('controllers/ui/preview')
@@ -47,7 +43,6 @@ class SceneForm extends Spine.Controller
     'change select[name=site]': 'siteChange'
     'blur input[name=slug]':    'updateSlug'
     'click .fullscreen-button': 'fullscreen'
-    'click .import-button':     'import'
 
   constructor: ->
     super
@@ -149,49 +144,6 @@ class SceneForm extends Spine.Controller
       @fullscreenButton.html "Exit #{@fullscreenButtonText}"
       @previewUI = new PreviewUI field: @formBody
 
-  import: (e) =>
-    # For importing old HTML to Markdown directly from old location
-    e?.preventDefault()
-    url = $.trim prompt("Paste a URL from #{@formSite.val()}", @item.old_url or '')
-    if url
-      $.ajax
-        type: 'GET'
-        url: url
-        success: (res) =>
-          $html = $(res.responseText)
-          $title = $html.find('.post > h2:first > a')
-          $author = $html.find('.post .entry-author > a:first')
-          $date = $html.find('.post .entry-date > .published')
-          $content = $html.find('.post .entry:first')
-          $image = $content.find('img:first')
-          if $content
-            $content.find('.addthis_toolbox, .author-bio').remove()
-            options =
-                link_list:  false    # render links as references, create link list as appendix
-                h1_setext:  true     # underline h1 headers
-                h2_setext:  true     # underline h2 headers
-                h_atx_suf:  true     # header suffixes (###)
-                gfm_code:   false    # render code blocks as via ``` delims
-                li_bullet:  "*"      # list item bullet style
-                hr_char:    "-"      # hr style
-                indnt_str:  "    "   # indentation string
-                bold_char:  "*"      # char used for strong
-                emph_char:  "_"      # char used for em
-                gfm_tbls:   false    # markdown-extra tables
-                tbl_edges:  false    # show side edges on tables
-                hash_lnks:  false    # anchors w/hash hrefs as links
-            reMarker = new reMarked(options)
-            markdown = reMarker.render($content.html())
-            @formBody.val(markdown)
-
-          if not @item.old_url
-            @formTitle.val($title.text()) if $title
-            $slug = @form.find('input[name=slug]')
-            unless slug.val()
-              $slug.val($title.attr('href').replace('www.', '').replace("http://#{@formSite.val().replace('www.', '')}", '')) if $title
-            @formAuthorId.val($author.text()) if $author
-            @form.find('input[name=published_at]').val($date.text()) if $date
-
   save: (e) ->
     e.preventDefault()
     if not navigator.onLine

+ 0 - 48
admin/controllers/videos.coffee

@@ -3,10 +3,6 @@ $           = Spine.$
 templates   = require('duality/templates')
 utils       = require('lib/utils')
 
-# For importing HTML from old sites
-require('lib/reMarked')
-require('lib/jquery-xdomainajax')
-
 MultiSelectUI = require('controllers/ui/multi-select')
 FileUploadUI  = require('controllers/ui/file-upload')
 PreviewUI     = require('controllers/ui/preview')
@@ -48,7 +44,6 @@ class VideoForm extends Spine.Controller
     'change select[name=site]': 'siteChange'
     'blur input[name=slug]':    'updateSlug'
     'click .fullscreen-button': 'fullscreen'
-    'click .import-button':     'import'
 
   constructor: ->
     super
@@ -151,49 +146,6 @@ class VideoForm extends Spine.Controller
       @fullscreenButton.html "Exit #{@fullscreenButtonText}"
       @previewUI = new PreviewUI field: @formBody
 
-  import: (e) =>
-    # For importing old HTML to Markdown directly from old location
-    e?.preventDefault()
-    url = $.trim prompt("Paste a URL from #{@formSite.val()}", @item.old_url or '')
-    if url
-      $.ajax
-        type: 'GET'
-        url: url
-        success: (res) =>
-          $html = $(res.responseText)
-          $title = $html.find('.post > h2:first > a')
-          $author = $html.find('.post .entry-author > a:first')
-          $date = $html.find('.post .entry-date > .published')
-          $content = $html.find('.post .entry:first')
-          $image = $content.find('img:first')
-          if $content
-            $content.find('.addthis_toolbox, .author-bio').remove()
-            options =
-                link_list:  false    # render links as references, create link list as appendix
-                h1_setext:  true     # underline h1 headers
-                h2_setext:  true     # underline h2 headers
-                h_atx_suf:  true     # header suffixes (###)
-                gfm_code:   false    # render code blocks as via ``` delims
-                li_bullet:  "*"      # list item bullet style
-                hr_char:    "-"      # hr style
-                indnt_str:  "    "   # indentation string
-                bold_char:  "*"      # char used for strong
-                emph_char:  "_"      # char used for em
-                gfm_tbls:   false    # markdown-extra tables
-                tbl_edges:  false    # show side edges on tables
-                hash_lnks:  false    # anchors w/hash hrefs as links
-            reMarker = new reMarked(options)
-            markdown = reMarker.render($content.html())
-            @formBody.val(markdown)
-
-          if not @item.old_url
-            @formTitle.val($title.text()) if $title
-            $slug = @form.find('input[name=slug]')
-            unless slug.val()
-              $slug.val($title.attr('href').replace('www.', '').replace("http://#{@formSite.val().replace('www.', '')}", '')) if $title
-            @formAuthorId.val($author.text()) if $author
-            @form.find('input[name=published_at]').val($date.text()) if $date
-
   save: (e) ->
     e.preventDefault()
     if not navigator.onLine

+ 1 - 1
admin/kanso.json

@@ -1,6 +1,6 @@
 {
   "name": "admin",
-  "version": "0.5.3",
+  "version": "0.6.0",
   "description": "Kleks admin application that allows management of sites and content.",
   "load": "server/setup",
   "modules": ["server","controllers","models","lib"],

+ 0 - 75
admin/lib/jquery-xdomainajax.js

@@ -1,75 +0,0 @@
-/**
- * jQuery.ajax mid - CROSS DOMAIN AJAX 
- * ---
- * @author James Padolsey (http://james.padolsey.com)
- * @version 0.11
- * @updated 12-JAN-10
- * ---
- * Note: Read the README!
- * ---
- * @info http://james.padolsey.com/javascript/cross-domain-requests-with-jquery/
- */
-
-jQuery.ajax = (function(_ajax){
-    
-    var protocol = location.protocol,
-        hostname = location.hostname,
-        exRegex = RegExp(protocol + '//' + hostname),
-        YQL = 'http' + (/^https/.test(protocol)?'s':'') + '://query.yahooapis.com/v1/public/yql?callback=?',
-        query = 'select * from html where url="{URL}" and xpath="*"';
-    
-    function isExternal(url) {
-        return !exRegex.test(url) && /:\/\//.test(url);
-    }
-    
-    return function(o) {
-        
-        var url = o.url;
-        
-        if ( /get/i.test(o.type) && !/json/i.test(o.dataType) && isExternal(url) ) {
-            
-            // Manipulate options so that JSONP-x request is made to YQL
-            
-            o.url = YQL;
-            o.dataType = 'json';
-            
-            o.data = {
-                q: query.replace(
-                    '{URL}',
-                    url + (o.data ?
-                        (/\?/.test(url) ? '&' : '?') + jQuery.param(o.data)
-                    : '')
-                ),
-                format: 'xml'
-            };
-            
-            // Since it's a JSONP request
-            // complete === success
-            if (!o.success && o.complete) {
-                o.success = o.complete;
-                delete o.complete;
-            }
-            
-            o.success = (function(_success){
-                return function(data) {
-                    
-                    if (_success) {
-                        // Fake XHR callback.
-                        _success.call(this, {
-                            responseText: (data.results[0] || '')
-                                // YQL screws with <script>s
-                                // Get rid of them
-                                .replace(/<script[^>]+?\/>|<script(.|\s)*?\/script>/gi, '')
-                        }, 'success');
-                    }
-                    
-                };
-            })(o.success);
-            
-        }
-        
-        return _ajax.apply(this, arguments);
-        
-    };
-    
-})(jQuery.ajax);

+ 0 - 556
admin/lib/reMarked.js

@@ -1,556 +0,0 @@
-/**
-* Copyright (c) 2012, Leon Sorokin
-* All rights reserved. (MIT Licensed)
-*
-* reMarked.js - DOM > markdown
-*/
-
-reMarked = function(opts) {
-
-	var links = [];
-	var cfg = {
-		link_list:	false,			// render links as references, create link list as appendix
-	//  link_near:					// cite links immediately after blocks
-		h1_setext:	true,			// underline h1 headers
-		h2_setext:	true,			// underline h2 headers
-		h_atx_suf:	false,			// header suffix (###)
-	//	h_compact:	true,			// compact headers (except h1)
-		gfm_code:	false,			// render code blocks as via ``` delims
-		li_bullet:	"*-+"[0],		// list item bullet style
-	//	list_indnt:					// indent top-level lists
-		hr_char:	"-_*"[0],		// hr style
-		indnt_str:	["    ","\t","  "][0],	// indentation string
-		bold_char:	"*_"[0],		// char used for strong
-		emph_char:	"*_"[1],		// char used for em
-		gfm_tbls:	true,			// markdown-extra tables
-		tbl_edges:	false,			// show side edges on tables
-		hash_lnks:	false,			// anchors w/hash hrefs as links
-	};
-
-	extend(cfg, opts);
-
-	function extend(a, b) {
-		if (!b) return a;
-		for (var i in a) {
-			if (typeof b[i] !== "undefined")
-				a[i] = b[i];
-		}
-	}
-
-	function rep(str, num) {
-		var s = "";
-		while (num-- > 0)
-			s += str;
-		return s;
-	}
-
-	function trim12(str) {
-		var	str = str.replace(/^\s\s*/, ''),
-			ws = /\s/,
-			i = str.length;
-		while (ws.test(str.charAt(--i)));
-		return str.slice(0, i + 1);
-	}
-
-	function lpad(targ, padStr, len) {
-		return rep(padStr, len - targ.length) + targ;
-	}
-
-	function rpad(targ, padStr, len) {
-		return targ + rep(padStr, len - targ.length);
-	}
-
-	function otag(tag) {
-		if (!tag) return "";
-		return "<" + tag + ">";
-	}
-
-	function ctag(tag) {
-		if (!tag) return "";
-		return "</" + tag + ">";
-	}
-
-	function pfxLines(txt, pfx)	{
-		return txt.replace(/^/gm, pfx);
-	}
-
-	function nodeName(e) {
-		return (e.nodeName == "#text" ? "txt" : e.nodeName).toLowerCase();
-	}
-
-	function wrap(str, opts) {
-		var pre, suf;
-
-		if (opts instanceof Array) {
-			pre = opts[0];
-			suf = opts[1];
-		}
-		else
-			pre = suf = opts;
-
-		pre = pre instanceof Function ? pre.call(this, str) : pre;
-		suf = suf instanceof Function ? suf.call(this, str) : suf;
-
-		return pre + str + suf;
-	}
-
-	this.render = function(ctr) {
-		if (typeof ctr == "string") {
-			var htmlstr = ctr;
-			ctr = document.createElement("div");
-			ctr.innerHTML = htmlstr;
-		}
-		var s = new lib.tag(ctr, null, 0);
-		var re = s.rend().replace(/^[\t ]+\n/gm, "\n");
-		if (cfg.link_list) {
-			// hack
-			re += "\n\n";
-			var maxlen = 0;
-			// get longest link href with title, TODO: use getAttribute?
-			for (var y in links) {
-				if (!links[y].e.title) continue;
-				var len = links[y].e.href.length;
-				if (len && len > maxlen)
-					maxlen = len;
-			}
-
-			for (var k in links) {
-				var title = links[k].e.title ? rep(" ", (maxlen + 2) - links[k].e.href.length) + '"' + links[k].e.title + '"' : "";
-				re += "  [" + (+k+1) + "]: " + links[k].e.href + title + "\n";
-			}
-		}
-
-		return re.replace(/^[\t ]+\n/gm, "\n");
-	};
-
-	var lib = {};
-
-	lib.tag = klass({
-		wrap: "",
-		lnPfx: "",		// only block
-		lnInd: 0,		// only block
-		init: function(e, p, i)
-		{
-			this.e = e;
-			this.p = p;
-			this.i = i;
-			this.c = [];
-			this.tag = nodeName(e);
-
-			this.initK();
-		},
-
-		initK: function()
-		{
-			var i;
-			if (this.e.hasChildNodes()) {
-				// inline elems allowing adjacent whitespace text nodes to be rendered
-				var inlRe = /^(?:a|strong|code|em|sub|sup|del|i|u|b|big|center)$/, n, name;
-				for (i in this.e.childNodes) {
-					if (!/\d+/.test(i)) continue;
-
-					n = this.e.childNodes[i];
-					name = nodeName(n);
-
-					// ignored tags
-					if (/style|script|canvas|video|audio/.test(name))
-						continue;
-
-					// empty whitespace handling
-					if (name == "txt" && /^\s+$/.test(n.textContent)) {
-						// ignore if first or last child (trim)
-						if (i == 0 || i == this.e.childNodes.length - 1 || !this.p)
-							continue;
-
-						// only ouput when has an adjacent inline elem
-						var prev = this.e.childNodes[i-1],
-							next = this.e.childNodes[i+1];
-						if (prev && !nodeName(prev).match(inlRe) || next && !nodeName(next).match(inlRe))
-							continue;
-					}
-					if (!lib[name])
-						name = "tag";
-
-					var node = new lib[name](n, this, this.c.length);
-
-					if (node instanceof lib.a && n.href || node instanceof lib.img) {
-						node.lnkid = links.length;
-						links.push(node);
-					}
-
-					this.c.push(node);
-				}
-			}
-		},
-
-		rend: function()
-		{
-			return this.rendK().replace(/\n{3,}/gm, "\n\n");		// can screw up pre and code :(
-		},
-
-		rendK: function()
-		{
-			var n, buf = "";
-			for (var i in this.c) {
-				n = this.c[i];
-				buf += (n.bef || "") + n.rend() + (n.aft || "");
-			}
-			return buf.replace(/^\n+|\n+$/, "");
-		}
-	});
-
-	lib.blk = lib.tag.extend({
-		wrap: ["\n\n", ""],
-		wrapK: null,
-		tagr: false,
-		lnInd: null,
-		init: function(e, p ,i) {
-			this.supr(e,p,i);
-
-			// kids indented
-			if (this.lnInd === null) {
-				if (this.p && this.tagr && this.c[0] instanceof lib.blk)
-					this.lnInd = 4;
-				else
-					this.lnInd = 0;
-			}
-
-			// kids wrapped?
-			if (this.wrapK === null) {
-				if (this.tagr && this.c[0] instanceof lib.blk)
-					this.wrapK = "\n";
-				else
-					this.wrapK = "";
-			}
-		},
-
-		rend: function()
-		{
-			return wrap.call(this, (this.tagr ? otag(this.tag) : "") + wrap.call(this, pfxLines(pfxLines(this.rendK(), this.lnPfx), rep(" ", this.lnInd)), this.wrapK) + (this.tagr ? ctag(this.tag) : ""), this.wrap);
-		},
-
-		rendK: function()
-		{
-			var kids = this.supr();
-			// remove min uniform leading spaces from block children. marked.js's list outdent algo sometimes leaves these
-			if (this.p instanceof lib.li) {
-				var repl = null, spcs = kids.match(/^[\t ]+/gm);
-				if (!spcs) return kids;
-				for (var i in spcs) {
-					if (repl === null || spcs[i][0].length < repl.length)
-						repl = spcs[i][0];
-				}
-				return kids.replace(new RegExp("^" + repl), "");
-			}
-			return kids;
-		}
-	});
-
-	lib.tblk = lib.blk.extend({tagr: true});
-
-	lib.cblk = lib.blk.extend({wrap: ["\n", ""]});
-		lib.ctblk = lib.cblk.extend({tagr: true});
-
-	lib.inl = lib.tag.extend({
-		rend: function()
-		{
-			return wrap.call(this, this.rendK(), this.wrap);
-		}
-	});
-
-		lib.tinl = lib.inl.extend({
-			tagr: true,
-			rend: function()
-			{
-				return otag(this.tag) + wrap.call(this, this.rendK(), this.wrap) + ctag(this.tag);
-			}
-		});
-
-		lib.p = lib.blk.extend({
-			rendK: function() {
-				return this.supr().replace(/^\s+/gm, "");
-			}
-		});
-
-		lib.div = lib.p.extend();
-
-		lib.span = lib.inl.extend();
-
-		lib.list = lib.blk.extend({
-			expn: false,
-			wrap: [function(){return this.p instanceof lib.li ? "\n" : "\n\n";}, ""]
-		});
-
-		lib.ul = lib.list.extend({});
-
-		lib.ol = lib.list.extend({});
-
-		lib.li = lib.cblk.extend({
-			wrap: ["\n", function(kids) {
-				return this.p.expn || kids.match(/\n{2}/gm) ? "\n" : "";			// || this.kids.match(\n)
-			}],
-			wrapK: [function() {
-				return this.p.tag == "ul" ? cfg.li_bullet + " " : (this.i + 1) + ".  ";
-			}, ""],
-			rendK: function() {
-				return this.supr().replace(/\n([^\n])/gm, "\n" + cfg.indnt_str + "$1");
-			}
-		});
-
-		lib.hr = lib.blk.extend({
-			wrap: ["\n\n", rep(cfg.hr_char, 3)]
-		});
-
-		lib.h = lib.blk.extend({});
-
-		lib.h_setext = lib.h.extend({});
-
-			cfg.h1_setext && (lib.h1 = lib.h_setext.extend({
-				wrapK: ["", function(kids) {
-					return "\n" + rep("=", kids.length);
-				}]
-			}));
-
-			cfg.h2_setext && (lib.h2 = lib.h_setext.extend({
-				wrapK: ["", function(kids) {
-					return "\n" + rep("-", kids.length);
-				}]
-			}));
-
-		lib.h_atx = lib.h.extend({
-			wrapK: [
-				function(kids) {
-					return rep("#", this.tag[1]) + " ";
-				},
-				function(kids) {
-					return cfg.h_atx_suf ? " " + rep("#", this.tag[1]) : "";
-				}
-			]
-		});
-			!cfg.h1_setext && (lib.h1 = lib.h_atx.extend({}));
-
-			!cfg.h2_setext && (lib.h2 = lib.h_atx.extend({}));
-
-			lib.h3 = lib.h_atx.extend({});
-
-			lib.h4 = lib.h_atx.extend({});
-
-			lib.h5 = lib.h_atx.extend({});
-
-			lib.h6 = lib.h_atx.extend({});
-
-		lib.a = lib.inl.extend({
-			lnkid: null,
-			rend: function() {
-				var kids = this.rendK(),
-					href = this.e.getAttribute("href"),
-					title = this.e.title ? ' "' + this.e.title + '"' : "";
-
-				if (!href || href == kids || href[0] == "#" && !cfg.hash_lnks)
-					return kids;
-
-				if (cfg.link_list)
-					return "[" + kids + "] [" + (this.lnkid + 1) + "]";
-
-				return "[" + kids + "](" + href + title + ")";
-			}
-		});
-
-		// almost identical to links, maybe merge
-		lib.img = lib.inl.extend({
-			lnkid: null,
-			rend: function() {
-				var kids = this.e.alt,
-					src = this.e.getAttribute("src");
-
-				if (cfg.link_list)
-					return "[" + kids + "] [" + (this.lnkid + 1) + "]";
-
-				var title = this.e.title ? ' "'+ this.e.title + '"' : "";
-
-				return "![" + kids + "](" + src + title + ")";
-			}
-		});
-
-
-		lib.em = lib.inl.extend({wrap: cfg.emph_char});
-
-			lib.i = lib.em.extend();
-
-		lib.del = lib.tinl.extend();
-
-		lib.br = lib.inl.extend({
-			wrap: ["", function() {
-				// br in headers output as html
-				return this.p instanceof lib.h ? "<br>" : "  \n";
-			}]
-		});
-
-		lib.strong = lib.inl.extend({wrap: rep(cfg.bold_char, 2)});
-
-			lib.b = lib.strong.extend();
-
-		lib.dl = lib.tblk.extend({lnInd: 2});
-
-		lib.dt = lib.ctblk.extend();
-
-		lib.dd = lib.ctblk.extend();
-
-		lib.sub = lib.tinl.extend();
-
-		lib.sup = lib.tinl.extend();
-
-		lib.blockquote = lib.blk.extend({
-			lnPfx: "> ",
-			rend: function() {
-				return this.supr().replace(/>[ \t]$/gm, ">");
-			}
-		});
-
-		// can render with or without tags
-		lib.pre = lib.blk.extend({
-			tagr: true,
-			wrapK: "\n",
-			lnInd: 0
-		});
-
-		// can morph into inline based on context
-		lib.code = lib.blk.extend({
-			tagr: false,
-			wrap: "",
-			wrapK: function(kids) {
-				return kids.indexOf("`") !== -1 ? "``" : "`";	// esc double backticks
-			},
-			lnInd: 0,
-			init: function(e, p, i) {
-				this.supr(e, p, i);
-
-				if (this.p instanceof lib.pre) {
-					this.p.tagr = false;
-
-					if (cfg.gfm_code) {
-						var cls = this.e.getAttribute("class");
-						this.wrapK = ["```" + (cls ? " " + cls : "") + "\n", "\n```"];
-					}
-					else {
-						this.wrapK = "";
-						this.p.lnInd = 4;
-					}
-				}
-			}
-		});
-
-		lib.table = cfg.gfm_tbls ? lib.blk.extend({
-			cols: [],
-			init: function(e, p, i) {
-				this.supr(e, p, i);
-				this.cols = [];
-			},
-			rend: function() {
-				// run prep on all cells to get max col widths
-				for (var tsec in this.c)
-					for (var row in this.c[tsec].c)
-						for (var cell in this.c[tsec].c[row].c)
-							this.c[tsec].c[row].c[cell].prep();
-
-				return this.supr();
-			}
-		}) : lib.tblk.extend();
-
-		lib.thead = cfg.gfm_tbls ? lib.cblk.extend({
-			wrap: ["\n", function(kids) {
-				var buf = "";
-				for (var i in this.p.cols) {
-					var col = this.p.cols[i],
-						al = col.a[0] == "c" ? ":" : " ",
-						ar = col.a[0] == "r" || col.a[0] == "c" ? ":" : " ";
-
-					buf += (i == 0 && cfg.tbl_edges ? "|" : "") + al + rep("-", col.w) + ar + (i < this.p.cols.length-1 || cfg.tbl_edges ? "|" : "");
-				}
-				return "\n" + trim12(buf);
-			}]
-		}) : lib.ctblk.extend();
-
-		lib.tbody = cfg.gfm_tbls ? lib.cblk.extend() : lib.ctblk.extend();
-
-		lib.tfoot = cfg.gfm_tbls ? lib.cblk.extend() : lib.ctblk.extend();
-
-		lib.tr = cfg.gfm_tbls ? lib.cblk.extend({
-			wrapK: [cfg.tbl_edges ? "| " : "", cfg.tbl_edges ? " |" : ""],
-		}) : lib.ctblk.extend();
-
-		lib.th = cfg.gfm_tbls ? lib.inl.extend({
-			guts: null,
-			// TODO: DRY?
-			wrap: [function() {
-				var col = this.p.p.p.cols[this.i],
-					spc = this.i == 0 ? "" : " ",
-					pad, fill = col.w - this.guts.length;
-
-				switch (col.a[0]) {
-					case "r": pad = rep(" ", fill); break;
-					case "c": pad = rep(" ", Math.floor(fill/2)); break;
-					default:  pad = "";
-				}
-
-				return spc + pad;
-			}, function() {
-				var col = this.p.p.p.cols[this.i],
-					edg = this.i == this.p.c.length - 1 ? "" : " |",
-					pad, fill = col.w - this.guts.length;
-
-				switch (col.a[0]) {
-					case "r": pad = ""; break;
-					case "c": pad = rep(" ", Math.ceil(fill/2)); break;
-					default:  pad = rep(" ", fill);
-				}
-
-				return pad + edg;
-			}],
-			prep: function() {
-				this.guts = this.rendK();					// pre-render
-				this.rendK = function() {return this.guts};
-
-				var cols = this.p.p.p.cols;
-				if (!cols[this.i])
-					cols[this.i] = {w: null, a: ""};		// width and alignment
-				var col = cols[this.i];
-				col.w = Math.max(col.w || 0, this.guts.length);
-				if (this.e.align)
-					col.a = this.e.align;
-			},
-		}) : lib.ctblk.extend();
-
-			lib.td = lib.th.extend();
-
-		lib.txt = lib.inl.extend({
-			initK: function()
-			{
-				this.c = this.e.textContent.split(/^/gm);
-			},
-			rendK: function()
-			{
-				var kids = this.c.join("").replace(/\r/gm, "");
-
-				// this is strange, cause inside of code, inline should not be processed, but is?
-				if (!(this.p instanceof lib.code || this.p instanceof lib.pre)) {
-					kids = kids
-					.replace(/^\s*#/gm,"\\#")
-					.replace(/\*/gm,"\\*");
-				}
-
-				if (this.i == 0)
-					kids = kids.replace(/^\n+/, "");
-				if (this.i == this.p.c.length - 1)
-					kids = kids.replace(/\n+$/, "");
-				return kids;
-			}
-		});
-};
-
-/*!
-  * klass: a classical JS OOP façade
-  * https://github.com/ded/klass
-  * License MIT (c) Dustin Diaz & Jacob Thornton 2012
-  */
-!function(a,b){typeof define=="function"?define(b):typeof module!="undefined"?module.exports=b():this[a]=b()}("klass",function(){function f(a){return j.call(g(a)?a:function(){},a,1)}function g(a){return typeof a===c}function h(a,b,c){return function(){var d=this.supr;this.supr=c[e][a];var f=b.apply(this,arguments);return this.supr=d,f}}function i(a,b,c){for(var f in b)b.hasOwnProperty(f)&&(a[f]=g(b[f])&&g(c[e][f])&&d.test(b[f])?h(f,b[f],c):b[f])}function j(a,b){function c(){}function l(){this.init?this.init.apply(this,arguments):(b||h&&d.apply(this,arguments),j.apply(this,arguments))}c[e]=this[e];var d=this,f=new c,h=g(a),j=h?a:this,k=h?{}:a;return l.methods=function(a){return i(f,a,d),l[e]=f,this},l.methods.call(l,k).prototype.constructor=l,l.extend=arguments.callee,l[e].implement=l.statics=function(a,b){return a=typeof a=="string"?function(){var c={};return c[a]=b,c}():a,i(this,a,d),this},l}var a=this,b=a.klass,c="function",d=/xyz/.test(function(){xyz})?/\bsupr\b/:/.*/,e="prototype";return f.noConflict=function(){return a.klass=b,this},a.klass=f,f});

+ 5 - 3
admin/lib/spine-couch-ajax.coffee

@@ -85,7 +85,7 @@ CouchAjax =
       @requests.push(callback)
     else
       @pending = true
-      @request(callback)    
+      @request(callback)
     callback
     
 class Base
@@ -190,6 +190,8 @@ class Singleton extends Base
         data = @model.fromJSON(_.pluck(data.rows, "doc")[0])
       else
         data = @model.fromJSON(data)
+
+      data._rev = xhr.getResponseHeader( 'X-Couch-Update-NewRev' )
     
       CouchAjax.disable =>
         if data
@@ -209,7 +211,7 @@ class Singleton extends Base
       options.error?.apply(@record)
       
       # Popup an alert box that we could communicate with server
-      alert "Could NOT communicate with server for \"#{@record.title or @record.name}\".\n\nCheck your connection and try again."
+      alert "#{statusText}\n#{error}\nSomehting may have gone wrong with an action for \"#{@record.title or @record.name}\".\n\nCheck your connection and try again."
 
 # CouchAjax endpoint
 Model.host = ''
@@ -223,7 +225,7 @@ Include =
     base += encodeURIComponent(@id)
     base
     
-Extend = 
+Extend =
   ajax: -> new Collection(this)
 
   url: ->

+ 77 - 0
admin/lib/spine-couch-changes.coffee

@@ -0,0 +1,77 @@
+Spine    = require('spine/core')
+db       = require('db')
+duality  = require('duality/core')
+session  = require('session')
+
+
+feeds = {} # Cache `_changes` feeds by their url
+
+
+Spine.Model.CouchChanges = (opts = {})->
+  opts.url = opts.url or duality.getDBURL()
+  opts.handler = Spine.Model.CouchChanges.Changes unless opts.handler
+  feeds[opts.url] or feeds[opts.url] =
+    changes: new opts.handler opts
+    extended: ->
+      # need to keep _rev around to support changes feed processing
+      @attributes.push "_rev" unless @attributes[ "_rev" ]
+      @changes.subscribe @className, @
+
+
+Spine.Model.CouchChanges.Changes = class Changes
+  subscribers: {}
+  query: include_docs: yes
+
+  constructor: (options = {})->
+    @url = options.url
+    @startListening()
+
+  subscribe: (classname, klass) =>
+    @subscribers[classname.toLowerCase()] = klass
+
+  startListening: =>
+    db.use(@url).changes @query, @handler()
+
+  # returns handler which you may disable by setting handler.disabled flag `true`
+  handler: -> self = (err, resp) =>
+    if self.disabled then false
+    else if err then false # TODO? @trigger error
+    else
+      @acceptChanges resp?.results
+      true
+
+  acceptChanges: (changes)->
+    return unless changes
+    Spine.CouchAjax.disable =>
+      for doc in changes
+        if type = doc.doc?.type
+          klass = @subscribers[type]
+        unless klass
+          console.warn "changes: can't find subscriber for #{doc.doc.type}"
+          continue
+        atts = doc.doc
+        atts.id = atts._id unless atts.id
+        try
+          obj = klass.find atts.id
+          if doc.deleted
+            obj.destroy()
+          else
+            unless obj._rev is atts._rev
+              obj.updateAttributes atts
+        catch e
+          klass.create atts unless doc.deleted
+
+
+# Start listening for _changes only when user is authenticated
+#   and stop listening for changes when he logged out
+Spine.Model.CouchChanges.PrivateChanges = class PrivateChanges extends Changes
+  startListening: =>
+    session.on "change", @authChanged
+
+  authChanged: (userCtx)=>
+    if userCtx.name
+      @currentHandler.disabled = true if @currentHandler
+      @currentHandler = @handler()
+      db.use(@url).changes @query, @currentHandler
+    else
+      @currentHandler.disabled = true if @currentHandler

+ 2 - 2
admin/models/author.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -9,6 +7,8 @@ class Author extends BaseModel
   @configure "Author", "site", "name", "email", "bio", "links", "photo"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @queryOn: ['name','email']
     

+ 2 - 2
admin/models/block.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -9,6 +7,8 @@ class Block extends BaseModel
   @configure "Block", "site", "code", "name", "content", "photo", "enabled", "_attachments"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @queryOn: ['name','code']
     

+ 2 - 2
admin/models/collection.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils  = require('lib/utils')
 moment = require('lib/moment')
 
@@ -10,6 +8,8 @@ class Collection extends BaseModel
   @configure "Collection", "site", "slug", "name", "intro", "photo", "pinned", "hidden", "updated_at", "sponsor_id", "sponsor_start", "sponsor_end", "sponsor_propagate", "sponsors_history", "_attachments"
 
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
 
   @dateSort: (a, b) ->
     if (a.updated_at or a.name) < (b.updated_at or b.name) then 1 else -1

+ 2 - 2
admin/models/contact.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -9,6 +7,8 @@ class Contact extends BaseModel
   @configure "Contact", "name", "email", "note"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @queryOn: ['name','email']
     

+ 2 - 2
admin/models/essay.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 moment = require('lib/moment')
 
@@ -10,6 +8,8 @@ class Essay extends BaseModel
   @configure "Essay", "site", "slug", "title", "intro", "body", "photo", "published", "published_at", "updated_at", "author_id", "sponsor_id", "sponsor_start", "sponsor_end", "sponsors_history", "collections", "_attachments"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @alphaSort: (a, b) ->
     if (a.title or a.published_at) > (b.title or b.published_at) then 1 else -1

+ 2 - 2
admin/models/redirect.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -9,6 +7,8 @@ class Redirect extends BaseModel
   @configure "Redirect", "_id", "site", "slug", "location"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @queryOn: ['slug','location']
     

+ 2 - 2
admin/models/scene.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 moment = require('lib/moment')
 
@@ -10,6 +8,8 @@ class Scene extends BaseModel
   @configure "Scene", "site", "slug", "title", "body", "photo", "published", "published_at", "updated_at", "author_id", "sponsor_id", "sponsor_start", "sponsor_end", "sponsors_history", "collections", "_attachments"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @alphaSort: (a, b) ->
     if (a.title or a.published_at) > (b.title or b.published_at) then 1 else -1

+ 2 - 2
admin/models/site.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -9,6 +7,8 @@ class Site extends BaseModel
   @configure "Site", "_id", "name", "name_html", "tagline", "menu_html", "header_html", "bottom_html", "footer_html", "link", "social_links", "theme", "css", "seo_description", "seo_keywords", "google_analytics_code", "editor_email", "admin_email", "default_ad_unit", "default_ad_enabled"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @queryOn: ['name','tagline','_id']
     

+ 2 - 2
admin/models/sponsor.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 
 BaseModel = require('models/base')
@@ -11,6 +9,8 @@ class Sponsor extends BaseModel
   @configure "Sponsor", "format", "name", "link", "label", "show_label", "content", "include_default_ad_unit", "image", "note", "contact_id", "_attachments"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
 
   @queryOn: ['name','content','link','format']
     

+ 2 - 2
admin/models/video.coffee

@@ -1,6 +1,4 @@
 Spine = require('spine/core')
-require('lib/spine-couch-ajax')
-
 utils = require('lib/utils')
 moment = require('lib/moment')
 
@@ -10,6 +8,8 @@ class Video extends BaseModel
   @configure "Video", "site", "slug", "title", "intro", "body", "video", "photo", "published", "published_at", "updated_at", "author_id", "sponsor_id", "sponsor_start", "sponsor_end", "sponsors_history", "collections", "_attachments"
   
   @extend @CouchAjax
+  @extend @CouchChanges
+    handler: @CouchChanges.PrivateChanges
   
   @alphaSort: (a, b) ->
     if (a.title or a.published_at) > (b.title or b.published_at) then 1 else -1

+ 2 - 1
admin/server/rewrites.coffee

@@ -42,7 +42,8 @@ module.exports = [
     to: "_view/docs_by_type",
     method: "GET",
     query: {
-      key: [":type", ":id"],
+      startkey: [":type", ":id"],
+      endkey: [":type", ":id", {}],
       include_docs: "true"
     }
   }

+ 1 - 1
admin/templates/author-form.html

@@ -48,7 +48,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
admin/templates/block-form.html

@@ -36,7 +36,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
admin/templates/collection-form.html

@@ -53,7 +53,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
admin/templates/contact-form.html

@@ -24,7 +24,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 2 - 2
admin/templates/essay-form.html

@@ -39,7 +39,7 @@
       <textarea name="intro" placeholder="Provide an essay introduction text as Markdown/HTML">{{intro}}</textarea>
     </div>
     <div class="field">
-      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a> | <a class="import-button">Import</a></label>
+      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a></label>
       <textarea name="body" placeholder="Write your essay content as Markdown/HTML">{{body}}</textarea>
     </div>
   </div>
@@ -48,7 +48,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 3 - 2
admin/templates/main-nav.html

@@ -12,12 +12,13 @@
   <!-- <li class="profiles"><a href="#/profiles"><i class="icon icon-profile"></i>Profiles</a></li> -->
   <li class="collections"><a href="#/collections"><i class="icon icon-collection"></i>Collections</a></li>
   <li class="seperator"></li>
-  <li class="sites"><a href="#/sites"><i class="icon icon-site"></i>Sites</a></li>
   <li class="blocks"><a href="#/blocks"><i class="icon icon-block"></i>Blocks</a></li>
   <li class="authors"><a href="#/authors"><i class="icon icon-author"></i>Authors</a></li>
+  <li class="redirects"><a href="#/redirects"><i class="icon icon-link"></i>Redirects</a></li>
+  <li class="seperator"></li>
+  <li class="sites"><a href="#/sites"><i class="icon icon-site"></i>Sites</a></li>
   <li class="sponsors"><a href="#/sponsors"><i class="icon icon-sponsor"></i>Sponsors</a></li>
   <li class="contacts"><a href="#/contacts"><i class="icon icon-contact"></i>Contacts</a></li>
-  <li class="redirects"><a href="#/redirects"><i class="icon icon-link"></i>Redirects</a></li>
   <li class="seperator"></li>
   <li class="logout"><a class="logout-button"><i class="icon icon-off"></i>Sign Out</a></li>
 </ul>

+ 1 - 1
admin/templates/redirect-form.html

@@ -32,7 +32,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 2 - 2
admin/templates/scene-form.html

@@ -35,7 +35,7 @@
       </div>
     </div>
     <div class="field">
-      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a> | <a class="import-button">Import</a></label>
+      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a></label>
       <textarea name="body" placeholder="Write your scene content as Markdown/HTML">{{body}}</textarea>
     </div>
   </div>
@@ -44,7 +44,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
admin/templates/site-form.html

@@ -102,7 +102,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
admin/templates/sponsor-form.html

@@ -54,7 +54,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 2 - 2
admin/templates/video-form.html

@@ -43,7 +43,7 @@
       <textarea name="intro" placeholder="Provide an video introduction text as Markdown/HTML">{{intro}}</textarea>
     </div>
     <div class="field">
-      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a> | <a class="import-button">Import</a></label>
+      <label>Content as <a class="markdown-help">Markdown/HTML</a> | <a class="fullscreen-button">Fullscreen</a></label>
       <textarea name="body" placeholder="Write your video content as Markdown/HTML">{{body}}</textarea>
     </div>
   </div>
@@ -52,7 +52,7 @@
     <div class="buttons">
       <button class="save-button" tabindex="0">Save</button>
       <button class="cancel-button plain" tabindex="0">Cancel</button>
-      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+      {{#if id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
     </div>
 
     <div class="top-spacer"></div>

+ 1 - 1
site/kanso.json

@@ -1,6 +1,6 @@
 {
   "name": "site",
-  "version": "0.5.3",
+  "version": "0.6.0",
   "description": "Kleks site application that renders each site.",
   "load": "server/setup",
   "modules": ["server","lib"],