define(['mmirf/languageManager', 'mmirf/parserModule', 'mmirf/storageUtils'],
//this comment is needed by jsdoc2 [copy of comment for: function ContentElement(...]
/**
* The ContentElement represents "content" parts of a view; it may itself contain one or more ContentElements.
*
* This class holds the name of the content-field (used via the yield-tag in the layouts: content, header, footer, dialogs, ...)
* and its definition as HTML-String.
*
* @class
* @name ContentElement
* @memberOf mmir.view
* @public
*
* @param {Array|Object} group
* an array or object with properties <code>name</code> {String}, and <code>content</code> {String}
* @param {Object} view
* the view that owns this ContentElement-element
* @param {mmir.parser.ParserUtils} parser
* for the the content (optional) if supplied this object must have a function <code>parse({String})</code> (see templateParseUtil)
* @param {mmir.parser.RenderUtils} renderer
* for the the content (optional) if supplied, a <code>parser</code> must also be supplied; the renderer must have a function <code>parse({String})</code> (see templateRenderUtil)
*
*/
function(
languageManager, parser_context
){//NOTE: dependency storageUtils is actually accessed through parser_context (i.e. it attaches its functions to parserModule)
/** @scope ContentElement.prototype *///for jsdoc2
//set to @ignore in order to avoid doc-duplication in jsdoc3
/**
* The ContentElement represents "content" parts of a view; it may itself contain one or more ContentElements.
*
* This class holds the name of the content-field (used via the yield-tag in the layouts: content, header, footer, dialogs, ...)
* and its definition as HTML-String.
*
* @constructs ContentElement
* @memberOf mmir.view
* @public
*
* @param {Array|Object} group
* an array or object with properties <code>name</code> {String}, and <code>content</code> {String}
* @param {Object} view
* the view that owns this ContentElement-element
* @param {mmir.parser.ParserUtils} parser
* for the the content (optional) if supplied this object must have a function <code>parse({String})</code> (see templateParseUtil)
* @param {mmir.parser.RenderUtils} renderer
* for the the content (optional) if supplied, a <code>parser</code> must also be supplied; the renderer must have a function <code>parse({String})</code> (see templateRenderUtil)
*
* @ignore
*/
function ContentElement(group, view, parser, renderer){
/**
* the "localizer" i.e. for handeling internationalization / localized Strings
*
* @protected
* @type mmir.LanguageManager
* @memberOf mmir.view.ContentElement#
* @member localizer
*/
this.localizer = languageManager;
if(arguments.length === 0){
return this;
}
/**
* dummy name, if the ContentElement does not have a name:
* only ContentElements that represent Views and Partials have names -
* other sub-elements (@if,@for etc) do not have their own name/identifier.
*
* TODO externalize as constant
*
* @private
* @constant
* @memberOf mmir.view.ContentElement#
*/
var SUB_ELEMENT_NAME = "@fragment";
/**
* @protected
* @type mmir.parser.ParserUtils
* @memberOf mmir.view.ContentElement#
* @member parser
*/
this.parser = parser;
/**
* @protected
* @type mmir.parser.RenderUtils
* @memberOf mmir.view.ContentElement#
* @member renderer
*/
this.renderer = renderer;
/**
* @protected
* @type mmir.view.View
* @memberOf mmir.view.ContentElement#
* @member view
*/
this.view = view;
if(typeof group.name !== 'undefined' && typeof group.content !== 'undefined'){
this.name = group.name;
//check if the name needs to be converted from a "raw" value:
if(typeof group.getValue === 'function' && typeof group.nameType !== 'undefined'){
this.name = group.getValue(this.name, group.nameType, null);
}
this.definition = group.content;
}
else {
this.name = group[1];
this.definition = group[2];
}
if(typeof group.start !== 'undefined' && typeof group.end !== 'undefined'){
this.start = group.start;
this.end = group.end;
}
if(typeof group.offset !== 'undefined'){
/**
* The offset of the ContentElement's raw String-content
* in relation to its parent ContentElement.
* <p>
* I.e. only when ContentElements are nested with other ContentElements.
* <p>
* For nested ContentElements, the offset always refers to outermost
* ContentElement, e.g.
* <pre>
* content
* ContentElement_1
* ContentElement_2.parentOffset: offset to ContentElement_1
* ...
* ContentElement_i.parentOffset: offset to ContentElement_1</pre>
*
* @type Number
* @private
* @member parentOffset
*/
this.parentOffset = group.offset;
}
else if(typeof group.contentOffset !== 'undefined'){
this.parentOffset = group.contentOffset;
}
else {
this.parentOffset = 0;
}
//if this is a sub-ContentElement (i.e. not directly attached to a view, but to another ContentElement):
// add a reference to its parent ContentElement
if(typeof group.parent !== 'undefined'){
/**
* the parent ContentElement, if this is a sub-ContentElement to another ContentElement
*
* NOTE: this field will only be present, if the ContentElement is initialized from parsing a template
* (i.e. not present when restored for persisted JS view)
* @private
* @type ContentElement
* @member _parent
*/
this._parent = group.parent;
}
/**
* The ParsingResult that represents this ContentElement
*
* @private
* @type mmir.parser.ParsingResult
*/
var parsingResult = parser.parse(this.definition, this);
/**
* The "raw" template text.
*
* @protected
* @type String
* @memberOf mmir.view.ContentElement#
* @member definition
*
*/
this.definition = parsingResult.rawTemplateText;
/**
* List of the "localize" statements in the template.
*
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member localizations
*
* @see mmir.parser.Element.LOCALIZE
*/
this.localizations = parsingResult.localizations;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member escapes
*
* @see mmir.parser.Element.ESCAPE_ENTER
* @see mmir.parser.Element.ESCAPE_EXIT
*/
this.escapes = parsingResult.escapes;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member helpers
*
* @see mmir.parser.Element.HELPER
*/
this.helpers = parsingResult.helpers;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member scriptBlocks
*
* @see mmir.parser.Element.BLOCK
*/
this.scriptBlocks = parsingResult.scriptBlocks;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member scriptStatements
*
* @see mmir.parser.Element.STATEMENT
*/
this.scriptStatements = parsingResult.scriptStatements;
//// this.includeScripts = parsingResult.includeScripts; @see mmir.parser.Element.INCLUDE_SCRIPT
//// this.includeStyles = parsingResult.includeStyles; @see mmir.parser.Element.INCLUDE_STYLE
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member partials
*
* @see mmir.parser.Element.RENDER
*/
this.partials = parsingResult.partials;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member ifs
*
* @see mmir.parser.Element.IF
*/
this.ifs = parsingResult.ifs;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member fors
*
* @see mmir.parser.Element.FOR
*/
this.fors = parsingResult.fors;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
*
* @see mmir.parser.Element.VAR_DECLARATION
*/
this.vars = parsingResult.vars;
/**
* @protected
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
*
* @see mmir.parser.Element.COMMENT
* @member comments
*/
this.comments = parsingResult.comments;
/**
* NOTE by default this field is not added to the allContentElements list (i.e. will not be stored/stringified)
*
* @memberOf mmir.view.ContentElement#
* @protected
* @member yields
*/
this.yields = parsingResult.yields;// @see mmir.parser.Element.YIELD_DECLARATION
// this.contentFors = parsingResult.contentFors; @see mmir.parser.Element.YIELD_CONTENT
/**
* a list of VarReferences that are relevant/active for sub-content elements
* (e.g. content of FOR elements)
*
*
* NOTE: this field is only filled, if the ContentElement is created for parsing a template
* (i.e. not present when restored from a persisted JS view object).
*
* @private
* @type mmir.parser.ParsingResult
* @memberOf mmir.view.ContentElement#
* @member _contentVars
*/
this._contentVars = [];
//create ALL array and sort localizations etc. ...
/**
* create ALL array and sort it, i.e. for localizations etc. ...
* @private
* @type Array<mmir.parser.ParsingResult>
* @memberOf mmir.view.ContentElement#
* @member all
*/
var all = this.localizations.concat(
this.escapes,
this.helpers,
this.scriptBlocks,
this.scriptStatements,
//// this.includeScripts,
//// this.includeStyles,
this.partials,
this.ifs,
this.fors,
this.vars,
this.comments//,
// this.yields,
// this.contentFors
);
/**
* HELPER sorting function -> sort elements by occurrence in raw template text
* @private
* @function sortAscByStart
* @memberOf mmir.view.ContentElement#
*/
var sortAscByStart = function(parsedElem1, parsedElem2){
return parsedElem1.getStart() - parsedElem2.getStart();
};
all.sort(sortAscByStart);
this.allContentElements = all;
/**
* HELPER check if a ContentElement has "dynamic content"
*
* @private
* @function
* @memberOf mmir.view.ContentElement#
*/
var checkHasDynamicContent = function(contentElement){
return (contentElement.localizations && contentElement.localizations.length > 0)
|| (contentElement.helpers && contentElement.helpers.length > 0)
|| (contentElement.scriptBlocks && contentElement.scriptBlocks.length > 0)
|| (contentElement.scriptStatements && contentElement.scriptStatements.length > 0)
|| (contentElement.partials && contentElement.partials.length > 0)
|| (contentElement.ifs && contentElement.ifs.length > 0)
|| (contentElement.fors && contentElement.fors.length > 0)
|| (contentElement.vars && contentElement.vars.length > 0)
//TODO should comments be "pre-rendered", i.e. already removed here, so that they need not be re-evaluated each time a view gets rendered?
|| (contentElement.comments && contentElement.comments.length > 0)
;//TODO if ContentElement supports more dynamic elements (e.g. child-ContentElement objects ...) then add appropriate checks here!
};
//"buffered" field that signifies if this ContentElement has dynamic content
// (--> i.e. has to be evaluated on each rendering, or -if not- can be statically rendered once)
this.internalHasDynamicContent = checkHasDynamicContent(this);
/**
* Error for parsing problems with detailed location information (i.e. where the parsing problem occured).
*
* @property {String} name the error name, that triggered the ScriptEvalError
* @property {String} message the message of the error that triggered the ScriptEvalError
* @property {String} stack the error stack (if available)
*
* @property {String} details the detailed message of the ScriptEvalError including the positional information and the error that triggered it
* @property {Number} offset the offset (number of characters) of the ContentElement where the error occurred (in relation to its parent/owning Element)
* @property {Number} start the starting position for the content (number of characters) within the ContentElement's <code>rawText</code>
* @property {Number} end the end position for the content (number of characters) within the ContentElement's <code>rawText</code>
*
* @class
* @name ScriptEvalError
* @memberOf mmir.view
*/
var ScriptEvalError = function(error, strScript, contentElement, parsingElement){
var err = Error.apply(this, arguments);
err.name = this.name = 'ScriptEvalError';
this.stack = err.stack;
this.message = err.message;
if(typeof this.stack === 'string'){
//remove first line of stack (which would only contain a reference to this constructor)
this.stack = this.stack.replace(/^.*?\r?\n/, this.name + ': ');
}
var offset = 0;
// if(parsingElement.contentOffset){
// console.error('elem.offset: '+parsingElement.contentOffset);
//// offset = parsingElement.contentOffset;
// }
offset += contentElement.getOffset();
this.offset = offset;
this.start = this.offset + parsingElement.getStart();
this.end = this.offset + parsingElement.getEnd();
this.errorDetails = parser_context.parserCreatePrintMessage(
'ContentElement.ScriptEvalError: Error evaluating script '
+JSON.stringify(strScript)
+' for ' + parsingElement.getTypeName()
+' element:\n',
this.message,
this
);
/**
* Get the detailed error message with origin information.
*
* @public
* @function getDetails
* @returns {String} the detailed error message
* @see #details
*
* @memberOf mmir.view.ScriptEvalError#
*/
this.getDetails = function(){
return this.errorDetails;
};
return this;
};
/**
* HELPER: get a list of VarReference ParsingResults from this ContentElement and all its parent ContentElements
* (i.e. all VarReferences that this ContentElement may "need to know about" in order to execute JavaScript code, e.g. template ScriptStatements like @())
*
* @returns {Array<VarReference>} list of VarReference ParsingResults from this ContentElement and all its parent ContentElements
*
* @private
* @function getAllVars
* @memberOf mmir.view.ContentElement#
*/
var getAllVars = function(contentElement){
var vars = contentElement.vars;
var list = vars.slice(0, vars.length);
var parent = contentElement._parent;
while(parent){
list = list.concat(parent.vars, parent._contentVars);
parent = parent._parent;
}
unifyVarList(list, null);
return list;
};
/**
* HELPER: get a list of VarReference ParsingResults from this ContentElement and all its parent ContentElements
* (i.e. all VarReferences that this ContentElement may "need to know about" in order to execute JavaScript code, e.g. template ScriptStatements like @())
*
* @param {Array<VarReference>} varList
* @param {String} [rawText]
* NOTE: if the list contains var-references from parsed JS-text, then the actual var-name must be extracted from the rawText
* (if necessary, the extracted var-name will be attached to the VarReference)
* can be omitted, if no VarReference entries in varList originate for parsing JS-content (i.e. renderer.parseJS())
* @returns {Array<VarReference>} list of VarReference ParsingResults from this ContentElement and all its parent ContentElements
*
* @private
* @function unifyVarList
* @memberOf mmir.view.ContentElement#
*/
var unifyVarList = function(varList, rawText){
var dict = {}, e, val, start;
for(var i=varList.length-1; i >= 0; --i){
e = varList[i];
val = e.getValue(e.name, e.nameType);
//for VarReference from parsed JS code: need to extract the actual var-name:
if(typeof val === 'undefined' && rawText){
start = e.start;
if(rawText.charAt(e.start) === '@'){//omit template-var char
++start;
}
val = rawText.substring(start, e.end);
e.name = e.name || val;
e.nameType = e.nameType || 'Identifier';
} else {
val[0] === '@'? val.substring(1) : val;//normalize var-name if necessary
}
if(dict[val]){
//remove duplicate entry from list;
varList.slice(i,1);
} else {
dict[val] = true;
}
}
}
/**
* HELPER: check whether a var-reference is already present
*
* @param {Array<VarReference>} varList
* @param {String} varName
* the name for the variable (if it starts with "@", it will be removed before checking)
* @returns {Boolean} <code>true</code> if <code>varName</code> corresponds to one of the VarReference entries in <code>varList</code>
*
* @private
* @function isVarInList
* @memberOf mmir.view.ContentElement#
*/
var isVarInList = function(varList, varName){
varName = varName[0] === '@'? varName.substring(1) : varName;
var e, val;
for(var i=varList.length-1; i >= 0; --i){
e = varList[i];
val = e.getValue(e.name, e.nameType);
if(val === varName){
return true;
}
}
return false;
}
/**
* HELPER: this creates function-code for embedded JavaScript code:
* using a function pre-compiles codes - this avoids (re-) parsing the code
* (by the execution environment) each time that the template is rendered.
*
* @param {String} strFuncBody
* the JavaScript code for the function body
* @param {String} strFuncName
* the name for the function
* @returns {PlainObject} an object with the function-code with one input argument (see <code>DATA_NAME</code>) and the function ID/name:
* <code>{func: STRING, funcName: STRING}</code>
*
* @private
* @function createJSEvalCode
* @memberOf mmir.view.ContentElement#
*/
var createJSEvalCode = function(strFuncBody, strFuncName){
//"automatic" export/update for variables
// FIXME this introduces an extra function/function-call which should be avoided
// (NOTE: the function exportRenderDataTo() is generated later in createJSEvalFunction(), see below)
var resultVarName = parser_context.element.DATA_NAME+'RESULT__';
var funcWrapStart = '\nvar '+resultVarName+' = (function(){\n';
var funcWrapEnd = '\n})(); exportRenderDataTo('+parser_context.element.DATA_NAME+'); return ' + resultVarName + ';'
var func = 'function '+strFuncName+'('+parser_context.element.DATA_NAME+'){'+//TODO use strict mode?: +'\n"use strict";\n'
funcWrapStart+strFuncBody+funcWrapEnd+'}';
return {
'func': func,
'funcName': strFuncName
};
};
/**
* HELPER: this creates the initialize-function for the generated script-eval functions
* which will be attached to <code>initEvalFunctions</code>.
*
* The init-function embeds all variables with their "clear name" (i.e. without prefix @)
* so that these can be referenced from within the javascript code (i.e. functions in <code>funcList</code>)
* without additional modifications.
*
* When the init-function is invoked, it will set the generated functions to their corresponding ParsingResult
* in <code>allContentElements</code>.
*
* In addition 2 functions are attached to the ContentElement itself:
* <code>setRenderData(data)</code>: this function must be called with the current render-data,
* each time before rendering the ContentElement
*
* <code>exportRenderData(data)</code>: this function will export the current render-data to the
* data-argument. This function can be used to retrieve the possibly
* modified data after rendering.
*
*
* @param {Array<GenFunc>} funcList
* the list of generated functions, where each entry has the form
* <code>{
* index: Integer: the index of the ParsingResult that contains the function in field allContentElements
* funcName: String: the name of the function in its ParsingResult
* code: the function-code as generated by createJSEvalCode
* }</code>
* @param {Array<VarReference>} templateVars
* the list of template variables (i.e. ParsingResult that encapsules a VarReference)
* @returns {Function} the function that initializes
*
* @private
* @function createJSEvalFunction
* @memberOf mmir.view.ContentElement#
*/
var createJSEvalFunction = function(funcList, templateVars){
//COMMENT: using new Function(..) may yield less performance than eval('function...'),
// since the function-body using the Function(..)-method is re-evaluated on each invocation
// whereas when the eval('function...')-method behaves as if the function was declared statically
// like a normal function-expression (after its first evaluation here).
//
// var func = new Function(parser_context.element.DATA_NAME, strFuncBody);
// func.name = strFuncName;
//create import/export data-argument
// for making fields in data-argument accessible without context-information within the function
// (e.g. instead of "data.theField" -> "theField")
var size=templateVars.length;
var importVarValues, exportVarValues, declVars;
if(size > 0){
importVarValues = [];
exportVarValues = [];
declVars = [];
var v, vname, nvname;
for(var i=0; i < size; ++i){
v = templateVars[i];
vname = v.getValue(v.name, v.nameType);
if(vname[0] === '@'){
nvname = vname.substring(1);
} else {
nvname = vname;
vname = '@' + nvname;
}
importVarValues.push('\n', nvname, ' = ', parser_context.element.DATA_NAME, '["', vname, '"];');
exportVarValues.push('\n', parser_context.element.DATA_NAME, '["', vname, '"] = ', nvname, ';');
declVars.push('\nvar ', nvname, ';');
}
importVarValues = importVarValues.join('') +'\n';
exportVarValues = exportVarValues.join('') + '\n return ' + parser_context.element.DATA_NAME + ';';
declVars = declVars.join('') +'\n';
} else {
importVarValues = '';
exportVarValues = '';
declVars = '';
}
var strFuncName = 'initEvalFunctions';
var el;
var funcs = [
'this.setRenderData = function('+parser_context.element.DATA_NAME+'){'+importVarValues+'};\n',
'var exportRenderDataTo = function('+parser_context.element.DATA_NAME+'){'+exportVarValues+'};\n',
'this.exportRenderDataTo = exportRenderDataTo;\n'
];
for(var i=0,size=funcList.length; i < size; ++i){
el = funcList[i];
funcs.push('this.allContentElements[',el.index, '].', el.funcName, '=', el.code, ';\n');
}
var strFuncBody = funcs.join('');
// //NOTE: need a dummy variable to catch and return the create function-definition in the eval-statement
// // (the awkward 'var dummy=...;dummy'-construction avoids leaking the dummy-var into the
// // global name-space, where the last ';dummy' represent the the return-statement for eval(..) )
var func = eval( 'var '+strFuncName+'=function '+strFuncName+'(){'//TODO use strict mode?: +'\n"use strict";\n'
+declVars+strFuncBody+'};'+strFuncName );
return func;
};
var allVars = getAllVars(this);
//init iter-variables
var i=0,size=0;
var parsedJS = null, preparedJSCode = null, forPropNameRef = null, forListNameRef = null;
var forIterInit = null, forIterFunc = null;
var renderPartialsElement = null, helperElement = null, ifElement = null, forElement = null, subContentElement = null;
//prepare render-partial-elements
for(i=0, size = this.partials.length; i < size; ++i){
renderPartialsElement = this.partials[i];
//for @render(ctrl,name, DATA):
// initialize the DATA-argument, if present:
if( renderPartialsElement.hasCallData() ){
//TODO use original parser/results instead of additional parsing pass
parsedJS = parser.parseJS(
this.definition.substring( renderPartialsElement.getCallDataStart(), renderPartialsElement.getCallDataEnd() ),
'embeddedStatementTail',//<- "internal" parser rule for parsing fragments: >>JS_STATEMENT EOF<<
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then renderPartialsElement.rawResult and .dataPos contain the information, where exactly this element is located...
, renderPartialsElement.getStart() + this.getOffset() + '@render('.length
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true);
try{
renderPartialsElement.argsEval = createJSEvalCode('return ('+preparedJSCode+');', 'argsEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, renderPartialsElement);
//attach a dummy function that prints the error each time it is invoked:
renderPartialsElement.argsEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
}
}
//prepare helper-elements
for(i=0, size = this.helpers.length; i < size; ++i){
helperElement = this.helpers[i];
//for @helper(name, DATA):
// initialize the DATA-argument, if present:
if( helperElement.hasCallData() ){
//TODO use original parser/results instead of additional parsing pass
parsedJS = parser.parseJS(
this.definition.substring( helperElement.getCallDataStart(), helperElement.getCallDataEnd() ),
'embeddedStatementTail',//<- "internal" parser rule for parsing fragments: >>JS_STATEMENT EOF<<
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, helperElement.getStart() + this.getOffset() + '@helper('
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true);
try{
helperElement.argsEval = createJSEvalCode('return ('+preparedJSCode+');', 'argsEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, helperElement);
//attach a dummy function that prints the error each time it is invoked:
helperElement.argsEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
}
}
//prepare if-elements
for(i=0, size = this.ifs.length; i < size; ++i){
ifElement = this.ifs[i];
//TODO use original parser/results instead of additional parsing pass
parsedJS = parser.parseJS(
ifElement.ifExpr,
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, ifElement.getStart() + this.getOffset() + '@if('.length
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences);
try{
ifElement.ifEval = createJSEvalCode('return ('+preparedJSCode+');', 'ifEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, ifElement);
//attach a dummy function that prints the error each time it is invoked:
ifElement.ifEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
}
//gather additional variables that may get "introduced" in for(in)-expressions
var allForVars = allVars;//DISABLED [now always export for-vars to all-vars]: .slice(0, allVars.length);
//prepare for-elements
for(i=0, size = this.fors.length; i < size; ++i){
forElement = this.fors[i];
if(forElement.forControlType === 'FORITER'){
// forElement.forIterationExpr = ...;
// forElement.forObjectExpr = ...;
forPropNameRef = forElement.forControlVarPos[0];
forListNameRef = forElement.forControlVarPos[1];
forElement.forPropName = this.definition.substring(forPropNameRef.getStart(), forPropNameRef.getEnd());
forElement.forListName = this.definition.substring(forListNameRef.getStart(), forListNameRef.getEnd());
//special case FOR-statement: "implicitly declare" property-name variable, if it is not declared yet (i.e. make available for JS code within for-loop)
var normalizedPropName = forElement.forPropName[0] === '@'? forElement.forPropName.substring(1) : forElement.forPropName;
if(!isVarInList(allVars, normalizedPropName) && !isVarInList(this._contentVars, normalizedPropName)){
//add name to ParsingResult, so that there is no need to extract it from raw-template anymore:
forPropNameRef.name = normalizedPropName;
forPropNameRef.nameType = 'Identifier';
this._contentVars.push(forElement.forControlVarPos[0]);
}
//prepend variable-names with template-var-prefix if necessary:
if( ! forElement.forPropName[0] === '@'){
forElement.forPropName = '@' + forElement.forPropName;
}
if( ! forElement.forListName[0] === '@'){
forElement.forListName = '@' + forElement.forListName;
}
forElement.forIterPos = null;
if(!forIterInit){
//the forIteration-function creates a list of all property names for the variable
// given in the FORITER statement
//TODO implement this using iteration-functionality of JavaScript (-> yield)
forIterInit = function (data) {
var list = new Array();
for(var theProp in data[this.forListName]){
list.push(theProp);
}
return list;
};
//creates an iterator for the property-list:
forIterFunc = function (data) {
var iterList = this.forInitEval(data);
var iterIndex = 0;
return {
hasNext : function(){
return iterList.length > iterIndex;
},
next : function(){
return iterList[iterIndex++];
}
};
};
}
forElement.forInitEval = forIterInit;
forElement.forIterator = forIterFunc;
}
else {
//offset within the for-expression
// (-> for locating the init-/condition-/increase-statements in case of an error)
var currentOffset = '@for('.length;//<- "@for("
//TODO remove?
// //list for template-vars: these may increased by "implicit" for-init-variables (see comment below)
// var allForVars = allVars.slice(0, allVars.length);
//TODO use original parser/results instead of additional parsing pass
if(forElement.forInitExpr){
parsedJS = parser.parseJS(
forElement.forInitExpr,
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, forElement.getStart() + this.getOffset() + currentOffset
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true);
currentOffset += forElement.forInitExpr.length;
//special case FOR-statement: if there occur template-vars in init-expression, then "implicitly declare" these (i.e. make available for JS code within for-loop)
var forInitVars = parsedJS.varReferences;
unifyVarList(parsedJS.varReferences, parsedJS.rawTemplateText);
if(forInitVars.length > 0){
//add to current var-list used in for-loop
allForVars = allForVars.concat(forInitVars);
unifyVarList(allForVars);
//add to content-var-list (for child content elements)
this._contentVars = this._contentVars.concat(forInitVars);
unifyVarList(this._contentVars);
}
}
else {
// -> empty init-statement
preparedJSCode = '';
}
try{
forElement.forInitEval = createJSEvalCode(preparedJSCode+';', 'forInitEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, forElement);
//attach a dummy function that prints the error each time it is invoked:
forElement.forInitEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
//increase by 1 for semicolon-separator:
++currentOffset;
if(forElement.forConditionExpr){
parsedJS = parser.parseJS(
forElement.forConditionExpr,
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, forElement.getStart() + this.getOffset() + currentOffset
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true);
currentOffset += forElement.forConditionExpr.length;
}
else {
//-> empty condition-element
preparedJSCode = 'true';
}
try {
forElement.forConditionEval = createJSEvalCode('return ('+preparedJSCode+');', 'forConditionEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, forElement);
//attach a dummy function that prints the error each time it is invoked:
forElement.forConditionEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
//increase by 1 for semicolon-separator:
++currentOffset;
if(forElement.forIncrementExpr){
parsedJS = parser.parseJS(
forElement.forIncrementExpr,
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, forElement.getStart() + this.getOffset() + currentOffset
);
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true);
}
else {
//-> empty "increase" expression
preparedJSCode = '';
}
try{
forElement.forIncrementEval = createJSEvalCode(preparedJSCode+';', 'forIncrementEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, forElement);
//attach a dummy function that prints the error each time it is invoked:
forElement.forIncrementEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
}
}
//(recursively) parse content-fields:
for(i=0, size = all.length; i < size; ++i){
subContentElement = all[i];
if(typeof subContentElement.scriptContent === 'string'){
var isScriptStatement = subContentElement.isScriptStatement();
var parsedJS;
if(isScriptStatement===true){
parsedJS = parser.parseJS(
subContentElement.scriptContent,
'embeddedStatementTail',
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, subContentElement.getStart() + this.getOffset() + '@('.length
);
}
else {
parsedJS = parser.parseJS(
subContentElement.scriptContent,
this//TODO supply/implement more accurate error-localization: this is indeed wrong, since it is not the view-defintion, but: this.definition=<view's contentFor>, then helperElement.rawResult and .dataPos contain the information, where exactly this element is located...
, subContentElement.getStart() + this.getOffset() + '@{'.length
);
}
subContentElement.scriptContent = parsedJS;
preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences);
if(isScriptStatement===true){
preparedJSCode = 'return ('+preparedJSCode+');';
}
try{
subContentElement.scriptEval = createJSEvalCode(preparedJSCode, 'scriptEval');
} catch (err){
var error = new ScriptEvalError(err, preparedJSCode, this, subContentElement);
//attach a dummy function that prints the error each time it is invoked:
subContentElement.scriptEval = function(){ console.error(error.getDetails()); };
//... and print the error now, too:
console.error(error.getDetails());
}
this.internalHasDynamicContent = true;
}
//recursively parse template-content by creating sub-ContentElements:
if(typeof subContentElement.content === 'string'){
subContentElement.content = new ContentElement({
name: SUB_ELEMENT_NAME,
content: subContentElement.content,
offset: this.getOffset() + subContentElement.contentOffset,
parent: this
}, view, parser, renderer
);
this.internalHasDynamicContent = this.internalHasDynamicContent || subContentElement.content.hasDynamicContent();
}
//IF-elements can have an additional ELSE-content field:
if(subContentElement.hasElse() && typeof subContentElement.elseContent.content === 'string'){
subContentElement.elseContent.content = new ContentElement({
name: SUB_ELEMENT_NAME,
content: subContentElement.elseContent.content,
offset: this.getOffset() + subContentElement.elseContent.contentOffset,
parent: this
}, view, parser, renderer
);
this.internalHasDynamicContent = this.internalHasDynamicContent || subContentElement.elseContent.content.hasDynamicContent();
}
}
//initialize generated functions
//(recursively) parse content-fields:
//1. gather generated functions (TODO create list when generating functions)
var funcs = [];
var reFuncTest = /Eval$/;//<- by convention the functions have the suffix 'Eval'
var prop;
for(i=0, size = all.length; i < size; ++i){
subContentElement = all[i];
//TODO avoid for(n in obj)
for(pname in subContentElement){
if(reFuncTest.test(pname) && subContentElement.hasOwnProperty(pname)){
prop = subContentElement[pname];
if(prop.func && prop.funcName){
funcs.push({index: i, funcName: pname, code: prop.func});
}
}
}
}
//2. create the (eval'ed) initializer-function for the generated function code:
this.initEvalFunctions = createJSEvalFunction(funcs, allVars);
//3. invoke initialzer-function and set the generated functions to their ParsingResult elements
this.initEvalFunctions();
return this;
}
/**
* Gets the name of a {@link mmir.view.ContentElement} object (content, header, footer, dialogs, ...).
*
* @function getName
* @returns {String} Name - used by yield tags in layout
* @public
* @memberOf mmir.view.ContentElement#
*/
ContentElement.prototype.getName = function(){
return this.name;
};
/**
* Gets the owner for this ContentElement, i.e. the {@link mmir.view.View} object.
*
* @function getView
* @returns {mmir.view.View} the owning View
* @public
* @memberOf mmir.view.ContentElement#
*/
ContentElement.prototype.getView = function(){
return this.view;
};
/**
* Gets the controller for this ContentElement.
*
* @function
* @returns {mmir.Controller} the Controller of the owning view
* @public
*/
ContentElement.prototype.getController = function(){
return this.getView().getController();
};
/**
* Gets the definition of a {@link mmir.view.ContentElement} object.
*
* TODO remove this?
*
* @function
* @returns {String} The HTML content.
* @public
*/
ContentElement.prototype.toHtml = function(){
// return this.definition;
return this.toStrings().join('');
};
/**
* Renders this object into the renderingBuffer.
*
* @param renderingBuffer {Array} of Strings (if <code>null</code> a new buffer will be created)
* @param data {Any} (optional) the event data with which the rendering was invoked
* @returns {Array<String>} of Strings the renderingBuffer with the contents of this object added at the end
*
* @public
*/
ContentElement.prototype.toStrings = function(renderingBuffer, data){
return this.renderer.renderContentElement(this, data, renderingBuffer);
};
/**
* @public
* @returns {String} the raw text from which this content element was parsed
* @see #getDefinition
*
* @public
*/
ContentElement.prototype.getRawText = function(){
return this.definition;
};
/**
* @deprecated use {@link #getRawText} instead
* @returns {String} the raw text from which this content element was parsed
* @see #getRawText
*
* @public
*/
ContentElement.prototype.getDefinition = function(){
return this.definition;
};
/**
* @returns {Number} the start position for this content Element within {@link #getRawText}
* @public
*/
ContentElement.prototype.getStart = function(){
return this.start;
};
/**
* @returns {Number} the end position for this content Element within {@link #getRawText}
* @public
*/
ContentElement.prototype.getEnd = function(){
return this.end;
};
//FIXME add to storage? (this should only be relevant for parsing, which is not necessary in case of store/restore...)
ContentElement.prototype.getOffset = function(){
return this.parentOffset;
};
/**
* @returns {Boolean} returns <code>true</code> if this ContentElement conatains dynamic content,
* i.e. if it needs to be "evaluated" for rendering
* (otherwise, its plain text representation can be used for rendering)
* @public
*/
ContentElement.prototype.hasDynamicContent = function(){
return this.internalHasDynamicContent;
};
/**
* create a String representation for this content element.
* @returns {String} the string-representation
* @public
*
* @requires StorageUtils
* @requires RenderUtils
*/
ContentElement.prototype.stringify = function(disableStrictMode){
//TODO use constants for lists
//primitive-type properties:
// write values 'as is' for these properties
var propList = [
'name',
'definition',
'start',
'end',
'parentOffset',
'internalHasDynamicContent'
];
//Array-properties
var arrayPropList = [
'allContentElements' //element type: ParsingResult (stringify-able)
];
// //SPECIAL: store view by getter function initView: use the view's name view {mmir.view.View} -> 'viewName' {String}, 'ctrlName' {String}
//
// //USED BY RENDERER:
//// allContentElements
//// definition
//// getRawText() == definition
//// getController() (by view)
//
// //SPECIAL: store renderer by getter function initRenderer
//
// //function properties:
// var funcPropList = [
// 'initView',
// 'initRenderer'
// ];
//function for iterating over the property-list and generating JSON-like entries in the string-buffer
var appendStringified = parser_context.appendStringified;
var sb = ['require("mmirf/storageUtils").restoreObject({ classConstructor: "mmirf/contentElement"', ','];
appendStringified(this, propList, sb);
//non-primitives array-properties with stringify() function:
appendStringified(this, arrayPropList, sb, null, function arrayValueExtractor(name, arrayValue){
var buf =['['];
for(var i=0, size = arrayValue.length; i < size; ++i){
buf.push(arrayValue[i].stringify(disableStrictMode));
buf.push(',');
}
//remove last comma
if(arrayValue.length > 0){
buf.splice( buf.length - 1, 1);
}
buf.push(']');
return buf.join('');
});
if(this.initEvalFunctions) sb.push( 'initEvalFunctions: ',this.initEvalFunctions.toString(),',');//MOD glob vars
//TODO is there a better way to store the view? -> by its name and its contoller's name, and add a getter function...
if(this['view']){
//getter/setter function for the view/controller
// (NOTE: needs to be called before view/controller can be accessed!)
sb.push( 'initView: function(){');
// store view-name:
sb.push( ' var viewName = ');
sb.push( JSON.stringify(this.getView().getName()) );
// store controller-name:
sb.push( '; var ctrlName = ');
sb.push( JSON.stringify(this.getController().getName()) );
// ... and the getter/setter code:
sb.push( '; this.view = require("mmirf/presentationManager").get');
sb.push(this['view'].constructor.name);//<- insert getter-name dependent on the view-type (e.g. View, Partial)
sb.push('(ctrlName, viewName); this.getView = function(){return this.view;}; return this.view; },' );
sb.push( 'getView: function(){ return this.initView();}');
//NOTE: need to add comma in a separate entry
// (-> in order to not break the removal method of last comma, see below)
sb.push( ',' );
}
//TODO is there a better way to store the renderer? -> by a getter function...
if(this['renderer']){
//getter/setter function for the (default) renderer
// (NOTE: needs to be called before view/controller can be accessed!)
sb.push( 'initRenderer: function(){');
// ... and the getter/setter code:
sb.push( ' this.renderer = require("mmirf/renderUtils"); }' );
//NOTE: need to add comma in a separate entry
// (-> in order to not break the removal method of last comma, see below)
sb.push( ',' );
}
if(this['renderer'] || this['view'] || this['this.initEvalFunctions']){
//add initializer function
// (NOTE: needs to be called before view/controller or renderer can be accessed!)
sb.push( 'init: function(){');
if(this['renderer']){
sb.push( ' this.initRenderer(); ' );
}
// if(this['view']){
// sb.push( ' this.initView(); ' );
// }
if(this['initEvalFunctions']){//MOD glob vars
sb.push(' this.initEvalFunctions(); ');
}
sb.push( ' }' );
//NOTE: need to add comma in a separate entry
// (-> in order to not break the removal method of last comma, see below)
sb.push( ',' );
}
//if last element is a comma, remove it
if(sb[sb.length - 1] === ','){
sb.splice( sb.length - 1, 1);
}
sb.push(' })');
return sb.join('');
};
return ContentElement;
});//END: define(..., function(){