mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
Refactoring and simplification
This commit is contained in:
parent
e8a7f66a62
commit
2e89fbe9cc
340
lib/Context.js
340
lib/Context.js
@ -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;
|
||||
@ -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++;
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"dependencies": [
|
||||
{
|
||||
"package": "raptor/render-context"
|
||||
},
|
||||
{
|
||||
"package": "raptor/promises"
|
||||
},
|
||||
{
|
||||
"package": "raptor/data-providers"
|
||||
},
|
||||
"render-context_async.js",
|
||||
"Context_async.js"
|
||||
]
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,2 +0,0 @@
|
||||
require('raptor-util').extend(require('../index'), { DEFAULT_TIMEOUT: 10000 });
|
||||
require('./Context_async');
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"dependencies": [
|
||||
{
|
||||
"package": "raptor-strings"
|
||||
},
|
||||
{
|
||||
"package": "raptor-xml/util"
|
||||
},
|
||||
"Context.js",
|
||||
"render-context.js"
|
||||
]
|
||||
}
|
||||
31
lib/package.json
Normal file
31
lib/package.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -26,5 +26,8 @@
|
||||
"main": "lib/promises.js",
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"browser": {
|
||||
"./lib/Context_server.js": null
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user