Source: kawapp.js

/*! kawapp.js */

/**
 * Kyukou asynchronous Web application framework
 */

/**
 * Application class.
 * @class kawapp
 */
function kawapp() {
  if (!(this instanceof kawapp)) return new kawapp();
}

(function(kawapp) {
  // for node.js environment
  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
    module.exports = kawapp;
  }

  kawapp.request = Object;

  /**
   * Alias to `kawapp.END`.
   *
   * @member {Object} kawapp.prototype.END
   */
  /**
   * A signature to terminate the middleware sequence.
   *
   * @member {Object} kawapp.END
   * @example
   * var app = kawapp();
   *
   * app.use(function(context, canvas, next) {
   *   next(kawapp.END); // send END signal to stop the application
   * });
   *
   * app.use(other_mw);  // this middleware will never be invoked
   */
  kawapp.prototype.END = kawapp.END = { end: true };

  /**
   * Alias to `kawapp.SKIP`.
   *
   * @member {Object} kawapp.prototype.SKIP
   */
  /**
   * A signature to skip the middleware sequence.
   *
   * @member {Object} kawapp.SKIP
   * @example
   * var sub = kawapp();
   * sub.use(function(context, canvas, next) {
   *   next(kawapp.SKIP);
   * });
   * sub.use(mw1); // this middleware will never be invoked
   *
   * var main = kawapp();
   * main.use(sub);
   * main.use(mw2); // this middleware will be invoked otherwise
   */
  kawapp.prototype.SKIP = kawapp.SKIP = { skip: true };

  /**
   * Number of middlewares installed.
   * This means kawapp instance behaves as an Array-like object.
   * @type {Number}
   */
  kawapp.prototype.length = 0;

  /**
   * This installs middleware(s).
   * @param {...Function} mw - Middleware(s) to install
   * @returns {kawapp}
   * @example
   * var app = kawapp();
   *
   * app.use(function(context, canvas, next) {
   *   var text = context.ok ? "OK" : "NG"; // use context as a locals
   *   canvas.append(text);                // use canvas with append() html() empty() methods
   *   next();                          // callback to chain middlewares
   * });
   *
   * app.use(function(context, canvas, next) {
   *   var err = new Error("something wrong");
   *   next(err);                       // send error to terminate the application
   * });
   */
  kawapp.prototype.use = function(mw) {
    for (var i = 0; i < arguments.length; i++) {
      this[this.length++] = arguments[i];
    }
    return this; // method chaining
  };

  /**
   * This installs middleware(s) which are invoked when conditional function returns true.
   * @param {Function} cond - Conditional function
   * @param {...Function} mw - Middleware(s) to install
   * @returns {kawapp}
   * @example
   * var app = kawapp();
   *
   * app.useif(test, mw1, mw2);     // mw1&mw2 will be invoked only when condition is true
   *
   * app.use(mw3, mw4);             // mw3&mw4 will be invoked only when condition is false
   *
   * function test(context, canvas) {
   *   return (context.key == "value"); // test something
   * }
   */
  kawapp.prototype.useif = function(cond, mw) {
    var args = Array.prototype.slice.call(arguments, 1);
    var subapp = kawapp();
    subapp.use(useif);
    subapp.use.apply(subapp, args);
    subapp.use(end);

    // this.when(useif, mw, ...)
    return this.use(subapp);

    function useif(context, canvas, next) {
      var ret = cond(context);
      if (ret instanceof Error) {
        next(ret);
      } else {
        next(ret ? null : kawapp.SKIP);
      }
    }

    function end(context, canvas, next) {
      next(kawapp.END);
    }
  };

  /**
   * This installs middleware(s) which are invoked when `location.pathname` matches.
   *
   * @param {String|RegExp} path - pathname to test
   * @param {...Function} mw - Middleware(s) to install
   * @returns {kawapp}
   * @example
   * var app = kawapp();
   *
   * app.mount("/about/", about_mw);        // test pathname with string
   *
   * app.mount(/^\/contact\//, contact_mw); // test pathname with regexp
   *
   * app.mount("/detail/", mw1, mw2, mw3);  // multiple middlewares to install
   */
  kawapp.prototype.mount = function(path, mw) {
    var args = Array.prototype.slice.call(arguments, 1);

    // insert location middleware at the first use of mount()
    if (!this.mounts) {
      this.use(kawapp.mw.location());
      this.mounts = 0;
    }
    this.mounts++;
    args.unshift(mount);
    return this.useif.apply(this, args);

    function mount(context, canvas, next) {
      var str = context.location.pathname;
      if (!str) return;
      if (path instanceof RegExp) {
        return path.test(str);
      } else {
        return (str.search(path) === 0);
      }
    }
  };

  /**
   * This invokes a kawapp application.
   *
   * @param {Object} [context] - request context object a.k.a. `locals`
   * @param {response|jQuery|cheerio} [canvas] - response element such as jQuery object
   * @param {Function} [callback] - callback function
   * @returns {kawapp}
   * @example
   * var app = kawapp();
   * app.use(mw1);              // install some middlewares
   *
   * var context = {};          // plain object as a request context
   * var canvas = $("#canvas"); // jQuery object as a response canvas
   *
   * app.start(context, canvas, function(err, canvas) {
   *   if (err) console.error(err);
   * });
   */
  kawapp.prototype.start = function(context, canvas, callback) {
    // both request and response are optional
    if (arguments.length == 1 && "function" === typeof context) {
      callback = context;
      context = null;
    } else if (arguments.length == 2 && "function" === typeof canvas) {
      callback = canvas;
      canvas = null;
    }

    // default parameteres
    if (!context) context = this.context || kawapp.request();
    if (!canvas) canvas = this.canvas || kawapp.response();

    // compile kawapp as a middleware and run it
    var array = Array.prototype.slice.call(this);
    var mw = kawapp.mw.merge.apply(null, array);
    mw(context, canvas, end);
    return this;

    function end(err) {
      if (err === kawapp.END) err = null;
      if (callback) callback(err, canvas);
    }
  };
})(kawapp);

