Source: lib/response.js

/*------------------------------------*\
	RESPONSE
\*------------------------------------*/
/* jslint node: true */

/**
 * Get all information to prepare the response.
 * @private
 * @function prepareResponse
 * @memberOf NA#
 * @this NA
 * @param {string}      path     The url listening.
 * @param {Object}      options  Option associate to this url.
 * @param {Object}      request  Initial request.
 * @param {Object}      response Initial response.
 * @param {NA~callback} next     Next step after.
 */
exports.prepareResponse = function (path, options, request, response, next) {
	var NA = this,

		/**
		 * All parameters from a specific page.
		 * @namespace routeParameters
		 * @public
		 * @alias routeParameters
		 * @type {Object}
		 * @memberOf NA#locals
		 */
		routeParameters = options[path],

		/**
		 * All locals provided for Views Template Engine and Hooks.
		 * @namespace locals
		 * @public
		 * @alias locals
		 * @type {Object}
		 * @memberOf NA#
		 */
		locals = {

			/**
			 * Expose route of current page from current webconfig `routes`.
			 * @public
			 * @alias route
			 * @type {string}
			 * @memberOf NA#locals
			 * @example /categories/:category/
			 */
			route: path
		};

	/* path contain `{ view, controller, ... }` because `NA.webconfig.routes` is `[{ view, controller, ... }, ...]` */
	if (typeof path === "object") {
		routeParameters = path;
	}

	/* routeParameters contain `"index.htm"` because `NA.webconfig.routes["/"]` is `"index.htm"` not `{ view, controller, ... }` */
	if (typeof routeParameters === 'string') {
		routeParameters = {

			/**
			 * This is the file name of view used for render of page behind the route.
			 * @public
			 * @alias view
			 * @type {string}
			 * @memberOf NA#locals.routeParameters
			 */
			view: routeParameters
		};
	}

	/* Case of `routeParameters.url` replace `path` because `path` is used like a key. */
	if (routeParameters.url) {
		locals.route = routeParameters.url;
	}

	if (routeParameters.url && typeof path === "string") {

		/**
		 * Expose the key from `<currentRoute>` object from webconfig.
		 * @public
		 * @alias routeKey
		 * @type {Object}
		 * @memberOf NA#locals
		 */
		locals.routeKey = path;
	}
	if (routeParameters.key) {
		locals.routeKey = routeParameters.key;
	}

	/**
	 * Expose all data from `routes[<currentRoute>]` object from webconfig.
	 * @public
	 * @alias routeParameters
	 * @type {Object}
	 * @memberOf NA#locals
	 */
	locals.routeParameters = routeParameters;

	/**
	 * Expose all webconfig values.
	 * @public
	 * @alias webconfig
	 * @type {Object}
	 * @memberOf NA#locals
	 */
	locals.webconfig = NA.webconfig;

	/* Add preparation into response. */
	if (response) {
		response.locals = locals;
	}

	NA.prepareRenderLanguage(locals, request, response, next);
};

/**
 * Create some variable for manage path for render.
 * @private
 * @function prepareRenderLanguage
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.prepareRenderLanguage = function (locals, request, response, next) {
	var NA = this;

	/**
	 * Expose the current language code for the page if setted else expose the global if setted.
	 * @public
	 * @alias languageCode
	 * @type {string}
	 * @memberOf NA#locals
	 * @default undefined
	 */
	locals.languageCode =

		/**
		 * Represent the language code for this page.
		 * @public
		 * @alias languageCode
		 * @type {string}
		 * @memberOf NA#locals.routeParameters
		 * @default undefined
		 */
		locals.routeParameters.languageCode ||

		/**
		 * Represent the global and main language code for website.
		 * @public
		 * @alias languageCode
		 * @type {string}
		 * @memberOf NA#webconfig
		 * @default undefined.
		 */
		NA.webconfig.languageCode;

	/* Next preparation render for variation. */
	NA.prepareRenderPath(locals, request, response, next);
};

