Comparing version 0.0.23 to 0.1.0
@@ -0,1 +1,5 @@ | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
module.exports = require('./lib/citizen'); |
// Initializes the framework | ||
module.exports = function (appConfig) { | ||
var helper = require('./helper')(), | ||
defaultConfig = { | ||
mode: 'production', | ||
directories: { | ||
app: '/', | ||
patterns: '/patterns', | ||
public: '/public' | ||
}, | ||
urlPaths: { | ||
app: '/' | ||
}, | ||
httpPort: 80, | ||
sessions: false, | ||
sessionLength: 1200000, // 20 minutes | ||
staticAssetUrl: '' | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
var fs = require('fs'), | ||
handlebars = require('handlebars'), | ||
jade = require('jade'), | ||
helper = require('./helper'), | ||
os = require('os'), | ||
path = require('path'), | ||
util = require('util'), | ||
defaultConfig = { | ||
mode: 'production', | ||
directories: { | ||
app: process.cwd(), | ||
logs: process.cwd() + '/logs', | ||
on: process.cwd() + '/on', | ||
controllers: process.cwd() + '/patterns/controllers', | ||
models: process.cwd() + '/patterns/models', | ||
views: process.cwd() + '/patterns/views', | ||
public: path.resolve(process.cwd(), '../public') | ||
}, | ||
urlPaths: { | ||
app: '/', | ||
fileNotFound: '/index.html' | ||
}, | ||
httpPort: 80, | ||
logs: { | ||
console: true, | ||
file: false | ||
}, | ||
sessions: false, | ||
sessionTimeout: 1200000, // 20 minutes | ||
requestTimeout: 30000, // 30 seconds | ||
mimetypes: JSON.parse(fs.readFileSync(path.join(__dirname, '../config/mimetypes.json'))), | ||
debug: { | ||
output: 'console', | ||
depth: 2 | ||
} | ||
}, | ||
config = getConfig(), | ||
finalConfig = helper.extend(defaultConfig, config.citizen), | ||
on = { | ||
application: { | ||
start: function (emitter) { | ||
// TODO: Log handler | ||
// helper.log({ | ||
// modes: 'debug', | ||
// log: 'citizen application start fired' | ||
// }); | ||
emitter.emit('ready'); | ||
}, | ||
config = helper.extend(defaultConfig, appConfig), | ||
helper = require('./helper')(config), | ||
patterns = helper.cachePatterns(), | ||
server = require('./server')(config, patterns), | ||
session = require('./session')(config); | ||
end: function (params, emitter) { | ||
emitter.emit('ready'); | ||
}, | ||
error: function (e, params, context, emitter) { | ||
if ( finalConfig.mode !== 'production' ) { | ||
console.log(e); | ||
console.trace(); | ||
} | ||
emitter.emit('ready'); | ||
} | ||
}, | ||
request: { | ||
start: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
}, | ||
end: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
} | ||
}, | ||
response: { | ||
start: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
}, | ||
end: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
} | ||
}, | ||
session: { | ||
start: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
}, | ||
end: function (params, context, emitter) { | ||
emitter.emit('ready'); | ||
} | ||
} | ||
}, | ||
appOn = getAppOn(), | ||
patterns = getPatterns(), | ||
server = require('./server'), | ||
session = require('./session'); | ||
CTZN = {}; | ||
config.citizen = finalConfig; | ||
if ( config.sessions ) { | ||
CTZN.sessions = {}; | ||
global.CTZN = { | ||
config: config, | ||
on: on, | ||
appOn: appOn, | ||
patterns: patterns, | ||
handlebars: handlebars, | ||
jade: jade | ||
}; | ||
if ( CTZN.config.citizen.sessions ) { | ||
CTZN.sessions = {}; | ||
} | ||
module.exports = { | ||
config: config, | ||
helper: helper, | ||
listen: helper.listen, | ||
handlebars: handlebars, | ||
jade: jade, | ||
on: on, | ||
patterns: patterns, | ||
start: server.start, | ||
session: session | ||
}; | ||
function getConfig() { | ||
var configDirectory = process.cwd() + '/config', | ||
files = [], | ||
config = {}; | ||
// If there isn't a config directory, return an empty config. | ||
// citizen will start under its default configuration. | ||
try { | ||
files = fs.readdirSync(configDirectory); | ||
} catch ( e ) { | ||
return config; | ||
} | ||
files.forEach( function (file, index, array) { | ||
var citizenConfig, | ||
citizenRegex = new RegExp(/^citizen[A-Za-z0-9_-]*\.json$/), | ||
appRegex = new RegExp(/^[A-Za-z0-9_-]*\.json$/), | ||
fileSafeName; | ||
if ( citizenRegex.test(file) ) { | ||
citizenConfig = JSON.parse(fs.readFileSync(configDirectory + '/' + file)); | ||
if ( citizenConfig.hostname && citizenConfig.hostname === os.hostname() ) { | ||
config.citizen = citizenConfig; | ||
} | ||
} else if ( appRegex.test(file) ) { | ||
fileSafeName = file.replace('/-/g', '_'); | ||
fileSafeName = fileSafeName.replace('.json', ''); | ||
config[fileSafeName] = JSON.parse(fs.readFileSync(configDirectory + '/' + file)); | ||
} | ||
}); | ||
return { | ||
config: config, | ||
helper: helper, | ||
events: { | ||
onApplicationStart: {}, | ||
onApplicationEnd: {}, | ||
onSessionStart: {}, | ||
onSessionEnd: {}, | ||
onRequestStart: {}, | ||
onRequestEnd: {}, | ||
onResponseStart: {}, | ||
onResponseEnd: {}, | ||
}, | ||
patterns: patterns, | ||
server: server, | ||
session: session | ||
}; | ||
}; | ||
if ( !config.citizen ) { | ||
try { | ||
config.citizen = JSON.parse(fs.readFileSync(configDirectory + '/citizen.json')); | ||
} catch ( e ) { | ||
// No big deal, citizen will start under the default configuration | ||
} | ||
} | ||
return config; | ||
} | ||
function getAppOn() { | ||
var on = {}, | ||
files = [], | ||
jsRegex = new RegExp(/.*\.js$/); | ||
// If there isn't an "on" directory, return an empty object | ||
try { | ||
files = fs.readdirSync(finalConfig.directories.on); | ||
} catch ( e ) { | ||
return on; | ||
} | ||
files.forEach( function (file, index, array) { | ||
var fileSafeName; | ||
if ( jsRegex.test(file) ) { | ||
fileSafeName = file.replace('.js', ''); | ||
on[fileSafeName] = require(finalConfig.directories.on + '/' + file); | ||
} | ||
}); | ||
return on; | ||
} | ||
function getPatterns() { | ||
var patterns = { | ||
controllers: {}, | ||
models: {}, | ||
views: {} | ||
}, | ||
controllers = [], | ||
models = [], | ||
views = [], | ||
jsRegex = new RegExp(/.+\.js$/), | ||
viewRegex = new RegExp(/.+\.(hbs|jade|html)$/); | ||
try { | ||
controllers = fs.readdirSync(finalConfig.directories.controllers); | ||
models = fs.readdirSync(finalConfig.directories.models); | ||
views = fs.readdirSync(finalConfig.directories.views); | ||
} catch ( e ) { | ||
getGroupedPatterns(); | ||
return; | ||
} | ||
controllers.forEach( function (file, index, array) { | ||
var fileSafeName; | ||
if ( jsRegex.test(file) ) { | ||
fileSafeName = file.replace('.js', ''); | ||
patterns.controllers[fileSafeName] = require(finalConfig.directories.controllers + '/' + file); | ||
} | ||
}); | ||
models.forEach( function (file, index, array) { | ||
var fileSafeName; | ||
if ( jsRegex.test(file) ) { | ||
fileSafeName = file.replace('.js', ''); | ||
patterns.models[fileSafeName] = require(finalConfig.directories.models + '/' + file); | ||
} | ||
}); | ||
views.forEach( function (directory, index, array) { | ||
var viewFiles; | ||
if ( fs.statSync(finalConfig.directories.views + '/' + directory).isDirectory() ) { | ||
viewFiles = fs.readdirSync(finalConfig.directories.views + '/' + directory); | ||
patterns.views[directory] = {}; | ||
viewFiles.forEach( function (file, index, array) { | ||
var fileExtension, | ||
viewName, | ||
viewContents; | ||
if ( viewRegex.test(file) ) { | ||
fileExtension = path.extname(file); | ||
viewName = path.basename(file, fileExtension); | ||
viewContents = fs.readFileSync(finalConfig.directories.views + '/' + directory + '/' + file, { 'encoding': 'utf8' }); | ||
switch ( fileExtension ) { | ||
case '.hbs': | ||
patterns.views[directory][viewName] = { | ||
engine: 'handlebars', | ||
raw: viewContents, | ||
compiled: handlebars.compile(viewContents) | ||
}; | ||
break; | ||
case '.jade': | ||
patterns.views[directory][viewName] = { | ||
engine: 'jade', | ||
raw: viewContents, | ||
compiled: jade.compile(viewContents) | ||
}; | ||
break; | ||
case '.html': | ||
patterns.views[directory][viewName] = { | ||
engine: 'html', | ||
raw: viewContents | ||
}; | ||
break; | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
return patterns; | ||
} | ||
function getGroupedPatterns() { | ||
var patterns = {}, | ||
patternFiles = fs.readdirSync(config.directories.patterns), | ||
patternName = '', | ||
patternFileName = '', | ||
viewContents = '', | ||
regex = new RegExp(/^([A-Za-z0-9-_])*$/); | ||
patternFiles.forEach( function (patternFileName, index, array) { | ||
if ( regex.test(patternFileName) ) { | ||
patternName = patternFileName.replace('/-/g', '_'); | ||
try { | ||
viewContents = fs.readFileSync(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '.html', { 'encoding': 'utf8' }); | ||
viewContents = viewContents.replace(/[\n|\t|\r]/g, ''); | ||
viewContents = viewContents.replace(/'/g, "\\'"); | ||
patterns[patternName] = { | ||
model: require(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '-model'), | ||
controller: require(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '-controller'), | ||
view: { | ||
raw: viewContents, | ||
compiled: handlebars.compile(viewContents) | ||
} | ||
}; | ||
} catch (e) { | ||
console.log(util.inspect(e)); | ||
throw e; | ||
} | ||
} | ||
}); | ||
return patterns; | ||
} |
// application event handlers | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
module.exports = function (config) { | ||
var methods = { | ||
var methods = { | ||
public: { | ||
public: { | ||
requestStart: function(methods) { | ||
requestStart: function(methods) { | ||
}, | ||
}, | ||
requestEnd: function(methods) { | ||
requestEnd: function(methods) { | ||
}, | ||
}, | ||
responseStart: function(methods) { | ||
responseStart: function(methods) { | ||
}, | ||
}, | ||
responseEnd: function(methods) { | ||
responseEnd: function(methods) { | ||
}, | ||
}, | ||
sessionStart: function(methods) { | ||
sessionStart: function(methods) { | ||
}, | ||
}, | ||
sessionEnd: function(methods) { | ||
sessionEnd: function(methods) { | ||
}, | ||
}, | ||
appStart: function(methods) { | ||
appStart: function(methods) { | ||
}, | ||
}, | ||
appEnd: function(methods) { | ||
appEnd: function(methods) { | ||
}, | ||
}, | ||
}, | ||
}, | ||
private: { | ||
private: { | ||
} | ||
} | ||
}; | ||
}; | ||
return methods.public; | ||
return methods.public; | ||
}; |
@@ -1,249 +0,173 @@ | ||
// core framework functions | ||
// core framework functions that might also be of use in the app | ||
module.exports = function (config) { | ||
var events = require('events'), | ||
fs = require('fs'), | ||
handlebars = require('handlebars'), | ||
util = require('util'), | ||
methods = { | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
public: { | ||
var events = require('events'), | ||
fs = require('fs'), | ||
util = require('util'); | ||
cachePatterns: function () { | ||
var patterns = {}, | ||
patternFiles = fs.readdirSync(config.directories.patterns), | ||
patternName = '', | ||
patternFileName = '', | ||
viewContents = '', | ||
regex = new RegExp(/^([A-Za-z0-9-_])*$/); | ||
module.exports = { | ||
copy: copy, | ||
extend: extend, | ||
isNumeric: isNumeric, | ||
listen: listen, | ||
// log: log | ||
}; | ||
patternFiles.forEach( function (patternFileName, index, array) { | ||
if ( regex.test(patternFileName) ) { | ||
patternName = patternFileName.replace('-', '_'); | ||
try { | ||
viewContents = fs.readFileSync(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '.html', { 'encoding': 'utf8' }); | ||
viewContents = viewContents.replace(/[\n|\t|\r]/g, ''); | ||
viewContents = viewContents.replace(/'/g, "\\'"); | ||
patterns[patternName] = { | ||
model: require(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '-model'), | ||
controller: require(config.directories.patterns + '/' + patternFileName + '/' + patternFileName + '-controller'), | ||
view: { | ||
raw: viewContents, | ||
compiled: handlebars.compile(viewContents) | ||
} | ||
}; | ||
} catch (e) { | ||
console.log(util.inspect(e)); | ||
throw e; | ||
} | ||
} | ||
}); | ||
// The copy() and getValue() functions were inspired by (meaning mostly stolen from) | ||
// Andrée Hanson: | ||
// http://andreehansson.se/ | ||
return patterns; | ||
}, | ||
function copy(object) { | ||
var objectCopy = {}; | ||
// The following copy(), extend(), and getValue() functions were inspired by (meaning mostly stolen from) | ||
// Andrée Hanson: | ||
// http://andreehansson.se/ | ||
for ( var property in object ) { | ||
if ( object.hasOwnProperty(property) ) { | ||
objectCopy[property] = getValue(object[property]); | ||
} | ||
} | ||
copy: function (object) { | ||
var objectCopy = {}; | ||
return objectCopy; | ||
} | ||
for ( var property in object ) { | ||
objectCopy[property] = methods.private.getValue(object[property]); | ||
} | ||
function extend(original, extension, copyObject) { | ||
var mergedObject, | ||
combinedArray = [], | ||
combinedArrayLength; | ||
return objectCopy; | ||
}, | ||
copyObject = typeof copyObject !== 'undefined' ? copyObject : true; | ||
extend: function (original, extension) { | ||
var mergedObject = methods.public.copy(original); | ||
if ( copyObject ) { | ||
mergedObject = copy(original); | ||
} else { | ||
mergedObject = original; | ||
} | ||
for ( var property in extension ) { | ||
mergedObject[property] = methods.private.getValue(extension[property]); | ||
} | ||
if ( !extension ) { | ||
return mergedObject; | ||
} | ||
return mergedObject; | ||
}, | ||
// for ( var property in extension ) { | ||
// if ( extension[property].constructor === Object && extension.hasOwnProperty(property) ) { | ||
// mergedObject[property] = extend(mergedObject[property], getValue(extension[property]), copyObject); | ||
// } else if ( extension[property].constructor === Array && mergedObject[property] && mergedObject[property].constructor === Array ) { | ||
// mergedObject[property].forEach( function (item, index, array) { | ||
// combinedArray[index] = getValue(item); | ||
// }); | ||
// combinedArrayLength = combinedArray.length; | ||
// extension[property].forEach( function (item, index, array) { | ||
// combinedArray[combinedArrayLength + index] = getValue(item); | ||
// }); | ||
// mergedObject = combinedArray; | ||
// } else { | ||
// mergedObject[property] = getValue(extension[property]); | ||
// } | ||
// } | ||
isNumeric: function (n) { | ||
return !isNaN(parseFloat(n)) && isFinite(n); | ||
}, | ||
for ( var property in extension ) { | ||
if ( extension[property].constructor === Object && extension.hasOwnProperty(property) ) { | ||
if ( extension.hasOwnProperty(property) ) { | ||
mergedObject[property] = extend(mergedObject[property], getValue(extension[property]), copyObject); | ||
} | ||
} else { | ||
mergedObject[property] = getValue(extension[property]); | ||
} | ||
} | ||
// TODO: create an optional timer to handle emitters that haven't been called | ||
listener: function (functions, callback) { | ||
var emitter = {}, | ||
output = {}, | ||
ready = {}, | ||
groupTracker = function () { | ||
var allReady = true; | ||
return mergedObject; | ||
} | ||
for ( var property in ready ) { | ||
if ( ready[property] === false ) { | ||
allReady = false; | ||
break; | ||
} | ||
} | ||
function getValue(object) { | ||
var isArray, | ||
isObject, | ||
val, | ||
i = 0, | ||
l; | ||
if ( allReady && typeof callback === 'function' ) { | ||
callback(output); | ||
} | ||
}; | ||
if ( typeof object !== 'undefined' ) { | ||
isArray = object.constructor.toString().indexOf('Array') >= 0; | ||
isObject = object.constructor.toString().indexOf('Object') >= 0; | ||
} else { | ||
object = 'undefined'; | ||
} | ||
for ( var property in functions ) { | ||
ready[property] = false; | ||
} | ||
if ( isArray ) { | ||
val = Array.prototype.slice.apply(object); | ||
l = val.length; | ||
for ( property in functions ) { | ||
emitter = new events.EventEmitter(); | ||
emitter.name = property; | ||
emitter.on('ready', function (result) { | ||
ready[this.name] = true; | ||
output[this.name] = result; | ||
groupTracker(); | ||
}); | ||
try { | ||
functions[property](emitter); | ||
} catch ( err ) { | ||
throw err; | ||
} | ||
do { | ||
val[i] = getValue(val[i]); | ||
} while (++i < l); | ||
} else if ( isObject ) { | ||
val = copy(object); | ||
} else { | ||
val = object; | ||
} | ||
} | ||
}, | ||
return val; | ||
} | ||
on: function (event, methods) { | ||
methods.extend(events[event], methods); | ||
}, | ||
function isNumeric(n) { | ||
return !isNaN(parseFloat(n)) && isFinite(n); | ||
} | ||
renderView: function (view, format, context) { | ||
var viewName = view.replace(/-/, '_'), | ||
viewOutput = ''; | ||
function listen(functions, callback) { | ||
var emitter = {}, | ||
output = { | ||
listener: { | ||
success: false | ||
} | ||
}, | ||
ready = {}, | ||
groupTracker = function () { | ||
var allReady = true; | ||
switch ( format ) { | ||
case 'html': | ||
switch ( config.mode ) { | ||
case 'production': | ||
viewOutput = app.patterns[viewName].view.compiled(context); | ||
break; | ||
case 'debug': | ||
case 'development': | ||
viewOutput = fs.readFileSync(config.directories.patterns + '/' + view + '/' + view + '.html', { 'encoding': 'utf8' }); | ||
viewOutput = handlebars.compile(viewOutput); | ||
viewOutput = viewOutput(context); | ||
if ( context.debugOutput ) { | ||
viewOutput = viewOutput.replace('</body>', '<div id="citizen-debug"><pre>' + context.debugOutput + '</pre></div></body>'); | ||
} | ||
break; | ||
} | ||
break; | ||
case 'json': | ||
viewOutput = JSON.stringify(context.content); | ||
break; | ||
} | ||
for ( var property in ready ) { | ||
if ( ready[property] === false ) { | ||
allReady = false; | ||
break; | ||
} | ||
} | ||
return viewOutput; | ||
}, | ||
if ( allReady && typeof callback === 'function' ) { | ||
output.listener.success = true; | ||
callback(output); | ||
} | ||
}; | ||
parseCookie: function (cookie) { | ||
var pairs = [], | ||
pair = [], | ||
cookies = {}; | ||
if ( Object.getOwnPropertyNames(functions).length > 0 ) { | ||
for ( var property in functions ) { | ||
ready[property] = false; | ||
} | ||
for ( var property in functions ) { | ||
emitter = new events.EventEmitter(); | ||
emitter.name = property; | ||
emitter.timer = setTimeout( function () { | ||
emitter.emit('timeout'); | ||
}, CTZN.config.citizen.requestTimeout); | ||
emitter.on('ready', function (result) { | ||
clearTimeout(this.timer); | ||
ready[this.name] = true; | ||
output[this.name] = result || ''; | ||
groupTracker(); | ||
}); | ||
emitter.on('timeout', function () { | ||
if ( typeof callback === 'function' ) { | ||
output.listener.message = this.name + ' has timed out.'; | ||
output.listener.completed = ready; | ||
callback(output); | ||
} | ||
throw 'Error in citizen helper.js: ' + this.name + ' has timed out.'; | ||
}); | ||
functions[property](emitter); | ||
} | ||
} else { | ||
throw 'Error in citizen helper.js: listener() requires at least one function'; | ||
} | ||
} | ||
if ( cookie ) { | ||
pairs = cookie.split(';'); | ||
for ( var i = 0; i < pairs.length; i += 1 ) { | ||
pair = pairs[i].trim(); | ||
pair = pair.split('='); | ||
cookies[pair[0]] = pair[1]; | ||
} | ||
} | ||
return cookies; | ||
}, | ||
toLocalTime: function (time, timeZoneOffset) { | ||
} | ||
// <cffunction name="toLocalTime" access="public" returntype="string" output="no" hint="Converts a time value to local time based on the provided time zone offset. Assumes the provided time is UTC."> | ||
// <cfargument name="time" type="date" required="true" /> | ||
// <cfargument name="timeZoneOffset" type="numeric" required="true" /> | ||
// <cfset var itemLocalTime = dateAdd("h", fix(arguments.timeZoneOffset), arguments.time)> | ||
// <cfif find(".5", arguments.timeZoneOffset)> | ||
// <cfif arguments.timeZoneOffset gte 0> | ||
// <cfset itemLocalTime = dateAdd("n", 30, arguments.time)> | ||
// <cfelse> | ||
// <cfset itemLocalTime = dateAdd("n", -30, arguments.time)> | ||
// </cfif> | ||
// </cfif> | ||
// <cfreturn itemLocalTime /> | ||
// </cffunction> | ||
}, | ||
private: { | ||
getValue: function (obj) { | ||
var isArray, | ||
isObject, | ||
val, | ||
i = 0, | ||
l; | ||
if ( typeof obj !== 'undefined' ) { | ||
isArray = obj.constructor.toString().indexOf('Array') >= 0, | ||
isObject = obj.constructor.toString().indexOf('Object') >= 0; | ||
} else { | ||
obj = 'undefined'; | ||
} | ||
if ( isArray ) { | ||
val = Array.prototype.slice.apply(obj); | ||
l = val.length; | ||
do { | ||
val[i] = methods.private.getValue(val[i]); | ||
} while (++i < l); | ||
} else if ( isObject ) { | ||
val = methods.public.copy(obj); | ||
} else { | ||
val = obj; | ||
} | ||
return val; | ||
} | ||
} | ||
}; | ||
// handlebars.registerHelper('ifCond', function (v1, operator, v2, options) { | ||
// | ||
// switch (operator) { | ||
// case '==': | ||
// return (v1 == v2) ? options.fn(this) : options.inverse(this); | ||
// case '===': | ||
// return (v1 === v2) ? options.fn(this) : options.inverse(this); | ||
// case '!=': | ||
// return (v1 != v2) ? options.fn(this) : options.inverse(this); | ||
// case '!==': | ||
// return (v1 !== v2) ? options.fn(this) : options.inverse(this); | ||
// case '<': | ||
// return (v1 < v2) ? options.fn(this) : options.inverse(this); | ||
// case '<=': | ||
// return (v1 <= v2) ? options.fn(this) : options.inverse(this); | ||
// case '>': | ||
// return (v1 > v2) ? options.fn(this) : options.inverse(this); | ||
// case '>=': | ||
// return (v1 >= v2) ? options.fn(this) : options.inverse(this); | ||
// case '&&': | ||
// return (v1 && v2) ? options.fn(this) : options.inverse(this); | ||
// case '||': | ||
// return (v1 || v2) ? options.fn(this) : options.inverse(this); | ||
// default: | ||
// return options.inverse(this); | ||
// } | ||
// }); | ||
return methods.public; | ||
}; | ||
// function log(content) { | ||
// | ||
// }; |
// router | ||
module.exports = function (config) { | ||
var url = require('url'), | ||
helper = require('./helper')(config), | ||
methods = { | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
public: { | ||
var url = require('url'); | ||
getRoute: function (urlToParse) { | ||
var pathToParse = url.parse(urlToParse).pathname, | ||
nameRegex = /^\/([A-Za-z0-9-_]+)\/?.*/, | ||
staticRegex = /^\/.*\.(avi|css|doc|docx|eot|gif|htm|html|ico|jpeg|jpg|js|mkv|mov|mp3|mp4|pdf|png|svg|ttf|txt|wmv|woff|zip).*/, | ||
route = { | ||
name: 'index', | ||
safeName: 'index', | ||
action: 'default', | ||
type: 'default', | ||
do: 'default', | ||
show: 'default', | ||
format: 'html', | ||
isStatic: false | ||
}; | ||
if ( staticRegex.test(pathToParse) ) { | ||
route = { | ||
name: url.parse(urlToParse).pathname, | ||
isStatic: true | ||
}; | ||
} else { | ||
if ( nameRegex.test(pathToParse) ) { | ||
route.name = pathToParse.replace(/^\/([A-Za-z0-9-_]+)\/?.*/, '$1'); | ||
route.safeName = route.name.replace('-', '_'); | ||
} | ||
} | ||
return route; | ||
}, | ||
module.exports = { | ||
getRoute: getRoute, | ||
getUrlParams: getUrlParams | ||
}; | ||
getUrlParams: function (urlToParse) { | ||
var pathToParse = url.parse(urlToParse).pathname, | ||
regex = /\/[A-Za-z_]+[A-Za-z0-9_]*\/[A-Za-z0-9-_\.]+\/?$/, | ||
parameterNames = [], | ||
parameterValues = [], | ||
assignment = '', | ||
urlParams = {}; | ||
function getRoute(urlToParse) { | ||
var pathToParse = url.parse(urlToParse).pathname, | ||
nameRegex = /^\/([A-Za-z0-9-_]+)\/?.*/, | ||
staticRegex = /^\/.*\.([A-Za-z0-9]+)$/, | ||
route = { | ||
name: 'index', | ||
safeName: 'index', | ||
renderer: 'index', | ||
view: 'index', | ||
renderedView: 'index', | ||
action: 'default', | ||
type: 'default', | ||
do: 'default', | ||
show: 'default', | ||
format: 'html', | ||
isStatic: false | ||
}; | ||
if ( regex.test(pathToParse) ) { | ||
while ( pathToParse.search(/\/[A-Za-z_]+[A-Za-z0-9_]*\/[A-Za-z0-9-_\.]+\/?$/) > 0 ) { | ||
parameterNames.unshift(pathToParse.replace(/.*\/([A-Za-z_]+[A-Za-z0-9_]*)\/[A-Za-z0-9-_\.]+\/?$/, '$1')); | ||
parameterValues.unshift(pathToParse.replace(/.*\/[A-Za-z_]+[A-Za-z0-9_]*\/([A-Za-z0-9-_\.]+)\/?$/, '$1')); | ||
pathToParse = pathToParse.replace(/(.+\/)[A-Za-z_]+[A-Za-z0-9_]*\/[A-Za-z0-9-_\.]+\/?$/, '$1'); | ||
} | ||
for ( var i = 0; i <= parameterNames.length-1; i+=1 ) { | ||
urlParams[parameterNames[i]] = parameterValues[i]; | ||
} | ||
} | ||
return urlParams; | ||
} | ||
}, | ||
if ( CTZN.config.citizen.mimetypes[pathToParse.replace(staticRegex, '$1')] ) { | ||
route = { | ||
name: url.parse(urlToParse).pathname, | ||
extension: pathToParse.replace(staticRegex, '$1'), | ||
isStatic: true | ||
}; | ||
} else { | ||
if ( nameRegex.test(pathToParse) ) { | ||
route.name = pathToParse.replace(/^\/([A-Za-z0-9-_]+)\/?.*/, '$1'); | ||
// route.safeName = route.name.replace('/-/g', '_'); | ||
route.renderer = route.name; | ||
route.view = route.name; | ||
route.renderedView = route.name; | ||
} | ||
} | ||
private: { | ||
return route; | ||
} | ||
} | ||
}; | ||
function getUrlParams(urlToParse) { | ||
var pathToParse = url.parse(urlToParse).pathname, | ||
paramsRegex = /\/[A-Za-z_]+[A-Za-z0-9_]*\/[^\/]+\/?$/, | ||
descriptorRegex = /^\/[A-Za-z_]+[A-Za-z0-9_]*\/[^\/]+\/?.*/, | ||
parameterNames = [], | ||
parameterValues = [], | ||
urlParams = {}; | ||
return methods.public; | ||
}; | ||
if ( paramsRegex.test(pathToParse) ) { | ||
while ( pathToParse.search(paramsRegex) > 0 ) { | ||
parameterNames.unshift(pathToParse.replace(/.*\/([A-Za-z_]+[A-Za-z0-9_]*)\/[^\/]+\/?$/, '$1')); | ||
parameterValues.unshift(pathToParse.replace(/.*\/[A-Za-z_]+[A-Za-z0-9_]*\/([^\/]+)\/?$/, '$1')); | ||
pathToParse = pathToParse.replace(/(.+)\/[A-Za-z_]+[A-Za-z0-9_]*\/[^\/]+\/?$/, '$1'); | ||
} | ||
parameterNames.forEach( function (name, index, array) { | ||
urlParams[name] = parameterValues[index]; | ||
}); | ||
} | ||
if ( descriptorRegex.test(pathToParse) ) { | ||
urlParams.descriptor = pathToParse.replace(/^\/[A-Za-z0-9-_]+\/([A-Za-z0-9-_]+)\/?.*/, '$1'); | ||
} | ||
return urlParams; | ||
} |
// server | ||
module.exports = function (config, patterns) { | ||
var domain = require('domain'), | ||
events = require('events'), | ||
fs = require('fs'), | ||
http = require('http'), | ||
querystring = require('querystring'), | ||
util = require('util'), | ||
helper = require('./helper')(config), | ||
router = require('./router')(config), | ||
session = require('./session')(config), | ||
methods = { | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
public: { | ||
var domain = require('domain'), | ||
events = require('events'), | ||
fs = require('fs'), | ||
http = require('http'), | ||
querystring = require('querystring'), | ||
util = require('util'), | ||
helper = require('./helper'), | ||
listen = helper.listen, | ||
router = require('./router'), | ||
session = require('./session'); | ||
start: function () { | ||
http.createServer( function (request, response) { | ||
var date = new Date(), | ||
route = router.getRoute(request.url), | ||
controller = {}, | ||
staticPath = '', | ||
params = {}, | ||
body = '', | ||
urlParams = router.getUrlParams(request.url), | ||
sessionID = 0, | ||
requestDomain = domain.create(), | ||
respond = true; | ||
module.exports = { | ||
start: start | ||
}; | ||
requestDomain.add(request); | ||
requestDomain.add(response); | ||
function start() { | ||
var applicationStart = { | ||
cookie: {}, | ||
session: {}, | ||
redirect: {} | ||
}; | ||
requestDomain.on('error', function (e) { | ||
methods.private.error(e, request, response); | ||
}); | ||
listen({ | ||
applicationStart: function (emitter) { | ||
CTZN.on.application.start(emitter); | ||
} | ||
}, function (output) { | ||
applicationStart = helper.extend(applicationStart, output.applicationStart); | ||
if ( CTZN.appOn.application && CTZN.appOn.application.start ) { | ||
listen({ | ||
applicationStart: function (emitter) { | ||
CTZN.appOn.application.start(helper.copy(applicationStart), emitter); | ||
} | ||
}, function (output) { | ||
applicationStart = helper.extend(applicationStart, output.applicationStart); | ||
createServer(applicationStart); | ||
}); | ||
} else { | ||
createServer(applicationStart); | ||
} | ||
}); | ||
} | ||
requestDomain.run( function () { | ||
// If it's a dynamic page request, fire the controller and serve the response when it's ready | ||
if ( !route.isStatic ) { | ||
function createServer(context) { | ||
http.createServer( function (request, response) { | ||
var params = { | ||
request: request, | ||
response: response, | ||
route: router.getRoute(request.url), | ||
url: router.getUrlParams(request.url), | ||
form: {}, | ||
payload: {}, | ||
cookie: parseCookie(request.headers.cookie), | ||
session: {}, | ||
config: CTZN.config | ||
}, | ||
requestDomain = domain.create(); | ||
// Overwrite the default route parameters with URL parameters if they exist | ||
if ( typeof urlParams.type !== 'undefined' ) { | ||
route.type = urlParams.type; | ||
} | ||
if ( typeof urlParams.format !== 'undefined' ) { | ||
route.format = urlParams.format; | ||
} | ||
if ( typeof urlParams.do !== 'undefined' ) { | ||
route.do = urlParams.do; | ||
} | ||
if ( typeof urlParams.show !== 'undefined' ) { | ||
route.show = urlParams.show; | ||
} | ||
// TODO: extend querystring with urlParams (url parameters should take precedence over query strings) | ||
// AJAX requests may also contain payloads in JSON format that need to be parsed as well. | ||
// TODO: extend querystring with urlParams (url parameters should take precedence over query strings) | ||
// AJAX requests may also contain payloads in JSON format that need to be parsed as well. | ||
// Overwrite the default route parameters with URL parameters if they exist | ||
if ( params.url.type ) { | ||
params.route.type = params.url.type; | ||
} | ||
if ( params.url.format ) { | ||
params.route.format = params.url.format; | ||
} | ||
if ( params.url.do ) { | ||
params.route.do = params.url.do; | ||
} | ||
if ( params.url.show ) { | ||
params.route.show = params.url.show; | ||
} | ||
if ( params.url.view ) { | ||
params.route.view = params.url.view; | ||
params.route.renderedView = params.url.view; | ||
} | ||
params = { | ||
config: config, | ||
request: request, | ||
response: response, | ||
route: route, | ||
url: urlParams, | ||
form: {}, | ||
payload: {}, | ||
content: {}, | ||
cookie: helper.parseCookie(request.headers.cookie), | ||
session: {}, | ||
set: { | ||
cookie: {}, | ||
session: {}, | ||
redirect: {} | ||
} | ||
}; | ||
requestDomain.add(params); | ||
requestDomain.add(request); | ||
requestDomain.add(response); | ||
requestDomain.add(context); | ||
controller = patterns[route.safeName].controller; | ||
requestDomain.on('error', function (e) { | ||
listen({ | ||
applicationError: function (emitter) { | ||
CTZN.on.application.error(e, params, context, emitter); | ||
} | ||
}, function (output) { | ||
var applicationError = helper.extend(context, output.applicationError); | ||
if ( CTZN.appOn.application && CTZN.appOn.application.error ) { | ||
listen({ | ||
applicationError: function (emitter) { | ||
CTZN.appOn.application.error(e, params, applicationError, emitter); | ||
} | ||
}, function (output) { | ||
applicationError = helper.extend(applicationError, output.applicationError); | ||
error(e, request, response, params, applicationError); | ||
}); | ||
} else { | ||
error(e, request, response, params, applicationError); | ||
} | ||
}); | ||
}); | ||
if ( config.sessions && ( !request.headers.origin || ( request.headers.origin && request.headers.origin.search(request.headers.host) ) ) ) { | ||
if ( params.cookie.ctzn_session_id && CTZN.sessions[params.cookie.ctzn_session_id] && CTZN.sessions[params.cookie.ctzn_session_id].expires > date.getTime() ) { | ||
CTZN.sessions[params.cookie.ctzn_session_id].expires = date.getTime() + config.sessionLength; | ||
params.session = CTZN.sessions[params.cookie.ctzn_session_id]; | ||
} else { | ||
sessionID = session.new(); | ||
params.set.cookie.ctzn_session_id = { | ||
value: sessionID | ||
}; | ||
params.cookie.ctzn_session_id = { | ||
value: sessionID | ||
}; | ||
params.session = CTZN.sessions[sessionID]; | ||
} | ||
} | ||
requestDomain.run( function () { | ||
var staticPath; | ||
// If the Origin header exists and it's not the host, check if it's allowed. If so, | ||
// set the response header to match the request header (per W3C recs). If not, end the response. | ||
if ( request.headers.origin && !request.headers.origin.search(request.headers.host) ) { | ||
if ( controller.access && controller.access['Access-Control-Allow-Origin'] ) { | ||
if ( controller.access['Access-Control-Allow-Origin'].search(request.headers.origin) >= 0 || access['Access-Control-Allow-Origin'] === '*' ) { | ||
if ( request.method === 'OPTIONS' && !request.headers['access-control-request-method'] ) { | ||
respond = false; | ||
response.end(); | ||
} else { | ||
for ( var property in controller.access ) { | ||
response.setHeader(property, controller.access[property]); | ||
} | ||
response.setHeader('Access-Control-Allow-Origin', request.headers.origin); | ||
} | ||
} else { | ||
respond = false; | ||
response.end(); | ||
} | ||
} else { | ||
respond = false; | ||
response.end(); | ||
} | ||
} | ||
// If it's a dynamic page request, fire requestStart(). Otherwise, serve the static asset. | ||
if ( !params.route.isStatic ) { | ||
if ( CTZN.config.citizen.sessions ) { | ||
sessionStart(helper.copy(params), helper.copy(context)); | ||
} else { | ||
requestStart(helper.copy(params), helper.copy(context)); | ||
} | ||
} else { | ||
staticPath = CTZN.config.citizen.directories.public + params.route.name; | ||
fs.readFile(staticPath, function (err, data) { | ||
if ( err ) { | ||
response.statusCode = 404; | ||
response.end(); | ||
if ( CTZN.config.citizen.mode !== 'production' ) { | ||
console.log('404 Not Found: ' + params.route.name); | ||
} | ||
} else { | ||
response.setHeader('Content-Type', CTZN.config.citizen.mimetypes[params.route.extension]); | ||
response.write(data); | ||
response.end(); | ||
if ( CTZN.config.citizen.mode !== 'production' ) { | ||
console.log('200 OK: ' + params.route.name); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
}).listen(CTZN.config.citizen.httpPort); | ||
} | ||
if ( respond ) { | ||
switch ( request.method ) { | ||
case 'GET': | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
methods.private.respond(controller, params); | ||
break; | ||
case 'PUT': | ||
// params.route.action = 'form'; | ||
request.on('data', function (chunk) { | ||
body += chunk.toString(); | ||
}); | ||
request.on('end', function () { | ||
params.payload = JSON.parse(body); | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
methods.private.respond(controller, params); | ||
}); | ||
break; | ||
case 'DELETE': | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
methods.private.respond(controller, params); | ||
break; | ||
case 'POST': | ||
params.route.action = 'form'; | ||
request.on('data', function (chunk) { | ||
body += chunk.toString(); | ||
}); | ||
request.on('end', function () { | ||
params.form = querystring.parse(body); | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
methods.private.respond(controller, params); | ||
}); | ||
break; | ||
case 'HEAD': | ||
case 'OPTIONS': | ||
response.end(); | ||
break; | ||
} | ||
} | ||
} else { | ||
staticPath = config.directories.public + route.name; | ||
fs.exists(staticPath, function (exists) { | ||
if ( exists ) { | ||
fs.readFile(staticPath, function (err, data) { | ||
if ( err ) { | ||
response.statusCode = 500; | ||
response.end(); | ||
if ( config.mode !== 'production' ) { | ||
console.log(err); | ||
} | ||
} else { | ||
response.write(data); | ||
response.end(); | ||
if ( config.mode !== 'production' ) { | ||
console.log(data); | ||
} | ||
} | ||
}); | ||
} else { | ||
response.statusCode = 404; | ||
response.end(); | ||
if ( config.mode !== 'production' ) { | ||
console.log('Missing file requested: ' + staticPath); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
}).listen(config.httpPort); | ||
function sessionStart(params, context) { | ||
var sessionID = 0; | ||
if ( !params.request.headers.origin || ( params.request.headers.origin && params.request.headers.origin.search(params.request.headers.host) ) ) { | ||
if ( params.cookie.ctzn_session_id && CTZN.sessions[params.cookie.ctzn_session_id] && CTZN.sessions[params.cookie.ctzn_session_id].expires > Date.now() ) { | ||
session.reset(params.cookie.ctzn_session_id); | ||
params.session = CTZN.sessions[params.cookie.ctzn_session_id]; | ||
requestStart(helper.copy(params), helper.copy(context)); | ||
} else { | ||
sessionID = session.create(); | ||
context.cookie.ctzn_session_id = { | ||
value: sessionID | ||
}; | ||
params.cookie.ctzn_session_id = sessionID; | ||
params.session = CTZN.sessions[sessionID]; | ||
listen({ | ||
sessionStart: function (emitter) { | ||
CTZN.on.session.start(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var sessionStart = helper.extend(context, output.sessionStart); | ||
if ( CTZN.appOn.session && CTZN.appOn.session.start ) { | ||
listen({ | ||
sessionStart: function (emitter) { | ||
CTZN.appOn.session.start(helper.copy(params), helper.copy(sessionStart), emitter); | ||
} | ||
}, function (output) { | ||
sessionStart = helper.extend(sessionStart, output.sessionStart); | ||
requestStart(helper.copy(params), helper.copy(sessionStart)); | ||
}); | ||
} else { | ||
requestStart(helper.copy(params), helper.copy(sessionStart)); | ||
} | ||
}); | ||
} | ||
} else { | ||
requestStart(params, context); | ||
} | ||
} | ||
function requestStart(params, context) { | ||
listen({ | ||
requestStart: function (emitter) { | ||
CTZN.on.request.start(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var requestStart = helper.extend(context, output.requestStart); | ||
if ( CTZN.appOn.request && CTZN.appOn.request.start ) { | ||
listen({ | ||
requestStart: function (emitter) { | ||
CTZN.appOn.request.start(helper.copy(params), helper.copy(requestStart), emitter); | ||
} | ||
}, function (output) { | ||
requestStart = helper.extend(requestStart, output.requestStart); | ||
processRequest(helper.copy(params), helper.copy(requestStart)); | ||
}); | ||
} else { | ||
processRequest(helper.copy(params), helper.copy(requestStart)); | ||
} | ||
}); | ||
} | ||
function processRequest(params, context) { | ||
var controller = CTZN.patterns.controllers[params.route.name], | ||
body = '', | ||
respond = true; | ||
// If a redirect is specified, do it immediately rather than firing the controller. | ||
if ( context.redirect.url ) { | ||
params.response.writeHead(context.redirect.statusCode || 302, { | ||
'Location': context.redirect.url | ||
}); | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(context))); | ||
} else if ( controller ) { | ||
// If the Origin header exists and it's not the host, check if it's allowed. If so, | ||
// set the response header to match the request header (per W3C recs). If not, end the response. | ||
if ( params.request.headers.origin && !params.request.headers.origin.search(params.request.headers.host) ) { | ||
if ( controller.access && controller.access['Access-Control-Allow-Origin'] ) { | ||
if ( controller.access['Access-Control-Allow-Origin'].search(params.request.headers.origin) >= 0 || controller.access['Access-Control-Allow-Origin'] === '*' ) { | ||
if ( params.request.method === 'OPTIONS' && !params.request.headers['access-control-request-method'] ) { | ||
respond = false; | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(context))); | ||
} else { | ||
for ( var property in controller.access ) { | ||
params.response.setHeader(property, controller.access[property]); | ||
} | ||
params.response.setHeader('Access-Control-Allow-Origin', params.request.headers.origin); | ||
} | ||
} else { | ||
respond = false; | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(context))); | ||
} | ||
} else { | ||
respond = false; | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(context))); | ||
} | ||
} | ||
if ( respond ) { | ||
switch ( params.request.method ) { | ||
case 'GET': | ||
listen({ | ||
requestEnd: function (emitter) { | ||
CTZN.on.request.end(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var requestEnd = helper.extend(context, output.requestEnd); | ||
if ( CTZN.appOn.request && CTZN.appOn.request.end ) { | ||
listen({ | ||
requestEnd: function (emitter) { | ||
CTZN.appOn.request.end(helper.copy(params), helper.copy(requestEnd), emitter); | ||
} | ||
}, function (output) { | ||
requestEnd = helper.extend(requestEnd, output.requestEnd); | ||
responseStart(controller, params, requestEnd); | ||
}); | ||
} else { | ||
responseStart(controller, params, requestEnd); | ||
} | ||
}); | ||
break; | ||
case 'PUT': | ||
// params.route.action = 'form'; | ||
params.request.on('data', function (chunk) { | ||
body += chunk.toString(); | ||
}); | ||
params.request.on('end', function () { | ||
params.payload = JSON.parse(body); | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
responseStart(controller, params, context); | ||
}); | ||
break; | ||
case 'DELETE': | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
responseStart(controller, params, context); | ||
break; | ||
case 'POST': | ||
params.route.action = 'form'; | ||
params.request.on('data', function (chunk) { | ||
body += chunk.toString(); | ||
}); | ||
params.request.on('end', function () { | ||
params.form = querystring.parse(body); | ||
// TODO: call onRequestEnd() method here, use listener, and on completion, call respond() | ||
responseStart(controller, params, context); | ||
}); | ||
break; | ||
case 'HEAD': | ||
case 'OPTIONS': | ||
params.response.end(); | ||
break; | ||
} | ||
} | ||
} else { | ||
params.response.writeHead(404, { | ||
'Location': 'http://' + params.request.headers.host + CTZN.config.citizen.urlPaths.fileNotFound | ||
}); | ||
params.response.write(); | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(context))); | ||
} | ||
} | ||
}, | ||
function responseStart(controller, params, context) { | ||
listen({ | ||
responseStart: function (emitter) { | ||
CTZN.on.response.start(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var responseStart = helper.extend(context, output.responseStart); | ||
if ( CTZN.appOn.response && CTZN.appOn.response.start ) { | ||
listen({ | ||
responseStart: function (emitter) { | ||
CTZN.appOn.response.start(helper.copy(params), helper.copy(responseStart), emitter); | ||
} | ||
}, function (output) { | ||
responseStart = helper.extend(responseStart, output.responseStart); | ||
fireController(controller, params, responseStart); | ||
}); | ||
} else { | ||
fireController(controller, params, responseStart); | ||
} | ||
}); | ||
} | ||
private: { | ||
function fireController(controller, params, context) { | ||
var responseDomain = domain.create(); | ||
debug: function (pattern, params) { | ||
var debug = params.url.ctzn_debug || 'pattern', | ||
showHidden = params.url.ctzn_debugShowHidden || false, | ||
depth = params.url.ctzn_debugDepth || 2, | ||
colors = params.url.ctzn_debugColors || false, | ||
dump = params.url.ctzn_dump || 'console'; | ||
responseDomain.on('error', function (e) { | ||
error(e, params.request, params.response, params, context); | ||
}); | ||
switch ( dump ) { | ||
case 'console': | ||
console.log(debug + ':\n' + util.inspect(eval(debug), { showHidden: showHidden, depth: depth, colors: colors })); | ||
return false; | ||
case 'view': | ||
return debug + ': ' + JSON.stringify(util.inspect(eval(debug), { showHidden: showHidden, depth: depth, colors: colors })); | ||
} | ||
}, | ||
responseDomain.add(controller); | ||
responseDomain.add(params); | ||
responseDomain.add(context); | ||
error: function (e, request, response) { | ||
switch ( config.mode ) { | ||
case 'production': | ||
switch ( e.code ) { | ||
case 'MODULE_NOT_FOUND': | ||
response.writeHead(404, { | ||
'Location': request.headers.host + config.paths.fileNotFound | ||
}); | ||
break; | ||
default: | ||
// TODO: friendly error page | ||
// response.writeHead(302, { | ||
// 'Location': request.headers.host + '/error/code/' + e.code | ||
// }); | ||
console.log(util.inspect(e)); | ||
response.statusCode = 500; | ||
response.write(e.stack); | ||
} | ||
response.end(); | ||
break; | ||
case 'development': | ||
case 'debug': | ||
console.log(util.inspect(e)); | ||
response.statusCode = 500; | ||
response.write(e.stack); | ||
response.end(); | ||
break; | ||
} | ||
}, | ||
responseDomain.run( function () { | ||
listen({ | ||
pattern: function (emitter) { | ||
controller.handler(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var requestContext = helper.extend(context, output.pattern), | ||
handoff = helper.copy(requestContext.handoff), | ||
handoffController = {}, | ||
handoffView, | ||
handoffParams = {}, | ||
viewContext = helper.extend(requestContext.content, params), | ||
include = requestContext.include || {}, | ||
includeGroup = {}, | ||
cookie = [], | ||
renderer = handoff.controller || params.route.renderer, | ||
renderedView = handoff.view || params.route.renderedView, | ||
contentType; | ||
buildCookie: function (cookies) { | ||
var defaults = {}, | ||
cookie = {}, | ||
cookieArray = [], | ||
pathString = '', | ||
expiresString = '', | ||
httpOnlyString = 'HttpOnly;', | ||
secureString = '', | ||
date = new Date(), | ||
now = date.getTime(); | ||
delete requestContext.handoff; | ||
for ( var property in cookies ) { | ||
defaults = { | ||
value: '', | ||
path: '/', | ||
expires: 'session', | ||
httpOnly: true, | ||
secure: false | ||
}; | ||
cookie = helper.extend(defaults, cookies[property]); | ||
cookieExpires = new Date(); | ||
pathString = 'path=' + cookie.path + ';'; | ||
switch ( cookie.expires ) { | ||
case 'session': | ||
expiresString = ''; | ||
break; | ||
case 'now': | ||
cookieExpires.setTime(now); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expiresString = 'expires=' + cookieExpires + ';'; | ||
break; | ||
case 'never': | ||
cookieExpires.setTime(now + 946080000000); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expiresString = 'expires=' + cookieExpires + ';'; | ||
break; | ||
default: | ||
cookieExpires.setTime(now + cookie.expires); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expiresString = 'expires=' + cookieExpires + ';'; | ||
} | ||
if ( !cookie.httpOnly ) { | ||
httpOnlyString = ''; | ||
} | ||
if ( cookie.secure ) { | ||
secureString = 'secure;'; | ||
} | ||
cookieArray.push(property + '=' + cookie.value + ';' + pathString + expiresString + httpOnlyString + secureString); | ||
} | ||
// If sessions are enabled, the request is from the local host, and set.session has | ||
// properties, merge those properties with the existing session | ||
if ( CTZN.config.citizen.sessions && ( !params.request.headers.origin || ( params.request.headers.origin && params.request.headers.origin.search(params.request.headers.host) ) ) && Object.getOwnPropertyNames(requestContext.session).length > 0 ) { | ||
if ( requestContext.session.expires && requestContext.session.expires === 'now' ) { | ||
session.end(params.session.id); | ||
requestContext.cookie = helper.extend(requestContext.cookie, { ctzn_session_id: { expires: 'now' }}); | ||
sessionEnd(helper.copy(params), helper.copy(requestContext)); | ||
} else { | ||
CTZN.sessions[params.session.id] = helper.extend(CTZN.sessions[params.session.id], requestContext.session); | ||
} | ||
} | ||
return cookieArray; | ||
}, | ||
cookie = buildCookie(requestContext.cookie); | ||
if ( cookie.length ) { | ||
params.response.setHeader('Set-Cookie', cookie); | ||
} | ||
respond: function (controller, params) { | ||
var responseDomain = domain.create(); | ||
if ( requestContext.redirect.url ) { | ||
params.response.writeHead(requestContext.redirect.statusCode || 302, { | ||
'Location': requestContext.redirect.url | ||
}); | ||
} | ||
responseDomain.on('error', function (e) { | ||
methods.private.error(e, params.request, params.response); | ||
}); | ||
if ( handoff.controller ) { | ||
handoffController = CTZN.patterns.controllers[handoff.controller.replace('/-/g', '_')]; | ||
handoffView = handoff.view || handoff.controller; | ||
handoffParams = helper.extend(params, { route: { renderer: renderer, renderedView: renderedView }}); | ||
fireController(handoffController, handoffParams, helper.copy(requestContext)); | ||
} else { | ||
if ( Object.getOwnPropertyNames(include).length > 0 && params.url.type !== 'ajax' ) { | ||
for ( var property in include ) { | ||
if ( include.hasOwnProperty(property) ) { | ||
includeGroup[property] = function (emitter) { | ||
CTZN.patterns.controllers[property].handler(helper.copy(params), helper.copy(requestContext), emitter); | ||
}; | ||
} | ||
} | ||
listen(includeGroup, function (output) { | ||
viewContext.include = {}; | ||
for ( var property in include ) { | ||
if ( include.hasOwnProperty(property) ) { | ||
viewContext.include[property] = renderView(property, include[property], params.route.format, helper.extend(viewContext, output[property])); | ||
} | ||
} | ||
viewContext.include._main = renderView(params.route.name, params.route.view, params.route.format, viewContext); | ||
respond(params, requestContext, viewContext, renderer, renderedView); | ||
}); | ||
} else { | ||
respond(params, requestContext, viewContext, renderer, renderedView); | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
responseDomain.add(controller); | ||
responseDomain.add(params); | ||
function respond(params, requestContext, viewContext, renderer, renderedView) { | ||
var contentType; | ||
responseDomain.run( function () { | ||
helper.listener({ | ||
pattern: function (emitter) { | ||
controller.handler(helper.copy(params), emitter); | ||
} | ||
}, function (output) { | ||
var cookie = []; | ||
switch ( params.route.format ) { | ||
case 'html': | ||
contentType = 'text/html'; | ||
break; | ||
case 'json': | ||
case 'JSON': | ||
contentType = 'application/json'; | ||
// If the output is JSON, pass the raw content to the view renderer | ||
viewContext = requestContext; | ||
break; | ||
case 'jsonp': | ||
case 'JSONP': | ||
contentType = 'text/javascript'; | ||
// If the output is JSONP, pass the raw content to the view renderer | ||
viewContext.content = requestContext.content; | ||
break; | ||
} | ||
if ( !requestContext.redirect.url ) { | ||
params.response.setHeader('Content-Type', contentType); | ||
} | ||
// If debugging is enabled, append the debug output to viewContext | ||
if ( CTZN.config.citizen.mode === 'debug' || ( CTZN.config.citizen.mode === 'development' && params.url.debug ) ) { | ||
viewContext.debugOutput = debug(requestContext, params); | ||
} | ||
params.response.write(renderView(renderer, renderedView, params.route.format, viewContext)); | ||
params.response.end(responseEnd(helper.copy(params), helper.copy(requestContext))); | ||
} | ||
// If sessions are enabled, the request is from the local host, and set.session has | ||
// properties, merge those properties with the existing session | ||
if ( config.sessions && ( !params.request.headers.origin || ( params.request.headers.origin && params.request.headers.origin.search(params.request.headers.host) ) ) && output.pattern.set.session ) { | ||
if ( output.pattern.set.session.expires && output.pattern.set.session.expires === 'now' ) { | ||
delete CTZN.sessions[params.session.id]; | ||
params.set.cookie = helper.extend(params.set.cookie, { ctzn_session_id: { expires: 'now' }}); | ||
} else { | ||
CTZN.sessions[params.session.id] = helper.extend(CTZN.sessions[params.session.id], output.pattern.set.session); | ||
} | ||
} | ||
function responseEnd(params, context) { | ||
listen({ | ||
responseEnd: function (emitter) { | ||
CTZN.on.response.end(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var responseEnd = helper.extend(context, output.responseEnd); | ||
if ( CTZN.appOn.response && CTZN.appOn.response.end ) { | ||
listen({ | ||
responseEnd: function (emitter) { | ||
CTZN.appOn.response.end(helper.copy(params), helper.copy(responseEnd), emitter); | ||
} | ||
}, function (output) { | ||
responseEnd = helper.extend(responseEnd, output.responseEnd); | ||
}); | ||
} | ||
}); | ||
} | ||
if ( output.pattern.set.cookie ) { | ||
params.set.cookie = helper.extend(params.set.cookie, output.pattern.set.cookie); | ||
} | ||
cookie = methods.private.buildCookie(params.set.cookie); | ||
if ( cookie.length ) { | ||
params.response.setHeader('Set-Cookie', cookie); | ||
} | ||
function sessionEnd(params, context) { | ||
listen({ | ||
sessionEnd: function (emitter) { | ||
CTZN.on.session.end(helper.copy(params), helper.copy(context), emitter); | ||
} | ||
}, function (output) { | ||
var sessionEnd = helper.extend(context, output.sessionEnd); | ||
if ( CTZN.appOn.session && CTZN.appOn.session.end ) { | ||
listen({ | ||
sessionEnd: function (emitter) { | ||
CTZN.appOn.session.end(helper.copy(params), helper.copy(sessionEnd), emitter); | ||
} | ||
}, function (output) { | ||
sessionEnd = helper.extend(sessionEnd, output.sessionEnd); | ||
}); | ||
} | ||
}); | ||
} | ||
// Debug handling | ||
if ( config.mode === 'debug' || ( config.mode === 'development' && params.url.debug ) ) { | ||
output.pattern.debugOutput = methods.private.debug(output.pattern.content, params); | ||
} | ||
function debug(pattern, params) { | ||
var toDebug = params.url.ctzn_debug || 'pattern', | ||
showHidden = params.url.ctzn_debugShowHidden || false, | ||
depth = params.url.ctzn_debugDepth || CTZN.config.citizen.debug.depth, | ||
colors = params.url.ctzn_debugColors || false, | ||
dump = params.url.ctzn_dump || CTZN.config.citizen.debug.output; | ||
// If set.redirect has properties, send the redirect | ||
if ( output.pattern.set.redirect.statusCode ) { | ||
params.response.writeHead(output.pattern.set.redirect.statusCode, { | ||
'Location': output.pattern.set.redirect.url | ||
}); | ||
} | ||
switch ( dump ) { | ||
case 'console': | ||
console.log(toDebug + ':\n' + util.inspect(eval(toDebug), { showHidden: showHidden, depth: depth, colors: colors })); | ||
return false; | ||
case 'view': | ||
return toDebug + ': ' + JSON.stringify(util.inspect(eval(toDebug), { showHidden: showHidden, depth: depth, colors: colors })); | ||
} | ||
} | ||
params.response.write(helper.renderView(params.route.name, params.route.format, output.pattern)); | ||
function error(e, request, response, params, context) { | ||
switch ( CTZN.config.citizen.mode ) { | ||
case 'production': | ||
switch ( e.code ) { | ||
case 'MODULE_NOT_FOUND': | ||
response.writeHead(404, { | ||
'Location': request.headers.host + CTZN.config.citizen.paths.fileNotFound | ||
}); | ||
break; | ||
default: | ||
// TODO: friendly error page | ||
// response.writeHead(302, { | ||
// 'Location': request.headers.host + '/error/code/' + e.code | ||
// }); | ||
response.statusCode = 500; | ||
if ( e.stack ) { | ||
response.write(e.stack); | ||
} else { | ||
response.write(e); | ||
} | ||
} | ||
response.end(); | ||
break; | ||
case 'development': | ||
case 'debug': | ||
console.log(util.inspect(e)); | ||
response.statusCode = 500; | ||
if ( e.stack ) { | ||
response.write(e.stack); | ||
} else { | ||
response.write(e); | ||
} | ||
response.end(); | ||
break; | ||
} | ||
} | ||
// TODO: call onResponseEnd method inside here | ||
params.response.end(); | ||
}); | ||
}); | ||
} | ||
function buildCookie(cookies) { | ||
var defaults = {}, | ||
cookie = {}, | ||
cookieArray = [], | ||
path = '', | ||
expires = '', | ||
httpOnly = 'HttpOnly;', | ||
secure = '', | ||
cookieExpires, | ||
now = Date.now(); | ||
} | ||
}; | ||
for ( var property in cookies ) { | ||
defaults = { | ||
value: '', | ||
path: '/', | ||
expires: 'session', | ||
httpOnly: true, | ||
secure: false | ||
}; | ||
cookie = helper.extend(defaults, cookies[property]); | ||
cookieExpires = new Date(); | ||
path = 'path=' + cookie.path + ';'; | ||
switch ( cookie.expires ) { | ||
case 'session': | ||
expires = ''; | ||
break; | ||
case 'now': | ||
cookieExpires.setTime(now); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expires = 'expires=' + cookieExpires + ';'; | ||
break; | ||
case 'never': | ||
cookieExpires.setTime(now + 946080000000); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expires = 'expires=' + cookieExpires + ';'; | ||
break; | ||
default: | ||
cookieExpires.setTime(now + cookie.expires); | ||
cookieExpires = cookieExpires.toUTCString(); | ||
expires = 'expires=' + cookieExpires + ';'; | ||
} | ||
if ( !cookie.httpOnly ) { | ||
httpOnly = ''; | ||
} | ||
if ( cookie.secure ) { | ||
secure = 'secure;'; | ||
} | ||
cookieArray.push(property + '=' + cookie.value + ';' + path + expires + httpOnly + secure); | ||
} | ||
return methods.public; | ||
}; | ||
return cookieArray; | ||
} | ||
function parseCookie(cookie) { | ||
var pairs = [], | ||
pair = [], | ||
cookies = {}; | ||
if ( cookie ) { | ||
pairs = cookie.split(';'); | ||
pairs.forEach( function (cookie, index, array) { | ||
pair = pairs[index].trim(); | ||
pair = pair.split('='); | ||
cookies[pair[0]] = pair[1]; | ||
}); | ||
} | ||
return cookies; | ||
} | ||
function renderView(pattern, view, format, context) { | ||
var viewOutput = '', | ||
callbackRegex; | ||
switch ( format ) { | ||
case 'html': | ||
case 'HTML': | ||
switch ( CTZN.config.citizen.mode ) { | ||
case 'production': | ||
switch ( CTZN.patterns.views[pattern][view].engine ) { | ||
case 'handlebars': | ||
viewOutput = CTZN.patterns.views[pattern][view].compiled(context, {}); | ||
break; | ||
case 'jade': | ||
viewOutput = CTZN.patterns.views[pattern][view].compiled(context, { compileDebug: false }); | ||
break; | ||
case 'html': | ||
viewOutput = CTZN.patterns.views[pattern][view].raw; | ||
break; | ||
} | ||
break; | ||
case 'debug': | ||
case 'development': | ||
switch ( CTZN.patterns.views[pattern][view].engine ) { | ||
case 'handlebars': | ||
viewOutput = fs.readFileSync(CTZN.config.citizen.directories.views + '/' + pattern + '/' + view + '.hbs', { 'encoding': 'utf8' }); | ||
viewOutput = CTZN.handlebars.compile(viewOutput); | ||
viewOutput = viewOutput(context, {}); | ||
break; | ||
case 'jade': | ||
viewOutput = fs.readFileSync(CTZN.config.citizen.directories.views + '/' + pattern + '/' + view + '.jade', { 'encoding': 'utf8' }); | ||
viewOutput = CTZN.jade.compile(viewOutput); | ||
viewOutput = viewOutput(context, {}); | ||
break; | ||
case 'html': | ||
viewOutput = fs.readFileSync(CTZN.config.citizen.directories.views + '/' + pattern + '/' + view + '.html', { 'encoding': 'utf8' }); | ||
break; | ||
} | ||
if ( context.debugOutput ) { | ||
viewOutput = viewOutput.replace('</body>', '<div id="citizen-debug"><pre>' + context.debugOutput + '</pre></div></body>'); | ||
} | ||
break; | ||
} | ||
break; | ||
case 'json': | ||
case 'JSON': | ||
viewOutput = JSON.stringify(context.content); | ||
break; | ||
case 'jsonp': | ||
case 'JSONP': | ||
callbackRegex = new RegExp(/^[A-Za-z0-9_]*$/); | ||
if ( callbackRegex.test(context.url.callback) ) { | ||
viewOutput = context.url.callback + '(' + JSON.stringify(context.content) + ');'; | ||
} else { | ||
throw 'Error: JSONP callback names should consist of letters, numbers, and underscores only.'; | ||
} | ||
break; | ||
} | ||
return viewOutput; | ||
} |
// session management | ||
module.exports = function (config) { | ||
var methods = { | ||
'use strict'; | ||
/* jshint node: true */ | ||
/* global CTZN: false */ | ||
public: { | ||
module.exports = { | ||
create: create, | ||
end: end, | ||
reset: reset | ||
}; | ||
new: function () { | ||
var sessionID = 0, | ||
date = new Date(); | ||
function create() { | ||
var sessionID = 0, | ||
started = Date.now(), | ||
expires = started + CTZN.config.citizen.sessionTimeout; | ||
while ( !CTZN.sessions[sessionID] ) { | ||
sessionID = methods.private.generateSessionID(); | ||
if ( !CTZN.sessions[sessionID] ) { | ||
CTZN.sessions[sessionID] = { | ||
id: sessionID, | ||
expires: date.getTime() + config.sessionLength | ||
}; | ||
} | ||
} | ||
while ( !CTZN.sessions[sessionID] ) { | ||
sessionID = generateSessionID(); | ||
if ( !CTZN.sessions[sessionID] ) { | ||
CTZN.sessions[sessionID] = { | ||
id: sessionID, | ||
started: started, | ||
expires: expires | ||
}; | ||
} | ||
} | ||
return sessionID; | ||
}, | ||
return sessionID; | ||
} | ||
end: function (sessionID) { | ||
function end(sessionID) { | ||
if ( CTZN.sessions[sessionID] ) { | ||
delete CTZN.sessions[sessionID]; | ||
} | ||
} | ||
} | ||
function reset(sessionID) { | ||
if ( CTZN.sessions[sessionID] ) { | ||
CTZN.sessions[sessionID].expires = Date.now() + CTZN.config.citizen.sessionTimeout; | ||
} | ||
} | ||
}, | ||
private: { | ||
generateSessionID: function () { | ||
return Math.random().toString().replace('0.', '') + Math.random().toString().replace('0.', ''); | ||
} | ||
} | ||
}; | ||
return methods.public; | ||
}; | ||
function generateSessionID() { | ||
return Math.random().toString().replace('0.', '') + Math.random().toString().replace('0.', ''); | ||
} |
{ | ||
"name": "citizen", | ||
"version": "0.0.23", | ||
"version": "0.1.0", | ||
"description": "An event-driven MVC framework for Node.js web applications.", | ||
@@ -20,3 +20,4 @@ "author": { | ||
"dependencies": { | ||
"handlebars": "1.3", | ||
"handlebars": "2.0.0", | ||
"jade": "1.7.0", | ||
"commander": "1.0.0" | ||
@@ -23,0 +24,0 @@ }, |
740
README.md
citizen | ||
======= | ||
citizen is an event-driven MVC framework for Node.js web applications. It's still in a pre-alpha state and not suitable for public consumption, but I wanted to get it out there before the name was taken. | ||
citizen is an event-driven MVC framework for Node.js web applications. Its goal is to handle serving, routing, and event emitter creation, while providing some useful helpers to get you on your way. The nuts and bolts of your application are up to you, but citizen's helpers are designed to work with certain patterns and conventions, which are covered throughout this guide. | ||
The goal of citizen is to handle serving, routing, and event emitter creation, while providing some useful helpers to get you on your way. The nuts and bolts of your application are up to you, but citizen's helpers are designed to work with certain patterns and conventions, which are covered throughout this guide. | ||
citizen is in beta. Your comments, criticisms, and requests are welcome. | ||
The only dependency at this point is [Handlebars](https://npmjs.org/package/handlebars). Current static file serving is just a hack to enable development; I use [nginx](http://nginx.org) as a front end and I'm debating whether I should even add a module to incorporate file serving into citizen. | ||
citizen's static file serving is just a hack to get your dev environment up and running quickly. I recommend something like [nginx](http://nginx.org) as a front end for static file serving in your production environment. | ||
### Windows compatibility | ||
I developed citizen using Mac and Linux environments. Windows support is first on my list of testing/fixes. | ||
Installing citizen | ||
@@ -23,56 +27,98 @@ ------------------ | ||
Initializing citizen | ||
------------------- | ||
Configuring and Initializing citizen | ||
------------------------------------ | ||
citizen can accept arguments when it starts, so initializing it is a bit different from typical Node.js modules because it's a function call. The following assignment will initialize citizen with the default configuration. | ||
The following assignment will initialize your citizen app: | ||
app = require('citizen')(); | ||
app = require('citizen'); | ||
You can pass arguments to change citizen's startup parameters: | ||
app = require('citizen')({ | ||
### Application Directory Structure | ||
// Mode determines certain framework behaviors such as error handling (dumps vs. friendly | ||
// errors). Options are 'debug', 'development', or 'production'. Default is 'production'. | ||
mode: 'debug', | ||
Here's the most basic directory structure of a citizen web app: | ||
directories: { | ||
// Full directory path pointing to this app. Default is '/'. | ||
app: '/path/to/your/app', | ||
app/ | ||
patterns/ | ||
controllers/ | ||
index.js | ||
models/ | ||
index.js | ||
views/ | ||
index/ | ||
index.js | ||
start.js // Entry point --> app = require('citizen'); | ||
public/ | ||
// Full directory path pointing to your patterns. Default is '/patterns'. | ||
patterns: '/path/to/your/app/patterns', | ||
Here's a more complex app example (more about `config` and `on` directories below): | ||
// Full directory path pointing to your static assets if you're using citizen to serve them. Default is '/public'. | ||
public: '/path/to/your/static/assets' | ||
}, | ||
app/ | ||
config/ | ||
citizen.json | ||
db.json | ||
logs/ | ||
on/ | ||
application.js | ||
request.js | ||
response.js | ||
session.js | ||
patterns/ | ||
controllers/ | ||
index.js | ||
models/ | ||
index.js | ||
views/ | ||
index/ | ||
index.hbs | ||
index-alt.hbs | ||
start.js | ||
public/ | ||
urlPaths: { | ||
// URL path to this app. If your app's URL is http://www.site.com/my/app, then this | ||
// should be set to '/my/app'. Default is '/'. | ||
app: '/' | ||
}, | ||
// Port for the web server. Default is 80. | ||
httpPort: 8080, | ||
### Configuration | ||
// Enable session management | ||
sessions: true, | ||
The `config` directory is optional and is used to store your app's configuration files in JSON format. You can have multiple JSON files within this directory, allowing different configurations based on environment. citizen retrieves its configuration file from this directory based on the following logic: | ||
// Session length in milliseconds. Default is 1200000 (20 minutes). | ||
sessionLength: 600000, | ||
1. citizen parses each JSON file looking for a "hostname" key that matches the machine's hostname. If it finds one, it loads that configuration. | ||
2. If it can't find a matching hostname key, it looks for a file named citizen.json and loads that configuration. | ||
3. If it can't find citizen.json, it runs under its default configuration. | ||
// If you use a separate URL to serve your static assets, specify the root URL here. | ||
staticAssetUrl: '//static.yoursite.com' | ||
}); | ||
citizen also parses any other files it finds in this directory and stores the resulting configuration within `app.config`. Using the file structure above, you'd end up with `app.config.citizen` and `app.config.db`. | ||
The following represents citizen's default configuration. | ||
{ | ||
mode: 'production', | ||
directories: { | ||
app: process.cwd(), | ||
logs: process.cwd() + '/logs', | ||
on: process.cwd() + '/on', | ||
patterns: process.cwd() + '/patterns', | ||
public: path.resolve(process.cwd(), '../public') | ||
}, | ||
urlPaths: { | ||
app: '/' | ||
}, | ||
httpPort: 80, | ||
sessions: false, | ||
sessionTimeout: 1200000, // 20 minutes | ||
requestTimeout: 30000, // 30 seconds | ||
mimeTypes: JSON.parse(fs.readFileSync(path.join(__dirname, '../config/mimetypes.json'))), | ||
debug: { | ||
output: 'console', | ||
depth: 2 | ||
} | ||
} | ||
Objects returned by citizen: | ||
- `app.config` Includes the configuration settings you supplied at startup | ||
- `app.helper` A function library to make it easier to work with citizen | ||
- `app.patterns` Controllers, models, and views (both raw and compiled) from your supplied patterns, which you can use instead of `require` | ||
- `app.server` Functions related to starting and running the web server | ||
- `CTZN` A global namespace used by citizen for session storage, among other things. | ||
- `app.config` Includes the configuration settings you supplied at startup | ||
- `app.helper` Functions citizen uses internally that you might find helpful in your own app | ||
- `app.patterns` Controllers, models, and views (both raw and compiled) from your supplied patterns, which you can use instead of `require` | ||
- `app.start()` The function used to start the web server | ||
- `app.listen()` citizen's event listener for one or many asynchronous functions | ||
- `app.handlebars` A pointer to the citizen Handlebars global, allowing you full access to Handlebars methods such as registerHelper | ||
- `app.jade` A pointer to the citizen Jade global | ||
- `CTZN` A global namespace used by citizen for session storage, among other things | ||
You should avoid accessing or modifying the `CTZN` namespace directly; anything that you might need in your application will be exposed by the server through local scopes. | ||
You should not access or modify the `CTZN` namespace directly; anything you might need in your application will be exposed by the server to your controllers through local scopes. | ||
@@ -84,3 +130,3 @@ | ||
app.server.start(); | ||
app.start(); | ||
@@ -94,7 +140,7 @@ | ||
http://www.site.com/pattern-name/SEO-content-goes-here/parameter/value/parameter2/value2 | ||
http://www.site.com/controller-name/SEO-content-here/parameter/value/parameter2/value2 | ||
For example, let's say your site's base URL is: | ||
http://www.cleverna.me/ | ||
http://www.cleverna.me | ||
@@ -115,7 +161,7 @@ Requesting that URL will cause the `index` controller to fire, because the index pattern is the default pattern. The following URL will also cause the index controller to fire: | ||
citizen also lets you optionally insert relevent content into your URLs, like so: | ||
citizen also lets you optionally insert relevant content into your URLs, like so: | ||
http://www.cleverna.me/article/My-clever-article-title/id/237/page/2 | ||
This SEO content is not parsed by the framework in any way and can be whatever you like, but it must always immediately follow the pattern name and precede any name/value pairs. | ||
This SEO content must always follow the pattern name and precede any name/value pairs. You can access it via a URL parameter called `descriptor`, which means you can use it as a unique identifier (more on URL parameters below). | ||
@@ -129,26 +175,29 @@ | ||
/your-app-path/patterns/article/article-controller.js | ||
/your-app-path/patterns/article/article-model.js | ||
/your-app-path/patterns/article/article.html | ||
app/ | ||
patterns/ | ||
controllers/ | ||
article.js | ||
models/ | ||
article.js | ||
views/ | ||
article/ | ||
article.hbs | ||
Each controller requires at least one public function named `handler()`. The citizen server calls `handler()` after it processes the initial request and passes it two arguments: an object containing the parameters of the request and an emitter for the controller to emit when it's done. | ||
### handler(params, context, emitter) | ||
The `args` object contains the following objects: | ||
Each controller requires at least one public function named `handler()`. The citizen server calls `handler()` after it processes the initial request and passes it 3 arguments: an object containing the parameters of the request, the current request's context generated by the app up to this point, and an emitter for the controller to emit when it's done. | ||
- `config` citizen config settings | ||
- `content` The content object is the view context to which you append your pattern's content | ||
- `request` The inital request object received by the server | ||
- `response` The response object sent by the server | ||
The `params` object contains the following objects: | ||
- `request` The request object generated by the server | ||
- `response` The response object generated by the server | ||
- `route` Details of the route, such as the requested URL and the name of the route (controller) | ||
- `url` Any URL parameters that were passed (See "Routing and URLs" above) | ||
- `url` Any URL parameters that were passed including the descriptor, if provided | ||
- `form` Data collected from a POST | ||
- `payload` Data collected from a PUT | ||
- `cookie` An object containing any cookies that were sent with the request | ||
- `session` An object containing any session variables | ||
- `set` An object for you to set cookies, session variables, and redirects within your controllers | ||
- `session` An object containing any session variables, if sessions are enabled | ||
The server passes `args` to your controller, which you then modify as necessary (to append your view content or set cookies, for example) and then pass back to the server via the emitter. | ||
In addition to having access to these objects within your controller, they are also passed to your view context automatically so you can use them within your view templates (more details in the Views section). | ||
In addition to having access to these objects within your controller, they are also passed to your view context automatically so you can use them within your Handlebars templates (more details in the Views section). | ||
Based on the previous example URL... | ||
@@ -158,23 +207,37 @@ | ||
...you'll have the following `args.url` object passed to your controller: | ||
...you'll have the following `params.url` object passed to your controller: | ||
{ id: 237, page: 2 } | ||
{ | ||
id: 237, | ||
page: 2, | ||
descriptor: 'My-clever-article-title' | ||
} | ||
Using these parameters, I can retrieve the article content from the model, append it to the view context, and pass the context back to the server: | ||
// article-controller.js | ||
// article controller | ||
exports.handler = handler; | ||
function handler(args, emitter) { | ||
// Populate your view context | ||
args.content.article = app.patterns.article.model.getArticle(args.url.id, args.url.page); | ||
function handler(params, context, emitter) { | ||
// Get the article content | ||
var content = { | ||
article: app.patterns.models.article.getArticle(params.url.id, params.url.page) | ||
}; | ||
// Emit the 'ready' event and pass the args object back to the server for rendering | ||
emitter.emit('ready', args); | ||
// Emit the 'ready' event and pass the view context back to the server for rendering. | ||
emitter.emit('ready', { | ||
content: content | ||
}); | ||
}; | ||
The emitter should emit a "ready" event when the controller has accomplished its task. This lets the server know it's time to send the response. | ||
The second argument in the emitter is an object containing any data you want to pass back to citizen, including the view context. All the content you want to render in your view should be passed to citizen within an object called `content`, as shown above. Additional objects can be passed to citizen to perform other functions, which are described later in this document. | ||
We haven't discussed the `context` argument yet. This argument contains any output that's been generated by the request up to this point. There are various events that can populate this argument, which is then passed to your controller so you can access it. More on this later. | ||
Here's a simple model: | ||
// article-model.js | ||
// article model | ||
@@ -184,29 +247,29 @@ exports.getArticle = getArticle; | ||
function getArticle(id, page) { | ||
var articles = { | ||
'236': { | ||
title: 'I <3 Node.js', | ||
summary: 'Things I love about Node', | ||
pages: { | ||
'1': 'First page content', | ||
'2': 'Second page content' | ||
} | ||
}, | ||
'237': { | ||
title: 'What\'s in room 237?', | ||
summary: 'Nothin\'. There\'s nothin\' in room 237.', | ||
pages: { | ||
'1': 'Nothing to see here.', | ||
'2': 'Actually, yeah, there is.' | ||
} | ||
} | ||
}; | ||
var articles = { | ||
'236': { | ||
title: 'I <3 Node.js', | ||
summary: 'Things I love about Node', | ||
pages: { | ||
'1': 'First page content', | ||
'2': 'Second page content' | ||
} | ||
}, | ||
'237': { | ||
title: 'What\'s in room 237?', | ||
summary: 'Nothin\'. There\'s nothin\' in room 237.', | ||
pages: { | ||
'1': 'Nothing to see here.', | ||
'2': 'Actually, yeah, there is.' | ||
} | ||
} | ||
}; | ||
return { | ||
title: articles[id]['title'], | ||
summary: articles[id]['summary'], | ||
text: articles[id]['pages'][page] | ||
}; | ||
return { | ||
title: articles[id]['title'], | ||
summary: articles[id]['summary'], | ||
text: articles[id]['pages'][page] | ||
}; | ||
}; | ||
In `article.html`, you can now reference the `content` object like so: | ||
In `article.hbs`, you can now reference objects you placed within the `content` object passed by the emitter: | ||
@@ -216,11 +279,11 @@ <!DOCTYPE html> | ||
<body> | ||
<h1> | ||
{{content.article.title}} | ||
</h1> | ||
<p id="summary"> | ||
{{content.article.summary}} | ||
</p> | ||
<div id="text"> | ||
{{content.article.text}} | ||
</div> | ||
<h1> | ||
{{article.title}} | ||
</h1> | ||
<p id="summary"> | ||
{{article.summary}} | ||
</p> | ||
<div id="text"> | ||
{{article.text}} | ||
</div> | ||
</body> | ||
@@ -231,32 +294,39 @@ </html> | ||
listener() | ||
listen() | ||
---------- | ||
The previous example has simple methods that return static content immediately, but things are rarely that simple. The `listener()` function takes advantage of the asynchronous, event-driven nature of Node.js, letting you wrap a single function or multiple asynchronous functions within it and firing a callback when they're done. You can also chain and nest multiple `listener()` functions for very powerful asynchronous function calls. | ||
The previous example has simple methods that return static content immediately, but things are rarely that simple. The `listen()` function takes advantage of the asynchronous, event-driven nature of Node.js, letting you wrap a single function or multiple asynchronous functions within it and firing a callback when they're done. You can also chain and nest multiple `listen()` functions for very powerful asynchronous function calls. | ||
`listener()` takes two arguments: an object containing one or more methods you want to call, and a callback to handle the output. `listener()` requires that your functions be written to accept an optional `args` object and an `emitter` object. | ||
`listen()` takes two arguments: an object containing one or more methods you want to call, and a callback to handle the output. `listen()` requires that your functions be written to accept an `emitter` argument, which is how your function notifies listen() that it's ready. | ||
Let's say our article model has two methods that need to be called before returning the results to the controller. One is called getContent() and the other is getViewers(). Assume that getViewers() makes an asynchronous database call and won't be able to return its output immediately, so we have to listen for when it's ready and then react. | ||
Let's say our article model has two methods that need to be called before returning the results to the controller. One is called getArticle() and the other is getViewers(). Assume that both methods make an asynchronous call to a database and won't be able to return their output immediately, so we have to listen for when they're ready and then react. | ||
// article-controller.js | ||
// article controller | ||
exports.handler = handler; | ||
function handler(args, emitter) { | ||
app.helper.listener({ | ||
// The property name references the action you want to listen for, which is wrapped in an anonymous function | ||
article: function (emitter) { | ||
app.patterns.article.model.getArticle({ id: 237, page: 2 }, emitter); | ||
}, | ||
// This property doesn't require an anonymous function because the only argument its function takes is the emitter generated by listener(), so you can just use the function name | ||
viewers: app.patterns.article.model.getViewers | ||
}, function (output) { | ||
// The property name you assign to the methods above becomes the name of the output object | ||
args.content.article: output.article; | ||
args.content.viewers: output.viewers; | ||
// Or just: args.content = output; | ||
function handler(params, context, emitter) { | ||
app.listen({ | ||
// The property contains the action you want to listen for, which is | ||
// wrapped in an anonymous function in order to pass the emitter | ||
article: function (emitter) { | ||
app.patterns.models.article.getArticle({ id: params.url.id, page: params.url.page }, emitter); | ||
}, | ||
viewers: function (emitter) { | ||
app.patterns.models.article.getViewers(params.url.id, emitter); | ||
} | ||
}, function (output) { | ||
// The property name you assign to the methods above becomes the | ||
// name of the output object | ||
var content = { | ||
article: output.article, | ||
viewers: output.viewers | ||
}; | ||
// Emit `ready` now that we have the handler output and pass `args` back to the server | ||
emitter.emit('ready', args); | ||
// Emit `ready` now that we have the handler output and pass the | ||
// context back to the server | ||
emitter.emit('ready', { | ||
content: content | ||
}); | ||
}); | ||
} | ||
@@ -266,91 +336,105 @@ | ||
// article-model.js | ||
// article model | ||
module.exports = { | ||
getArticle: getArticle, | ||
getViewers: getViewers | ||
getArticle: getArticle, | ||
getViewers: getViewers | ||
}; | ||
function getArticle(args, emitter) { | ||
var articles = { | ||
'236': { | ||
title: 'I <3 Node.js', | ||
summary: 'Things I love about Node', | ||
pages: { | ||
'1': 'First page content', | ||
'2': 'Second page content' | ||
} | ||
}, | ||
'237': { | ||
title: 'What\'s in room 237?', | ||
summary: 'Nothin\'. There\'s nothin\' in room 237.', | ||
pages: { | ||
'1': 'Nothing to see here.', | ||
'2': 'Actually, yeah, there is.' | ||
} | ||
} | ||
}; | ||
emitter.emit('ready', { | ||
title: articles[args.id]['title'], | ||
summary: articles[args.id]['summary'], | ||
text: articles[args.id]['pages'][page] | ||
}); | ||
app.db.article(args.id, function (data) { | ||
// When the database returns the data, emit `ready` and pass the | ||
// data back to listen() | ||
emitter.emit('ready', data); | ||
}); | ||
}; | ||
function getViewers(emitter) { | ||
myFunction.that.gets.viewers( function (data) { | ||
// When the database returns the data, emit `ready` and pass the data back to listener() | ||
emitter.emit('ready', data); | ||
}); | ||
function getViewers(id, emitter) { | ||
app.db.viewers(id, function (data) { | ||
// When the database returns the data, emit `ready` and pass the | ||
// data back to listen() | ||
emitter.emit('ready', data); | ||
}); | ||
}; | ||
listen() currently fires all functions asynchronously and returns the results for every function in a single output object after all functions have completed. A waterfall-type execution is being worked on, but in the meantime, you can nest listen() functions to achieve the same effect: | ||
listen({ | ||
first: function (emitter) { | ||
doSomething(emitter); | ||
} | ||
}, function (output) { | ||
listen({ | ||
second: function (emitter) { | ||
doNextThing(output, emitter); | ||
} | ||
}, function (output) { | ||
listen({ | ||
third: function (emitter) { | ||
doOneMoreThing(output, emitter); | ||
} | ||
}, function (output) { | ||
thisIsExhausting(output); | ||
}); | ||
}); | ||
}); | ||
Setting Cookies and Session Variables | ||
------------------------------------- | ||
In addition to the view context, the server's `ready` emitter also accepts an object called `set`, which is used for setting cookies and session variables. | ||
Setting Cookies, Session Variables, and Redirects | ||
------------------------------------------------- | ||
In addition to the view context, the server's `ready` emitter also accepts objects used for setting cookies, session variables, and redirects. | ||
### Cookies | ||
You set cookies by appending them to `set.cookie`. Cookies can be set one at a time or in groups. The following code tells the server to set `username` and `passwordHash` cookies that never expire: | ||
You set cookies by appending a `cookie` object to the return context. Cookies can be set one at a time or in groups. The following code tells the server to set `username` and `passwordHash` cookies that never expire: | ||
// login-controller.js | ||
// login controller | ||
function handler(args, emitter) { | ||
app.helper.listener({ | ||
login: function (emitter) { | ||
app.patterns.login.model.authenticate({ | ||
username: args.form.username, | ||
password: args.form.password | ||
}, emitter); | ||
} | ||
}, function (output) { | ||
args.content.login = output.login; | ||
exports.handler = handler; | ||
if ( args.content.login.success === true ) { | ||
args.set.cookie = { | ||
// The cookie gets its name from the property name | ||
username: { | ||
// The cookie value | ||
value: args.content.login.username, | ||
function handler(params, context, emitter) { | ||
app.listen({ | ||
login: function (emitter) { | ||
app.patterns.models.login.authenticate({ | ||
// Form values, just like URL parameters, are passed via the params | ||
// argument | ||
username: params.form.username, | ||
password: params.form.password | ||
}, emitter); | ||
} | ||
}, function (output) { | ||
var content = { | ||
login: output.login | ||
}, | ||
cookie; | ||
// Valid expiration options are: | ||
// 'now' - deletes an existing cookie | ||
// 'never' - current time plus 30 years, so effectively never | ||
// 'session' - expires at the end of the browser session (default) | ||
// [time in milliseconds] - length of time, added to the current time | ||
expires: 'never' | ||
}, | ||
passwordHash: { | ||
value: args.content.login.passwordHash, | ||
expires: 'never' | ||
} | ||
}; | ||
if ( content.login.success === true ) { | ||
cookie = { | ||
// The cookie gets its name from the property name | ||
username: { | ||
// The cookie value | ||
value: content.login.username, | ||
// Valid expiration options are: | ||
// 'now' - deletes an existing cookie | ||
// 'never' - current time plus 30 years, so effectively never | ||
// 'session' - expires at the end of the browser session (default) | ||
// [time in milliseconds] - length of time, added to current time | ||
expires: 'never' | ||
}, | ||
passwordHash: { | ||
value: content.login.passwordHash, | ||
expires: 'never' | ||
} | ||
}; | ||
} | ||
emitter.emit('ready', args); | ||
emitter.emit('ready', { | ||
content: content, | ||
cookie: cookie | ||
}); | ||
}); | ||
}; | ||
@@ -360,8 +444,8 @@ | ||
args.set.cookie.username = 'Danny'; | ||
args.set.cookie.nickname = 'Doc'; | ||
cookie.username = content.login.username; | ||
cookie.passwordHash = content.login.passwordHash; | ||
Other cookie options include `path` (default is `/`), `httpOnly` (default is `true`), and `secure` (default is `false`). | ||
Other cookie options include `path` (default is `/`), `httpOnly` (default is `true` for security reasons), and `secure` (default is `false`). | ||
Cookies sent by the client are available in `args.cookie` within the controller and simply `cookie` within the view context: | ||
Cookies sent by the client are available in `params.cookie` within the controller and simply `cookie` within the view context: | ||
@@ -371,13 +455,13 @@ <!DOCTYPE html> | ||
<body> | ||
<div id="welcome"> | ||
{{#if cookie.username}} | ||
Welcome, {{cookie.username}}. | ||
{{else}} | ||
<a href="/login">Login</a> | ||
{{/if}} | ||
</div> | ||
<div id="welcome"> | ||
{{#if cookie.username}} | ||
Welcome, {{cookie.username}}. | ||
{{else}} | ||
<a href="/login">Login</a> | ||
{{/if}} | ||
</div> | ||
</body> | ||
</html> | ||
Cookie variables you set within your controller aren't immediately available within the `args.cookie` scope. citizen's server has to receive the response from the controller before it can send the cookie to the client, so use a local instance of the variable if you need to access it during the same request. | ||
Cookie variables you set within your controller aren't immediately available within the `params.cookie` scope. citizen's server has to receive the response from the controller before it can send the cookie to the client, so use a local instance of the variable if you need to access it during the same request. | ||
@@ -388,3 +472,3 @@ | ||
If sessions are enabled, citizen creates an object called `CTZN.sessions` to store session information. You should avoid accessing this object directly and use `args.session` instead (or simply `session` within the view context), which automatically references the current user's session. | ||
If sessions are enabled, citizen creates an object called `CTZN.sessions` to store session information. You should avoid accessing this object directly and use `params.session` instead (or simply `session` within the view), which automatically references the current user's session. | ||
@@ -395,42 +479,90 @@ By default, the session has two properties: `id` and `expires`. The session ID is also sent to the client as a cookie called `ctzn_session_id`. | ||
args.set.session.username = 'Danny'; | ||
session.username = 'Danny'; | ||
session.nickname = 'Doc'; | ||
To forcibly clear and expire a user's session: | ||
emitter.emit('ready', { | ||
content: content, | ||
session: session | ||
}); | ||
args.set.session.expires = 'now'; | ||
To forcibly clear and expire the current user's session: | ||
Like cookies, session variables you've just assigned aren't available during the same request within the `args.session` scope, so use a local instance if you need to access this data right away. | ||
session.expires = 'now'; | ||
Like cookies, session variables you've just assigned aren't available during the same request within the `params.session` scope, so use a local instance if you need to access this data right away. | ||
Redirects | ||
--------- | ||
The `set` object is also used to pass redirect instructions to the server that are to be enacted after the request is complete. Redirects using `set` are not immediate, so everything your controller is asked to do, it will do before the redirect is processed. The user agent won't receive a full response, however. No view content will be sent. | ||
### Redirects | ||
The `redirect` object takes two keys: `statusCode` and `url`. | ||
You can pass redirect instructions to the server that are to be enacted after the request is complete. Redirects using this method are not immediate, so everything your controller is asked to do, it will do before the redirect is processed. The user agent won't receive a full response, however. No view content will be sent, but cookies and session variables will be set if specified. | ||
args.set.redirect = { | ||
statusCode: 302, | ||
url: 'http://redirect.com' | ||
The `redirect` object takes two keys: `statusCode` and `url`. If you don't provide a status code, citizen uses 302 (temporary redirect). | ||
redirect = { | ||
statusCode: 301, | ||
url: 'http://redirect.com' | ||
}; | ||
emitter.emit('ready', { | ||
content: content, | ||
redirect: redirect | ||
}); | ||
Application Events and the Context Argument | ||
------------------------------------------- | ||
Certain events will occur throughout the life of your citizen application. You can intercept these application events, execute functions, and pass the results to the next event, where they'll eventually be passed to your controller via the `context` argument. For example, you might set a custom cookie at the beginning of every new session, or check for cookies at the beginning of every request and redirect the user to a login page if they're not authenticated. | ||
To take advantage of these events, include a directory called "on" in your app with the following modules and exports: | ||
app/ | ||
on/ | ||
application.js // exports start(), end(), and error() | ||
request.js // exports start() and end() | ||
response.js // exports start() and end() | ||
session.js // exports start() and end() | ||
All files and exports are optional. citizen only calls them if they exist. For example, you could have only a session.js module that exports end(). | ||
_Note: As of this update, session end() and application end() aren't functional. They'll be in a future version._ | ||
Here's an example of a request module that checks for a username cookie at the beginning of every request and redirects the user to a login page if it's not there, unless the requested pattern is the login page: | ||
// app/on/request.js | ||
exports.start = start; | ||
function start(params, context, emitter) { | ||
var redirect = {}; | ||
if ( !params.cookie.username && params.route.name !== 'login' ) { | ||
redirect.url = 'http://' + params.request.headers.host + '/login'; | ||
} | ||
emitter.emit('ready', { | ||
redirect: redirect | ||
}); | ||
}; | ||
HTTP Access Control (CORS) | ||
-------------------------- | ||
citizen supports cross-domain HTTP requests via access control headers. By default, all patterns respond to requests from the host only. To enable cross-domain access, simply add an `access` object with the necessary headers to your controller's exports: | ||
citizen supports cross-domain HTTP requests via access control headers. By default, all controllers respond to requests from the host only. To enable cross-domain access, simply add an `access` object with the necessary headers to your controller's exports: | ||
module.exports = { | ||
handler: handler, | ||
access: { | ||
'Access-Control-Allow-Origin': 'http://www.somesite.com http://anothersite.com', | ||
'Access-Control-Expose-Headers': 'X-My-Custom-Header, X-Another-Custom-Header', | ||
'Access-Control-Max-Age': 1728000, | ||
'Access-Control-Allow-Credentials': 'true', | ||
'Access-Control-Allow-Methods': 'OPTIONS, PUT', | ||
'Access-Control-Allow-Headers': 'Content-Type', | ||
'Vary': 'Origin' | ||
} | ||
handler: handler, | ||
access: { | ||
'Access-Control-Allow-Origin': 'http://www.somesite.com http://anothersite.com', | ||
'Access-Control-Expose-Headers': 'X-My-Custom-Header, X-Another-Custom-Header', | ||
'Access-Control-Max-Age': 1728000, | ||
'Access-Control-Allow-Credentials': 'true', | ||
'Access-Control-Allow-Methods': 'OPTIONS, PUT', | ||
'Access-Control-Allow-Headers': 'Content-Type', | ||
'Vary': 'Origin' | ||
} | ||
}; | ||
@@ -445,7 +577,9 @@ | ||
If you set `mode: 'debug'` at startup, citizen dumps the current pattern's output to the console by default. You can also dump it to the view with the `ctzn_dump` URL parameter: | ||
*** `debug` and `development` modes are inherently insecure. Do not use them in a production environment. *** | ||
If you set `"mode": "debug"` in your config file, citizen dumps the current pattern's output to the console by default. You can also dump it to the view with the `ctzn_dump` URL parameter: | ||
http://www.cleverna.me/article/id/237/page/2/ctzn_dump/view | ||
By default, the pattern's complete output is dumped to the console. You can specify the exact object to debug with the `ctzn_debug` URL parameter. You can access globals, `pattern`, and server `params`: | ||
By default, the pattern's complete output is dumped. You can specify the exact object to debug with the `ctzn_debug` URL parameter. You can access globals, `pattern`, and server `params`: | ||
@@ -466,16 +600,166 @@ // Dumps pattern.content to the console | ||
If you always want debug output dumped to the view, set debug output to "view" in your citizen config file. | ||
Helpers | ||
------- | ||
In addition to `listener()`, citizen includes a few more basic helper functions to make your life easier. | ||
In addition to `listen()`, citizen includes a few more basic helper functions that it uses internally, but might be of use to you, so it returns them for public use. | ||
(docs coming soon) | ||
### copy(object) | ||
Creates a deep copy of an object. | ||
var myCopy = app.helper.copy(myObject); | ||
### extend(object, extension[, boolean]) | ||
Extends an object with another object, effectively merging the two objects. By default, extend() creates a copy of the original before extending it, creating a new object. If your intention is to alter the original and create a pointer, pass the optional third argument of `false`. | ||
var newObject = app.helper.extend(originalObject, extensionObject); | ||
### isNumeric(object) | ||
Returns `true` if the object is a number, `false` if not. | ||
Views | ||
----- | ||
citizen currently uses [Handlebars](https://npmjs.org/package/handlebars) for view rendering, but I'll probably add other options in the future. | ||
citizen supports [Handlebars](https://npmjs.org/package/handlebars) and [Jade](https://www.npmjs.org/package/jade) templates, as well as good old HTML. You can even mix and match Handlebars, Jade, and HTML templates as you see fit; just use the appropriate file extensions (.hbs, .jade, or .html) and citizen will compile and render each view with the appropriate engine. | ||
You have direct access to each engine's methods via `app.handlebars` and `app.jade`, allowing you to use methods like `app.handlebars.registerHelper` to create global helpers. Just keep in mind that you're extending the global Handlebars and Jade objects and could potentially affect citizen's view rendering if you do anything wacky because citizen relies on these same objects. | ||
### Includes | ||
citizen has a standardized way of handling includes that works in both Handlebars and Jade templates. In citizen, includes are more than just chunks of code that you can reuse. citizen includes are full patterns, each having its own controller, model, and view(s). | ||
Let's say our article pattern's Handlebars template has the following contents: | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>{{metaData.title}}</title> | ||
<meta name="description" content="{{metaData.description}}" /> | ||
<meta name="keywords" content="{{metaData.keywords}}" /> | ||
<link rel="stylesheet" type="text/css" href="app.css" /> | ||
</head> | ||
<body> | ||
<header> | ||
<a id="logo">Home page</a> | ||
<nav> | ||
<ul> | ||
<li> | ||
<a href="/">Home</a> | ||
</li> | ||
<li> | ||
<a href="/articles">Settings</a> | ||
</li> | ||
</ul> | ||
</nav> | ||
</header> | ||
<main> | ||
<h1> | ||
{{article.title}} | ||
</h1> | ||
<p id="summary"> | ||
{{article.summary}} | ||
</p> | ||
<div id="text"> | ||
{{article.text}} | ||
</div> | ||
</main> | ||
</body> | ||
</html> | ||
It probably makes sense to use includes for the head section and header because you'll use that code everywhere. Let's create patterns for the head and header. I like to follow the convention of starting partials with an underscore, but that's up to you: | ||
app/ | ||
patterns/ | ||
controllers/ | ||
_head.js | ||
_header.js | ||
article.js | ||
models/ | ||
_head.js | ||
_header.js | ||
article.js | ||
views/ | ||
_head/ | ||
_head.hbs | ||
_header/ | ||
_header.hbs | ||
_header-authenticated.hbs // A different header for logged in users | ||
article/ | ||
article.hbs | ||
When the article controller is fired, it needs to tell citizen which includes it needs. We do that with the `include` directive, which we pass via the context in the emitter: | ||
// article controller | ||
exports.handler = handler; | ||
function handler(params, context, emitter) { | ||
app.listen({ | ||
article: function (emitter) { | ||
app.patterns.models.article.getArticle({ id: params.url.id, page: params.url.page }, emitter); | ||
}, | ||
viewers: function (emitter) { | ||
app.patterns.models.article.getViewers(params.url.id, emitter); | ||
} | ||
}, function (output) { | ||
var content = { | ||
article: output.article, | ||
viewers: output.viewers | ||
}; | ||
emitter.emit('ready', { | ||
content: content, | ||
include: { | ||
// The property name is the name of the include's controller. | ||
// The property value is the name of the view. | ||
_head: '_head', | ||
_header: '_header' | ||
} | ||
}); | ||
}); | ||
} | ||
This tells citizen to call the _head controller and render the _head view, and the _header controller and the _header view, then add both to the view context. In article.hbs, we now reference the includes using the `include` object: | ||
<!DOCTYPE html> | ||
<html> | ||
{{include._head}} | ||
<body> | ||
{{include._header}} | ||
<main> | ||
<h1> | ||
{{article.title}} | ||
</h1> | ||
<p id="summary"> | ||
{{article.summary}} | ||
</p> | ||
<div id="text"> | ||
{{article.text}} | ||
</div> | ||
</main> | ||
</body> | ||
</html> | ||
What if logged in users get a different header? Just tell citizen to use a different view: | ||
emitter.emit('ready', { | ||
content: content, | ||
include: { | ||
_head: '_head', | ||
_header: '_header-authenticated' | ||
} | ||
}); | ||
Includes can generate content and add it to the view context of your primary controller (article.js in this example) because the primary view is the last to be rendered. However, includes are called and rendered asynchronously, so while your _head controller can generate content and add it to the view context of your article controller, don't assume that your _header controller will have access to that data. (The option of waterfall execution is being worked on, so this is only true for the time being.) | ||
<!-- ### handoff | ||
citizen allows the requested controller to give another controller the responsibility of handling the request and rendering its own view via a context object called `handoff`. The secondary controller assumes responsibility for the request, adding its own content to the context and rendering its own view. You can implement as many handoffs as you want (controller A can handoff to controller B, who can handoff to controller C, and so on). --> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
75707
11
1309
750
3
8
+ Addedjade@1.7.0
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedcamelcase@1.2.1(transitive)
+ Addedcharacter-parser@1.2.0(transitive)
+ Addedcommander@2.1.0(transitive)
+ Addedconstantinople@2.0.1(transitive)
+ Addedcss@1.0.8(transitive)
+ Addedcss-parse@1.0.4(transitive)
+ Addedcss-stringify@1.0.5(transitive)
+ Addeddecamelize@1.2.0(transitive)
+ Addedhandlebars@2.0.0(transitive)
+ Addedis-promise@1.0.1(transitive)
+ Addedjade@1.7.0(transitive)
+ Addedminimatch@10.0.1(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedmonocle@1.1.51(transitive)
+ Addedpromise@2.0.0(transitive)
+ Addedreaddirp@0.2.5(transitive)
+ Addedsource-map@0.1.34(transitive)
+ Addedtransformers@2.1.0(transitive)
+ Addeduglify-js@2.2.52.4.24(transitive)
+ Addeduglify-to-browserify@1.0.2(transitive)
+ Addedvoid-elements@1.0.0(transitive)
+ Addedwindow-size@0.1.0(transitive)
+ Addedwith@3.0.1(transitive)
+ Addedwordwrap@0.0.2(transitive)
+ Addedyargs@3.5.4(transitive)
- Removedhandlebars@1.3.0(transitive)
- Removedsource-map@0.1.43(transitive)
Updatedhandlebars@2.0.0