/**
 * Utility functions.
 * This provides the following function but no constructor.
 * @class kawapp.util
 */

(function(kawapp) {
  var util = kawapp.util || (kawapp.util = {});

  /**
   * Alias to `kawapp.util`.
   *
   * @member {kawapp.util} kawapp.prototype.util
   */
  kawapp.prototype.util = util;

  /**
   * This parses query parameters.
   *
   * @method kawapp.util.parseParam
   * @see https://gist.github.com/kawanet/8384773
   * @param {String} query string
   * @returns {Object} parameter parsed
   * @example
   * // parse query parameters after "?"
   * var param1 = kawapp.util.parseParam(location.search.substr(1));
   *
   * // parse query parameters after "#!" hash bang
   * if (location.hash.search(/^#!.*\?/) > -1) {
   *   var param2 = kawapp.util.parseParam(location.hash.replace(/^#!.*\?/, ""))
   * }
   */

  util.parseParam = function(query) {
    var vars = query.split(/[&;]/);
    var param = {};
    for (var i = 0; i < vars.length; i++) {
      var pair = vars[i];
      if (!pair.length) continue;
      var pos = pair.indexOf("=");
      var key, val;
      if (pos > -1) {
        key = pair.substring(0, pos);
        val = pair.substring(pos + 1);
      } else {
        key = val = pair;
      }
      key = key.replace(/\+/g, " ");
      val = val.replace(/\+/g, " ");
      key = decodeURIComponent(key);
      val = decodeURIComponent(val);
      param[key] = val;
    }
    return param;
  };
})(kawapp);

/**
 * This provides following functions which return middlewares.
 * No constructor.
 * @class kawapp.mw
 */