/**
 * Create some variable for manage path for render.
 * @private
 * @function prepareRenderPath
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.prepareRenderPath = function (locals, request, response, next) {
	var NA = this,
		path = NA.modules.path,
		url = NA.modules.url,
		query = (request  && request.originalUrl && request.originalUrl.split("?"));

	/**
	 * Idem as `NA#webconfig.urlRoot`.
	 * @public
	 * @alias urlRootPath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example http://localhost:7777
	 * https://www.example.here
	 */
	locals.urlRootPath = NA.webconfig.urlRoot;

	/**
	 * Idem as `NA#webconfig.urlRelativeSubPath`.
	 * @public
	 * @alias urlSubPath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example /subpath
	 */
	locals.urlSubPath = NA.webconfig.urlRelativeSubPath;

	/**
	 * Expose the current URL of page with `NA#webconfig.urlRoot` and `NA#webconfig.urlRelativeSubPath`.
	 * @public
	 * @alias urlBasePath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example http://localhost:7777/subpath
	 * https://www.example.here
	 */
	locals.urlBasePath = NA.webconfig.urlRoot + NA.webconfig.urlRelativeSubPath;

	/**
	 * Url from `url` value for current route.
	 * @public
	 * @alias urlFilePath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example /example.html
	 * /example/this/
	 */
	locals.urlFilePath = locals.routeParameters.output || locals.routeParameters.url;
	if (request) {
		locals.urlFilePath = url.format(path.join("/", request.url.replace(new RegExp("^/?" + locals.urlSubPath), "")));
	}

	/**
	 * Query from `url` value for current route.
	 * @public
	 * @alias urlQueryPath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example ?title=MachinisteWeb&description=ok
	 * ?title=MachinisteWeb
	 */
	locals.urlQueryPath = query && query[1] ? "?" + query[1] : "";

	/**
	 * Expose the current URL of page with `NA#webconfig.urlBasePath` and the current page route.
	 * @public
	 * @alias urlPath
	 * @type {string}
	 * @memberOf NA#locals
	 * @example http://localhost:7777/subpath/example.html?title=MachinisteWeb&description=ok
	 * https://www.example.here/example/this/?title=MachinisteWeb
	 */
	locals.urlPath = locals.urlBasePath + locals.route + locals.urlQueryPath;
	if (request) {
		locals.urlPath = "http" + ((NA.webconfig.httpSecure) ? "s" : "") + '://' + request.get("host") + request.originalUrl;
	}

	/* Next preparation render for variation. */
	NA.prepareRenderVariation(locals, request, response, next);
};

/**
 * Create some variable for manage variation into render.
 * @private
 * @function prepareRenderVariation
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.prepareRenderVariation = function (locals, request, response, next) {
	var NA = this,
		extend = NA.modules.extend,
		async = NA.modules.async;

	if (request) {

		/**
		 * Expose list of slug parameters used into URL.
		 * @public
		 * @alias params
		 * @type {string}
		 * @memberOf NA#locals
		 * @example If current route is '/example/:selector/'
		 * At http://localhost/example/test/ the value of `NA.locals#params` is
		 * { "selector": "test" }
		 */
		locals.params = request.params || {};

		/**
		 * Expose list of query parameters used into URL.
		 * @public
		 * @alias query
		 * @type {string}
		 * @memberOf NA#locals
		 * @example At http://localhost/example/?param=test the value of `NA.locals#query` is
		 * { "param": "test" }
		 */
		locals.query = request.query || {};

		/**
		 * Expose list of body parameters used into page.
		 * @public
		 * @alias body
		 * @type {string}
		 * @memberOf NA#locals
		 * @example If the Response body is `test=This+is+a+test` the value of `NA.locals#body` is
		 * { "test": "This is a test" }
		 */
		locals.body = request.body || {};
	}

	async.parallel([
		function (callback) {

			/**
			 * Name of file for `common` variation.
			 * @public
			 * @alias variation
			 * @type {string}
			 * @memberOf NA#webconfig
			 */
			locals.common = NA.openVariation(NA.webconfig.variation, locals.languageCode);
			if (locals.languageCode) {

				/**
				 * Expose all JSON data from `variation` file.
				 * @public
				 * @alias common
				 * @type {Object}
				 * @memberOf NA#locals
				 */
				locals.common = extend(true, NA.openVariation(NA.webconfig.variation, undefined, true), locals.common);
			}

			callback();
		},
		function (callback) {

			/**
			 * Name of file for `specific` variation.
			 * @public
			 * @alias variation
			 * @type {string}
			 * @memberOf NA#locals.routeParameters
			 */
			locals.specific = NA.openVariation(locals.routeParameters.variation, locals.languageCode);
			if (locals.languageCode) {

				/**
				 * Expose all JSON data from `routes[<currentRoute>].variation` file.
				 * @public
				 * @alias specific
				 * @type {Object}
				 * @memberOf NA#locals
				 */
				locals.specific = extend(true, NA.openVariation(locals.routeParameters.variation, undefined, true), locals.specific);
			}

			callback();
		}
	], function () {

		/* Nexts Step for render. */
		NA.prepareHeaders(locals, request, response, next);
	});
};

