/*! 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("
"); 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);