UNPKG

mmir-lib

Version:

MMIR (Mobile Multimodal Interaction and Relay) library

1,301 lines (1,135 loc) 46.9 kB
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(){