/**
 * Add and Remove headers from Webconfig.
 * @private
 * @function manageHeaders
 * @memberOf NA~
 * @param {NA}     NA       NodeAtlas instance.
 * @param {Object} headers  All headers from webconfig.
 * @param {Object} response All stuff for HTTP response.
 */
function manageHeaders(NA, headers, response) {
	var header;

	for (header in NA.webconfig.headers) {
		if (!NA.webconfig.headers.hasOwnProperty(header)) {
			continue;
		}
		if (NA.webconfig.headers[header] === false) {
			response.removeHeader(header);
		} else {
			response.setHeader(header, NA.webconfig.headers[header]);
		}
	}
	for (header in headers) {
		if (!headers.hasOwnProperty(header)) {
			continue;
		}
		if (headers[header] === false) {
			response.removeHeader(header);
		} else {
			response.setHeader(header, headers[header]);
		}
	}
}

/**
 * Set all webconfig headers.
 * @private
 * @function prepareHeaders
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.prepareHeaders = function (locals, request, response, next) {
	var NA = this,

		/**
		 * Charset used for render of this page.
		 * @public
		 * @alias charset
		 * @type {string}
		 * @memberOf NA#locals.routeParameters
		 * @default "utf-8"
		 */
		charset = locals.routeParameters.charset || NA.webconfig.charset,

		/**
		 * Content Type used for respond with this page.
		 * @public
		 * @alias mimeType
		 * @type {string}
		 * @memberOf NA#locals.routeParameters
		 * @default "text/html"
		 */
		mimeType = locals.routeParameters.mimeType || NA.webconfig.mimeType,

		/**
		 * Status Code used for respond with this page.
		 * @public
		 * @alias statusCode
		 * @type {number}
		 * @memberOf NA#locals.routeParameters
		 * @default 200
		 */
		statusCode = locals.routeParameters.statusCode || 200,

		/**
		 * Headers value used for respond with this page.
		 * @public
		 * @alias mimeType
		 * @type {string}
		 * @memberOf NA#locals.routeParameters
		 * @default "text/html"
		 */
		headers = locals.routeParameters.headers || {};

	if (response) {
		/* Set charset and  */
		response.statusCode = statusCode;

		/* Set headers into response */
		response.setHeader("Content-Type", mimeType + ";" + " charset=" + charset);
		manageHeaders(NA, headers, response);
	}

	/* Nexts Step for render. */
	next(locals, request, response);
};

/**
 * Intercept Variation from common file.
 * @private
 * @function changeVariationsCommon
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.changeVariationsCommon = function (locals, request, response, next) {
	var NA = this;

	/* Loading the controller file if `routeParameters.controller` exist. */
	NA.openController(

		/**
		 * This is the file name of specific controller used for back-end part of this page.
		 * @public
		 * @alias controller
		 * @type {string}
		 * @memberOf NA#locals.routeParameters
		 */
		locals.routeParameters.controller);

	/* Use the `NA.controllers[<controller>].changeVariations(...)` function if set... */
	if (typeof NA.controllers[NA.webconfig.controller] !== 'undefined' &&
		typeof NA.controllers[NA.webconfig.controller].changeVariations !== 'undefined') {

			/**
			 * Define this function for intercept Variation object and modify it. Both `common` and `specific` controller.
			 * @function changeVariations
			 * @memberOf NA#controllers[]
			 * @param {changeVariations~callback} callback   Next steps after configuration is done.
			 * @param {Object}                    locals     Local variables object of current page.
			 * @param {Object}                    response   Initial response.
			 * @param {Object}                    request    Initial request.
			 */
			NA.controllers[NA.webconfig.controller].changeVariations.call(NA,

			/**
			 * Next steps after changeVariations is done.
			 * @callback changeVariations~callback
			 */
			function () {
				NA.changeVariationsSpecific(locals, request, response, next);
			}, locals, request, response);
	/* ...else, just continue. */
	} else {
		NA.changeVariationsSpecific(locals, request, response, next);
	}
};

/**
 * Intercept Variation from specific file.
 * @private
 * @function changeVariationsSpecific
 * @memberOf NA#
 * @this NA
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.changeVariationsSpecific = function (locals, request, response, next) {
	var NA = this;

	if (typeof NA.controllers[locals.routeParameters.controller] !== 'undefined' &&
		typeof NA.controllers[locals.routeParameters.controller].changeVariations !== 'undefined') {
			/* Use the `NA.controllers[<controller>].changeVariations(...)` function if set... */
			NA.controllers[locals.routeParameters.controller].changeVariations.call(NA, function () {
				NA.changeDomCommon(locals, request, response, next);
			}, locals, request, response);
	} else {
		/* ...else, just continue. */
		NA.changeDomCommon(locals, request, response, next);
	}
};