(function(kawapp) {
  var mw = kawapp.mw || (kawapp.mw = {});
  /**
   * Alias to `kawapp.mw`.
   *
   * @member {kawapp.mw} kawapp.prototype.mw
   */
  kawapp.prototype.mw = mw;

  /**
   * This returns a single middleware combined
   * with multiple middlewares (or kawapp applications).
   *
   * @method kawapp.mw.merge
   * @param {...Function} mw - middlewars or applications
   * @returns {Function} middleware merged
   * @example
   * var app = kawapp();
   *
   * app.use(kawapp.mw.merge(mw1, mw2, mw3));
   */
  mw.merge = function(mw) {
    var args = arguments;
    return merge;

    function merge(context, canvas, next) {
      var idx = 0;
      iterator();

      function iterator(err) {
        if (err || idx >= args.length) {
          if (err === kawapp.SKIP) err = null;
          next(err);
          return;
        }
        mw = args[idx++];
        if (mw instanceof kawapp) {
          var array = Array.prototype.slice.call(mw);
          mw = kawapp.mw.merge.apply(null, array);
        }
        mw(context, canvas, iterator);
      }
    }
  };

  /**
   * This returns a middleware to set `location` object.
   * This would be great when running kawapp not on a browser environment.
   *
   * @method kawapp.mw.location
   * @param {Object} [defaults] - default location object
   * @returns {Function} middleware
   * @example
   * var app = kawapp();
   *
   * var loc = {
   *   href: "http://www.example.com/about"
   * };
   * app.use(kawapp.location(loc));     // store default location
   *
   * app.use(function(context, canvas, next) {
   *   console.log(context.location.href);  // fetch location in a middleware
   *   next();
   * });
   */
  mw.location = function(defaults) {
    /* global location */
    return _location;

    function _location(context, canvas, next) {
      if (!context.location) {
        context.location = ("undefined" !== typeof location) ? location : defaults || {};
      }
      next();
    }
  };

  /**
   * This returns a middleware to parse parameters at `location.search`.
   *
   * @method kawapp.mw.parseQuery
   * @param {String} [root] - root key to set parsed queries such as `"param"`
   * @param {String} [defaults] - default location.search string such as `"?key=value"`
   * @returns {Function} middleware
   * @example
   * var app = kawapp();
   *
   * app.use(kawapp.mw.parseQuery("param", "?key=value"));
   *
   * app.use(function(context, canvas, next) {
   *   console.log(context.param.key); // => "value"
   *   next();
   * });
   */
  mw.parseQuery = function(root, defaults) {
    return _parseQuery;

    function _parseQuery(context, canvas, next) {
      if (context.locationSearch) return next(); // already parsed
      kawapp.mw.location()(context, canvas, function(err) {
        if (err) return next(err);
        return parseQuery(context, canvas, next);
      });
    }

    function parseQuery(context, canvas, next) {
      if (root && !context[root]) context[root] = {};
      var param = root ? context[root] : context;
      var q = context.location.search || defaults;
      if (q && q.length > 1) {
        var p = context.locationSearch = kawapp.util.parseParam(q.substr(1));
        for (var key in p) {
          param[key] = p[key];
        }
      }
      next();
    }
  };

  /**
   * This returns a middleware to parse parameters at `location.hash`.
   *
   * @method kawapp.mw.parseHash
   * @param {String} [root] - root key to set parsed queries such as `"param"`
   * @param {String} [defaults] - default location.hash string such as `"#!?key=value"`
   * @returns {Function} middleware
   * @example
   * var app = kawapp();
   *
   * app.use(kawapp.parseHash("param", "#!?key=value"));
   *
   * app.use(function(context, canvas, next) {
   *   console.log(context.param.key); // => "value"
   *   next();
   * });
   */
  mw.parseHash = function(root, defaults) {
    return _parseHash;

    function _parseHash(context, canvas, next) {
      if (context.locationHash) return next(); // already parsed
      kawapp.mw.location()(context, canvas, function(err) {
        if (err) return next(err);
        return parseHash(context, canvas, next);
      });
    }

    function parseHash(context, canvas, next) {
      if (root && !context[root]) context[root] = {};
      var param = root ? context[root] : context;
      var q = context.location.hash || defaults;
      if (q && q.search(/^#!.*\?/) > -1) {
        var p = context.locationHash = kawapp.util.parseParam(q.replace(/^#!.*\?/, ""));
        for (var key in p) {
          param[key] = p[key];
        }
      }
      next();
    }
  };
})(kawapp);

/**
 * This is an alternative lightweight response class.
 * On node.js environment, use a jQuery or cheerio object instead.
 * On browser environment, use a jQuery object for most purpose.
 * This is a reference implementation to define a common interface for response objects.
 *
 * @class kawapp.response
 */
(function(kawapp) {
  kawapp.response = response;

  function response() {
    if (!(this instanceof response)) return new response();
    this[0] = [];
  }

  /**
   * This always returns 1.
   * Response object behaves Array-like object which has an item.
   *
   * @member {Number} kawapp.response.prototype.length
   * @example
   * var app = kawapp();
   *
   * app.use(some_mw);
   *
   * app.start(function(err, canvas) {
   *   $("#canvas").append(canvas[0]);
   * });
   */
  response.prototype.length = 1;

  /**
   * This flushes the current response element.
   *
   * @method kawapp.response.prototype.empty
   * @returns {response} response object for method chaining.
   * @example
   * var app = kawapp();
   *
   * app.use(function(context, canvas, next) {
   *   canvas.empty().append("hi, there!");
   * });   */
  response.prototype.empty = function() {
    this[0].length = 0;
    return this;
  };

  /**
   * This appends a block of HTML to the response element.
   *
   * @method kawapp.response.prototype.append
   * @param {...String} html - HTML to append
   * @returns {response} response object for method chaining.
   * @example
   * var app = kawapp();
   *
   * app.use(function(context, canvas, next) {
   *   canvas.append("foo");
   *   canvas.append("bar");
   * });
   */
  response.prototype.append = function() {
    var args = Array.prototype.slice.call(arguments);
    var list = this[0];
    args.forEach(function(item) {
      list.push(item);
    });
    return this;
  };

  /**
   * This replaces or retrieves HTML source of the response element.
   *
   * @method kawapp.response.prototype.html
   * @param {String} [html] - HTML to replace
   * @returns {String|response} HTML to retrieve, or response element for method chaining.
   * @example
   * var app = kawapp();
   *
   * app.use(function(context, canvas, next) {
   *   canvas.html("Hello!");
   * });
   *
   * app.start(function(err, canvas) {
   *   console.log(canvas.html());       // => "Hello!"
   * });
   */
  response.prototype.html = function(html) {
    if (arguments.length) {
      // update HTML contents
      this.empty().append(html);
      return this;
    } else {
      // retrieve HTML contents
      var array = this[0].map(function(elem) {
        if ("object" === typeof elem) {
          if (elem.cheerio) {
            // cheerio has a toString() method
            return elem;
          } else if (elem.jquery) {
            // jQuery does not have outerHTML feature
            var jQuery = elem.constructor;
            var wrap = new jQuery("<div/>");
            elem = elem.first();
            if (elem.parent().length) elem = elem.clone();
            var html = wrap.append(elem).html();
            return html;
          } else if (elem.hasOwnProperty("outerHTML")) {
            // elem is a HTMLElement
            return elem.outerHTML;
          }
        }
        return elem;
      });
      return array.join("");
    }
  };
})(kawapp);