Refactoring and simplification

This commit is contained in:
Patrick Steele-Idem 2014-01-29 21:32:07 -08:00
parent e8a7f66a62
commit 2e89fbe9cc
9 changed files with 242 additions and 331 deletions

View File

@ -20,149 +20,221 @@
* used to capture the rendered output.
*/
'use strict';
var StringBuilder = require('raptor-strings/StringBuilder');
var logger = require('raptor-logging').logger(module);
var promiseUtil = require('raptor-promises/util');
var extend = require('raptor-util').extend;
var createError = require('raptor-util').createError;
var forEachEntry = require('raptor-util').forEachEntry;
var escapeXmlAttr = require('raptor-xml/utils').escapeXmlAttr;
var StringBuilder = require('../strings/StringBuilder');
var nextUniqueId = 0;
function _classFunc(className, name) {
var Clazz = require(className);
var func = Clazz[name] || Clazz.prototype && Clazz.prototype[name];
if (!func) {
throw createError(new Error('Helper function not found with name "' + name + '" in class "' + className + '"'));
}
return func;
function getAsyncAttributes(context) {
var attrs = context.attributes;
return attrs.async || (attrs.async = {});
}
function addTimeoutHandler(timeoutMillis, asyncFragment) {
setTimeout(function () {
if (!asyncFragment.finished) {
logger.error('Async fragment timed out after ' + timeoutMillis + 'ms. Creation stack trace: ' + asyncFragment.stack);
asyncFragment.end();
}
}, timeoutMillis);
}
function Fragment(context) {
this.context = context;
// The context that this async fragment is associated with
this.writer = context.writer;
// The original writer this fragment was associated with
this.finished = false;
// Used to keep track if this async fragment was ended
this.flushed = false;
// Set to true when the contents of this async fragment have been
// flushed to the original writer
this.next = null;
// A link to the next sibling async fragment (if any)
this.ready = true; // Will be set to true if this fragment is ready to be flushed
// (i.e. when there are no async fragments preceeding this fragment)
}
function flushNext(fragment, writer) {
var next = fragment.next;
if (next) {
next.ready = true;
// Since we have flushed the next fragment is ready
next.writer = next.context.writer = writer;
// Update the next fragment to use the original writer
next.flush(); // Now flush the next fragment (if it is not finish then it will just do nothing)
}
}
function BufferedFragment(context, buffer) {
Fragment.call(this, context);
this.buffer = buffer;
}
BufferedFragment.prototype = {
flush: function () {
if (!this.ready || this.flushed) {
throw new Error('Invalid state');
}
var writer = this.writer;
writer.write(this.buffer.toString());
//this.writer = this.context.writer = voidWriter; // Prevent additional out-of-order writes
this.flushed = true;
flushNext(this, writer);
}
};
function AsyncFragment(context) {
Fragment.call(this, context);
}
AsyncFragment.prototype = {
end: function (e) {
if (e) {
logger.error('Async fragment failed. Exception: ' + (e.stack || e) + '\n Creation stack trace: ' + this.stack);
}
if (this.finished) {
return;
}
var asyncAttributes = getAsyncAttributes(this.context);
try {
// Make sure end is only called once by the user
this.finished = true;
if (this.ready) {
// There are no nesting asynchronous fragments that are
// remaining and we are ready to be flushed then let's do it!
this.flush();
}
// Keep track of how many asynchronous fragments are in the template
// NOTE: firstPassComplete changes to true after processing all of the nodes of the template
if (--asyncAttributes.remaining === 0 && asyncAttributes.firstPassComplete) {
// If we were the last fragment to complete then fulfil the promise for
// the template rendering using the output of the underlying writer
this.context.emit('end');
}
} catch (e) {
this.context.emit('error', e);
}
},
flush: function () {
if (!this.ready || this.flushed) {
throw new Error('Invalid state');
}
if (!this.finished) {
// Skipped Flushing since not finished
return;
}
this.flushed = true;
flushNext(this, this.writer);
}
};
function Context(writer) {
this.writer = writer;
this.w = this.write;
this.listeners = {};
this.attributes = {};
}
Context.classFunc = _classFunc;
var proto = {
getAttributes: function () {
return this.attributes;
},
getAttribute: function (name) {
return this.attributes[name];
},
uniqueId: function () {
return 'c' + nextUniqueId++;
},
write: function (str) {
if (str !== null && str !== undefined) {
if (typeof str !== 'string') {
str = str.toString();
}
this.writer.write(str);
Context.DEFAULT_TIMEOUT = 10000;
Context.prototype = {
getAttributes: function () {
return this.attributes;
},
getAttribute: function (name) {
return this.attributes[name];
},
uniqueId: function () {
return 'c' + nextUniqueId++;
},
write: function (str) {
if (str !== null && str !== undefined) {
if (typeof str !== 'string') {
str = str.toString();
}
return this;
},
getOutput: function () {
return this.writer.toString();
},
captureString: function (func, thisObj) {
var sb = new StringBuilder();
this.swapWriter(sb, func, thisObj);
return sb.toString();
},
swapWriter: function (newWriter, func, thisObj) {
var oldWriter = this.writer;
try {
this.writer = newWriter;
func.call(thisObj);
} finally {
this.writer = oldWriter;
}
},
createNestedContext: function (writer) {
var context = require('raptor-render-context').createContext(writer);
context.attributes = this.getAttributes();
return context;
},
invokeHandler: function (handler, input) {
if (typeof handler === 'string') {
handler = require(handler);
}
var func = handler.process || handler.render;
func.call(handler, input, this);
},
getFunction: function (className, name) {
if (!this._helpers) {
this._helpers = {};
}
var key = className + ':' + name;
var helper = this._helpers[key];
if (!helper) {
helper = this._helpers[key] = _classFunc(className, name).bind(this);
}
return helper;
},
getHelperObject: function (className) {
if (!this._helpers) {
this._helpers = {};
}
var Helper = this._helpers[className] || (this._helpers[className] = require(className));
return new Helper(this);
},
isTagInput: function (input) {
return input && input.hasOwnProperty('_tag');
},
renderTemplate: function (name, data) {
require('raptor-templates').render(name, data, this);
return this;
},
attr: function (name, value, escapeXml) {
if (value === null || value === true) {
value = '';
} else if (value === undefined || value === false || typeof value === 'string' && value.trim() === '') {
return this;
} else {
value = '="' + (escapeXml === false ? value : escapeXmlAttr(value)) + '"';
}
this.write(' ' + name + value);
return this;
},
attrs: function (attrs) {
if (arguments.length !== 1) {
this.attr.apply(this, arguments);
} else if (attrs) {
forEachEntry(attrs, this.attr, this);
}
return this;
},
t: function (handler, props, body, dynamicAttributes, namespacedProps) {
if (!props) {
props = {};
}
props._tag = true;
if (body) {
props.invokeBody = body;
}
if (dynamicAttributes) {
props.dynamicAttributes = dynamicAttributes;
}
if (namespacedProps) {
extend(props, namespacedProps);
}
this.invokeHandler(handler, props);
return this;
},
c: function (func) {
var output = this.captureString(func);
return {
toString: function () {
return output;
}
};
this.writer.write(str);
}
};
// Add short-hand method names that should be used in compiled templates *only*
proto.a = proto.attrs;
proto.f = proto.getFunction;
proto.o = proto.getHelperObject;
proto.i = proto.renderTemplate;
Context.prototype = proto;
return this;
},
getOutput: function () {
return this.writer.toString();
},
captureString: function (func, thisObj) {
var sb = new StringBuilder();
this.swapWriter(sb, func, thisObj);
return sb.toString();
},
swapWriter: function (newWriter, func, thisObj) {
var oldWriter = this.writer;
try {
this.writer = newWriter;
func.call(thisObj);
} finally {
this.writer = oldWriter;
}
},
createNestedContext: function (writer) {
var context = require('raptor-render-context').createContext(writer);
context.attributes = this.getAttributes();
return context;
},
beginAsyncFragment: function (callback, timeout) {
var asyncAttributes = getAsyncAttributes(this);
// Keep a count of all of the async fragments for this rendering
asyncAttributes.remaining++;
var ready = true;
var asyncContext = this.createNestedContext(this.writer);
var buffer = new StringBuilder();
this.writer = buffer;
var asyncFragment = new AsyncFragment(asyncContext);
var bufferedFragment = new BufferedFragment(this, buffer);
asyncFragment.next = bufferedFragment;
asyncContext.parentAsyncFragment = asyncFragment;
var prevAsyncFragment = this.prevAsyncFragment || this.parentAsyncFragment;
// See if we are being buffered by a previous asynchronous
// fragment
if (prevAsyncFragment) {
// Splice in our two new fragments and add a link to the previous async fragment
// so that it can let us know when we are ready to be flushed
bufferedFragment.next = prevAsyncFragment.next;
prevAsyncFragment.next = asyncFragment;
if (!prevAsyncFragment.flushed) {
ready = false; // If we are preceeded by another async fragment then we aren't ready to be flushed
}
}
asyncFragment.ready = ready;
// Set the ready flag based on our earlier checks above
this.prevAsyncFragment = bufferedFragment;
// Record the previous async fragment for linking purposes
asyncFragment.stack = new Error().stack;
if (timeout == null) {
timeout = Context.DEFAULT_TIMEOUT;
}
if (timeout > 0) {
addTimeoutHandler(timeout, asyncFragment);
}
try {
var promise = callback(asyncContext, asyncFragment);
// Provide the user with the async context and the async fragment
if (promise) {
promiseUtil.immediateThen(
promise,
function (result) {
if (result != null) {
asyncContext.write(result);
}
asyncFragment.end();
},
function (e) {
asyncFragment.end(e);
});
}
} catch (e) {
logger.error('beginAsyncFragment failed. Exception: ' + e, e);
asyncFragment.end();
}
}
};
if (typeof window === 'undefined') {
extend(Context.prototype, require('./Context_server.js'));
}
module.exports = Context;

View File

@ -1,5 +1,4 @@
'use strict';
require('raptor-util').extend(require('./Context'), {
module.exports = {
uniqueId: function () {
var attrs = this.attributes;
if (!attrs._nextId) {
@ -7,4 +6,4 @@ require('raptor-util').extend(require('./Context'), {
}
return attrs._nextId++;
}
});
};

View File

@ -1,15 +0,0 @@
{
"dependencies": [
{
"package": "raptor/render-context"
},
{
"package": "raptor/promises"
},
{
"package": "raptor/data-providers"
},
"render-context_async.js",
"Context_async.js"
]
}

View File

@ -1,162 +0,0 @@
'use strict';
var StringBuilder = require('../strings/StringBuilder');
var logger = require('raptor-logging').logger(module);
var renderContext = require('raptor-render-context');
var promiseUtil = require('raptor-promises/util');
function getAsyncAttributes(context) {
var attrs = context.attributes;
return attrs.async || (attrs.async = {});
}
function addTimeoutHandler(timeoutMillis, asyncFragment) {
setTimeout(function () {
if (!asyncFragment.finished) {
logger.error('Async fragment timed out after ' + timeoutMillis + 'ms. Creation stack trace: ' + asyncFragment.stack);
asyncFragment.end();
}
}, timeoutMillis);
}
function Fragment(context) {
this.context = context;
// The context that this async fragment is associated with
this.writer = context.writer;
// The original writer this fragment was associated with
this.finished = false;
// Used to keep track if this async fragment was ended
this.flushed = false;
// Set to true when the contents of this async fragment have been
// flushed to the original writer
this.next = null;
// A link to the next sibling async fragment (if any)
this.ready = true; // Will be set to true if this fragment is ready to be flushed
// (i.e. when there are no async fragments preceeding this fragment)
}
function flushNext(fragment, writer) {
var next = fragment.next;
if (next) {
next.ready = true;
// Since we have flushed the next fragment is ready
next.writer = next.context.writer = writer;
// Update the next fragment to use the original writer
next.flush(); // Now flush the next fragment (if it is not finish then it will just do nothing)
}
}
function BufferedFragment(context, buffer) {
Fragment.call(this, context);
this.buffer = buffer;
}
BufferedFragment.prototype = {
flush: function () {
if (!this.ready || this.flushed) {
throw new Error('Invalid state');
}
var writer = this.writer;
writer.write(this.buffer.toString());
//this.writer = this.context.writer = voidWriter; // Prevent additional out-of-order writes
this.flushed = true;
flushNext(this, writer);
}
};
function AsyncFragment(context) {
Fragment.call(this, context);
}
AsyncFragment.prototype = {
end: function (e) {
if (e) {
logger.error('Async fragment failed. Exception: ' + (e.stack || e) + '\n Creation stack trace: ' + this.stack);
}
if (this.finished) {
return;
}
var asyncAttributes = getAsyncAttributes(this.context);
try {
// Make sure end is only called once by the user
this.finished = true;
if (this.ready) {
// There are no nesting asynchronous fragments that are
// remaining and we are ready to be flushed then let's do it!
this.flush();
}
// Keep track of how many asynchronous fragments are in the template
// NOTE: firstPassComplete changes to true after processing all of the nodes of the template
if (--asyncAttributes.remaining === 0 && asyncAttributes.firstPassComplete) {
// If we were the last fragment to complete then fulfil the promise for
// the template rendering using the output of the underlying writer
asyncAttributes.deferred.resolve(this.context);
}
} catch (e) {
asyncAttributes.deferred.reject(e); // Something went wrong... make sure we always reject or resolve the promise
}
},
flush: function () {
if (!this.ready || this.flushed) {
throw new Error('Invalid state');
}
if (!this.finished) {
// Skipped Flushing since not finished
return;
}
this.flushed = true;
flushNext(this, this.writer);
}
};
require('raptor-util').extend(require('./Context').prototype, {
beginAsyncFragment: function (callback, timeout) {
var asyncAttributes = getAsyncAttributes(this);
// Keep a count of all of the async fragments for this rendering
asyncAttributes.remaining++;
var ready = true;
var asyncContext = this.createNestedContext(this.writer);
var buffer = new StringBuilder();
this.writer = buffer;
var asyncFragment = new AsyncFragment(asyncContext);
var bufferedFragment = new BufferedFragment(this, buffer);
asyncFragment.next = bufferedFragment;
asyncContext.parentAsyncFragment = asyncFragment;
var prevAsyncFragment = this.prevAsyncFragment || this.parentAsyncFragment;
// See if we are being buffered by a previous asynchronous
// fragment
if (prevAsyncFragment) {
// Splice in our two new fragments and add a link to the previous async fragment
// so that it can let us know when we are ready to be flushed
bufferedFragment.next = prevAsyncFragment.next;
prevAsyncFragment.next = asyncFragment;
if (!prevAsyncFragment.flushed) {
ready = false; // If we are preceeded by another async fragment then we aren't ready to be flushed
}
}
asyncFragment.ready = ready;
// Set the ready flag based on our earlier checks above
this.prevAsyncFragment = bufferedFragment;
// Record the previous async fragment for linking purposes
asyncFragment.stack = new Error().stack;
if (timeout == null) {
timeout = renderContext.DEFAULT_TIMEOUT;
}
if (timeout > 0) {
addTimeoutHandler(timeout, asyncFragment);
}
try {
var promise = callback(asyncContext, asyncFragment);
// Provide the user with the async context and the async fragment
if (promise) {
promiseUtil.immediateThen(
promise,
function (result) {
if (result != null) {
asyncContext.write(result);
}
asyncFragment.end();
},
function (e) {
asyncFragment.end(e);
});
}
} catch (e) {
logger.error('beginAsyncFragment failed. Exception: ' + e, e);
asyncFragment.end();
}
}
});

View File

@ -1,2 +0,0 @@
require('raptor-util').extend(require('../index'), { DEFAULT_TIMEOUT: 10000 });
require('./Context_async');

View File

@ -21,13 +21,10 @@
* in the {@link raptor/templating/compiler} module.
*/
'use strict';
var StringBuilder = require('raptor-strings/StringBuilder');
var Context = require('./Context');
var tryRequire = require('raptor-util').tryRequire;
exports.createContext = function (writer) {
tryRequire('./async', require);
return new Context(writer || new StringBuilder()); //Create a new context using the writer provided
};

View File

@ -1,12 +0,0 @@
{
"dependencies": [
{
"package": "raptor-strings"
},
{
"package": "raptor-xml/util"
},
"Context.js",
"render-context.js"
]
}

31
lib/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"type": "raptor-module",
"name": "raptor/renderer",
"version": "1.0",
"description": "Helper module for rendering UI components",
"homepage": "http://raptorjs.org",
"authors": [
{
"name": "Patrick Steele-Idem",
"email": "psteeleidem@ebay.com"
}
],
"raptor": {
"dependencies": [
{"module": "raptor/strings"},
{"module": "raptor/xml/utils"},
"Context.js",
"render-context.js"
],
"extensions": [
{
"name": "server",
"dependencies": [
"Context_server.js",
{"module": "raptor/render-context/async"}
]
}
]
}
}

View File

@ -26,5 +26,8 @@
"main": "lib/promises.js",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"browser": {
"./lib/Context_server.js": null
}
}