/**
 * Prepare the choosen engine to parse view.
 * @private
 * @function prepareEngineProcess
 * @memberOf NA~
 * @param {NA}          NA       NodeAtlas instance.
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
function prepareEngineProcess(NA, locals, response, next) {
  var ejs = NA.modules.ejs,
		pug = NA.modules.pug,
		pathM = NA.modules.path,
		engine = NA.webconfig.pug ? pug : ejs,
		path = NA.modules.path,
		view = path.join(NA.serverPath, NA.webconfig.viewsRelativePath, (

			/**
			 * Name of file for `common` view.
			 * @public
			 * @alias view
			 * @type {string}
			 * @memberOf NA#webconfig
			 */
			NA.webconfig.view) ? NA.webconfig.view : (locals.routeParameters.view || ""));

	if (typeof locals.routeParameters.pug === "boolean") {

		/**
		 * Allow you to enable Pug only for a page.
		 * @public
		 * @alias pug
		 * @type {boolean}
		 * @memberOf NA#locals.routeParameters
		 * @default undefined
		 */
		engine = locals.routeParameters.pug ? pug : ejs;
	}

	/* Without view, no data. */
	if (!locals.routeParameters.view) {
		return next("");
	}

	/**
	 * Allow template engine know which file is currently in use.
	 * @public
	 * @alias filename
	 * @type {string}
	 * @memberOf NA#locals
	 */
	locals.filename = pathM.join(NA.serverPath, NA.webconfig.viewsRelativePath, NA.webconfig.view || locals.routeParameters.view);

	engineProcess(NA, view, engine, locals, response, next);
}

/**
 * Choose an engine to parse view.
 * @private
 * @function engineProcess
 * @memberOf NA~
 * @param {NA}          NA       NodeAtlas instance.
 * @param {string}      view     View to parse.
 * @param {string}      engine   Engine to parse.
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
function engineProcess(NA, view, engine, locals, response, next) {
	if (NA.webconfig.engine) {

		/* Transform from any engine but globaly. */
		response.render(view, locals, function (err, data) {
			if (err) {
				data = err.toString();
			}

			next(data);
		});
	} else {
		/* Open the template file */
		NA.openView(locals.routeParameters, view, function (data) {

			/* Transform ejs/pug data and inject incduded file. */
			try {
				data = engine.render(data, locals);
			} catch (err) {
				/* Make error more readable. */
				data = err.toString()
					.replace(/</g, "&lt;")
					.replace(/[\n]/g, "<br>")
					.replace(/\t/g, "<span style='display:inline-block;width:32px'></span>")
					.replace(/    /g, "<span style='display:inline-block;width:32px'></span>")
					.replace(/   /g, "<span style='display:inline-block;width:32px'></span>")
					.replace(/  /g, "<span style='display:inline-block;width:32px'></span>")
					.replace(/ >> /g, "<span style='display:inline-block;width:32px'>&gt;&gt;</span>")
					.replace(/> ([0-9])+\|/g, "<span style='display:inline-block;margin-left:-13px'>> $1|</span>")
					.replace(/^([a-zA-Z]+):/g, "$1:<br><br>");
			}

			next(data);
		});
	}
}

/**
 * Intercept DOM from common file.
 * @private
 * @function changeDomCommon
 * @memberOf NA#
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.changeDomCommon = function (locals, request, response, next) {
	var NA = this;

		// Transform into HTML
		prepareEngineProcess(NA, locals, response, function (data) {

			/**
			 * The compiled HTML of view + locals provided by response.
			 * @public
			 * @alias dom
			 * @type {string}
			 * @memberOf NA#locals
			 */
			locals.dom = data;

			/* Use the `NA.controllers[<controller>].changeDom(...)` function if set... */
			if (typeof NA.controllers[NA.webconfig.controller] !== 'undefined' &&
				typeof NA.controllers[NA.webconfig.controller].changeDom !== 'undefined') {

					/**
					 * Generate a virtual DOM to use jQuery on it.
					 * @function virtualDom
					 * @memberOf NA#locals
					 * @returns {Object} The $ object to manipulate the virtual DOM.
					 */
					locals.virtualDom = function () {
						var jsdom = NA.modules.jsdom;
						return new jsdom.JSDOM(data);
					};

					/**
					 * Define this function for intercept DOM and modify it with jQuery for example. Both `common` and `specific` controller.
					 * @function changeDom
					 * @memberOf NA#controllers[]
					 * @param {changeDom~callback} callback   Next steps after configuration is done.
					 * @param {Object}             locals     Local variables for the current page.
					 * @param {string}             locals.dom DOM of current page.
					 * @param {Object}             response   Initial response.
					 * @param {Object}             request    Initial request.
					 */
					NA.controllers[NA.webconfig.controller].changeDom.call(NA,

					/**
					 * Next steps after changeDomSpecific is done.
					 * @callback changeDomSpecific~callback
					 * @param {Object} dom DOM with modifications.
					 */
					function (dom) {
						if (typeof dom === "object") {
							locals.dom = dom.serialize();
						}
						NA.changeDomSpecific(locals, request, response, next);
					}, locals, request, response);
			/* ...else, just continue. */
			} else {
				NA.changeDomSpecific(locals, request, response, next);
			}
		});
};

/**
 * Intercept DOM from specific file.
 * @private
 * @function changeDomSpecific
 * @memberOf NA#
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.changeDomSpecific = function (locals, request, response, next) {
	var NA = this;

	if (typeof NA.controllers[locals.routeParameters.controller] !== 'undefined' &&
		typeof NA.controllers[locals.routeParameters.controller].changeDom !== 'undefined') {

			locals.virtualDom = function () {
				var jsdom = NA.modules.jsdom;
				return new jsdom.JSDOM(data);
			};

			/** Use the `NA.controllers[<controller>].changeVariations(...)` function if set... */
			NA.controllers[locals.routeParameters.controller].changeDom.call(NA, function (dom) {
				if (typeof dom === "object") {
					locals.dom = dom.serialize();
				}
				NA.intoBrowserAndFiles(locals, request, response, next);
			}, locals, request, response);
	} else {
		/** ...else, just continue. */
		NA.intoBrowserAndFiles(locals, request, response, next);
	}
};

/**
 * Inject CSS into DOM if needed.
 * @private
 * @function intoBrowserAndFiles
 * @memberOf NA#
 * @param {Object}      locals   Local variables for the current page.
 * @param {Object}      request  Information from request.
 * @param {Object}      response Information from response.
 * @param {NA~callback} next     Next step after.
 */
exports.intoBrowserAndFiles = function (locals, request, response, next) {
	var NA = this;

	/* Inject CSS into DOM... */
	if (NA.webconfig.injectCss || locals.routeParameters.injectCss) {
		NA.injectCss(locals.dom, locals.routeParameters.injectCss, function (dom) {
			NA.renderTemplate(dom, locals, request, response, next);
		});
	/* ...or do nothing. */
	} else {
		NA.renderTemplate(locals.dom, locals, request, response, next);
	}
};

/**
 * Write file or/and send response.
 * @private
 * @function renderTemplate
 * @memberOf NA#
 * @param {string}      data            HTML DOM ready for sending.
 * @param {Object}      locals          Local variables for the current page.
 * @param {Object}      request         Information from request.
 * @param {Object}      response        Information from response.
 * @param {NA~callback} next            Next step after.
 */
exports.renderTemplate = function (data, locals, request, response, next) {
	var NA = this,
		async = NA.modules.async,

		/**
		 * Allow NodeAtlas to generate real file into `NA#webconfig.serverlessRelativePath` directory if set to true.
		 * @public
		 * @alias htmlGenerationBeforeResponse
		 * @type {boolean}
		 * @memberOf NA#webconfig
		 * @default false
		 */
		htmlGenerationBeforeResponse = NA.webconfig.htmlGenerationBeforeResponse,
		output = (typeof NA.webconfig.output === 'boolean') ? NA.webconfig.output : true,
		templateRenderName;

	/* Create the file for asset mode */
	if (typeof response === "undefined" || (htmlGenerationBeforeResponse && output)) {

		/**
		 * Output name of file generate if `NA#webconfig.htmlGenerationBeforeResponse` is set to true or if `--generate` command is used.
		 * If value is set to `false`, no generate page will be generated.
		 * @public
		 * @alias output
		 * @type {string|boolean}
		 * @memberOf NA#locals.routeParameters
		 */
		templateRenderName = locals.route;

		if (typeof locals.routeParameters.output !== 'undefined') {
			templateRenderName = locals.routeParameters.output;
		}

		NA.saveRender(data, templateRenderName);
	}

	/* Run page into browser. */
	if (typeof response !== "undefined") {
		/* Compression of CSS, JS and Images if required. */
		async.parallel([
			NA.cssCompilation.bind(NA),
			NA.jsObfuscation.bind(NA)
		], function () {
			NA.sendResponse(request, response, data, next);
		});
	}
};