1 /* 2 * Copyright (C) 2012-2013 DFKI GmbH 3 * Deutsches Forschungszentrum fuer Kuenstliche Intelligenz 4 * German Research Center for Artificial Intelligence 5 * http://www.dfki.de 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a 8 * copy of this software and associated documentation files (the 9 * "Software"), to deal in the Software without restriction, including 10 * without limitation the rights to use, copy, modify, merge, publish, 11 * distribute, sublicense, and/or sell copies of the Software, and to 12 * permit persons to whom the Software is furnished to do so, subject to 13 * the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included 16 * in all copies or substantial portions of the Software. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 */ 26 27 define(['languageManager', 'parserModule', 'storageUtils'], 28 //this comment is needed by jsdoc2 [copy of comment for: function ContentElement(...] 29 /** 30 * The ContentElement represents "content" parts of a view; it may itself contain one or more ContentElements. 31 * 32 * This class holds the name of the content-field (used via the yield-tag in the layouts: content, header, footer, dialogs, ...) 33 * and its definition as HTML-String. 34 * 35 * @class 36 * @name ContentElement 37 * @public 38 * 39 * @param {Array|Object} group 40 * an array or object with properties <code>name</code> {String}, and <code>content</code> {String} 41 * @param {Object} view 42 * the view that owns this ContentElement-element 43 * @param {mmir.parser.ParserUtils} parser 44 * for the the content (optional) if supplied this object must have a function <code>parse({String})</code> (see templateParseUtil) 45 * @param {mmir.parser.RenderUtils} renderer 46 * 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) 47 * 48 */ 49 function( 50 languageManager, parser_context 51 ){//NOTE: dependency storageUtils is actually accessed through parser_context (i.e. it attaches its functions to parserModule) 52 53 /** @scope ContentElement.prototype *///for jsdoc2 54 55 //set to @ignore in order to avoid doc-duplication in jsdoc3 56 /** 57 * @ignore 58 * 59 * The ContentElement represents "content" parts of a view; it may itself contain one or more ContentElements. 60 * 61 * This class holds the name of the content-field (used via the yield-tag in the layouts: content, header, footer, dialogs, ...) 62 * and its definition as HTML-String. 63 * 64 * @constructs ContentElement 65 * @public 66 * 67 * @param {Array|Object} group 68 * an array or object with properties <code>name</code> {String}, and <code>content</code> {String} 69 * @param {Object} view 70 * the view that owns this ContentElement-element 71 * @param {mmir.parser.ParserUtils} parser 72 * for the the content (optional) if supplied this object must have a function <code>parse({String})</code> (see templateParseUtil) 73 * @param {mmir.parser.RenderUtils} renderer 74 * 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) 75 * 76 */ 77 function ContentElement(group, view, parser, renderer){ 78 79 /** 80 * the "localizer" i.e. for handeling internationalization / localized Strings 81 * 82 * @protected 83 * @type mmir.LanguageManager 84 * @memberOf ContentElement# 85 */ 86 this.localizer = languageManager; 87 88 if(arguments.length === 0){ 89 return this; 90 } 91 92 93 /** 94 * dummy name, if the ContentElement does not have a name: 95 * only ContentElements that represent Views and Partials have names - 96 * other sub-elements (@if,@for etc) do not have their own name/identifier. 97 * 98 * TODO externalize as constant 99 * 100 * @private 101 * @constant 102 * @memberOf ContentElement# 103 */ 104 var SUB_ELEMENT_NAME = "@fragment"; 105 106 this.parser = parser; 107 this.renderer = renderer; 108 this.view = view; 109 110 if(typeof group.name !== 'undefined' && typeof group.content !== 'undefined'){ 111 this.name = group.name; 112 113 //check if the name needs to be converted from a "raw" value: 114 if(typeof group.getValue === 'function' && typeof group.nameType !== 'undefined'){ 115 this.name = group.getValue(this.name, group.nameType, null); 116 } 117 118 this.definition = group.content; 119 } 120 else { 121 this.name = group[1]; 122 this.definition = group[2]; 123 } 124 125 if(typeof group.start !== 'undefined' && typeof group.end !== 'undefined'){ 126 this.start = group.start; 127 this.end = group.end; 128 } 129 130 if(typeof group.offset !== 'undefined'){ 131 /** 132 * The offset of the ContentElement's raw String-content 133 * in relation to its parent ContentElement. 134 * <p> 135 * I.e. only when ContentElements are nested with other ContentElements. 136 * <p> 137 * For nested ContentElements, the offset always refers to outermost 138 * ContentElement, e.g. 139 * <pre> 140 * content 141 * ContentElement_1 142 * ContentElement_2.parentOffset: offset to ContentElement_1 143 * ... 144 * ContentElement_i.parentOffset: offset to ContentElement_1</pre> 145 * 146 * @type Number 147 * @private 148 */ 149 this.parentOffset = group.offset; 150 } 151 else if(typeof group.contentOffset !== 'undefined'){ 152 153 this.parentOffset = group.contentOffset; 154 } 155 else { 156 this.parentOffset = 0; 157 } 158 159 /** 160 * The ParsingResult that represents this ContentElement 161 * 162 * @private 163 * @type mmir.parser.ParsingResult 164 * @memberOf ContentElement# 165 */ 166 var parsingResult = parser.parse(this.definition, this); 167 /** 168 * The "raw" template text. 169 * 170 * @protected 171 * @type String 172 * @memberOf ContentElement# 173 * 174 */ 175 this.definition = parsingResult.rawTemplateText; 176 /** 177 * List of the "localize" statements in the template. 178 * 179 * @protected 180 * @type mmir.parser.ParsingResult 181 * @memberOf ContentElement# 182 * 183 * @see mmir.parser.element.LOCALIZE 184 */ 185 this.localizations = parsingResult.localizations; 186 /** 187 * @protected 188 * @type mmir.parser.ParsingResult 189 * @memberOf ContentElement# 190 * 191 * @see mmir.parser.element.ESCAPE_ENTER 192 * @see mmir.parser.element.ESCAPE_EXIT 193 */ 194 this.escapes = parsingResult.escapes; 195 /** 196 * @protected 197 * @type mmir.parser.ParsingResult 198 * @memberOf ContentElement# 199 * 200 * @see mmir.parser.element.HELPER 201 */ 202 this.helpers = parsingResult.helpers; 203 /** 204 * @protected 205 * @type mmir.parser.ParsingResult 206 * @memberOf ContentElement# 207 * 208 * @see mmir.parser.element.BLOCK 209 */ 210 this.scriptBlocks = parsingResult.scriptBlocks; 211 /** 212 * @protected 213 * @type mmir.parser.ParsingResult 214 * @memberOf ContentElement# 215 * 216 * @see mmir.parser.element.STATEMENT 217 */ 218 this.scriptStatements = parsingResult.scriptStatements; 219 //// this.includeScripts = parsingResult.includeScripts; @see mmir.parser.element.INCLUDE_SCRIPT 220 //// this.includeStyles = parsingResult.includeStyles; @see mmir.parser.element.INCLUDE_STYLE 221 /** 222 * @protected 223 * @type mmir.parser.ParsingResult 224 * @memberOf ContentElement# 225 * 226 * @see mmir.parser.element.RENDER 227 */ 228 this.partials = parsingResult.partials; 229 /** 230 * @protected 231 * @type mmir.parser.ParsingResult 232 * @memberOf ContentElement# 233 * 234 * @see mmir.parser.element.IF 235 */ 236 this.ifs = parsingResult.ifs; 237 /** 238 * @protected 239 * @type mmir.parser.ParsingResult 240 * @memberOf ContentElement# 241 * 242 * @see mmir.parser.element.FOR 243 */ 244 this.fors = parsingResult.fors; 245 /** 246 * @protected 247 * @type mmir.parser.ParsingResult 248 * @memberOf ContentElement# 249 * 250 * @see mmir.parser.element.VAR_DECLARATION 251 */ 252 this.vars = parsingResult.vars; 253 /** 254 * @protected 255 * @type mmir.parser.ParsingResult 256 * @memberOf ContentElement# 257 * 258 * @see mmir.parser.element.COMMENT 259 */ 260 this.comments = parsingResult.comments; 261 262 // this.yields = parsingResult.yields; @see mmir.parser.element.YIELD_DECLARATION 263 // this.contentFors = parsingResult.contentFors; @see mmir.parser.element.YIELD_CONTENT 264 265 //create ALL array and sort localizations etc. ... 266 /** 267 * create ALL array and sort it, i.e. for localizations etc. ... 268 * @private 269 * @type Array<mmir.parser.ParsingResult> 270 * @memberOf ContentElement# 271 */ 272 var all = this.localizations.concat( 273 this.escapes, 274 this.helpers, 275 this.scriptBlocks, 276 this.scriptStatements, 277 //// this.includeScripts, 278 //// this.includeStyles, 279 this.partials, 280 this.ifs, 281 this.fors, 282 this.vars, 283 this.comments//, 284 // this.yields, 285 // this.contentFors 286 ); 287 288 /** 289 * HELPER sorting function -> sort elements by occurrence in raw template text 290 * @private 291 * @function 292 * @memberOf ContentElement# 293 */ 294 var sortAscByStart = function(parsedElem1, parsedElem2){ 295 return parsedElem1.getStart() - parsedElem2.getStart(); 296 }; 297 all.sort(sortAscByStart); 298 299 this.allContentElements = all; 300 301 /** 302 * HELPER check if a ContentElement has "dynamic content" 303 * 304 * @private 305 * @function 306 * @memberOf ContentElement# 307 */ 308 var checkHasDynamicContent = function(contentElement){ 309 return (contentElement.localizations && contentElement.localizations.length > 0) 310 || (contentElement.helpers && contentElement.helpers.length > 0) 311 || (contentElement.scriptBlocks && contentElement.scriptBlocks.length > 0) 312 || (contentElement.scriptStatements && contentElement.scriptStatements.length > 0) 313 || (contentElement.partials && contentElement.partials.length > 0) 314 || (contentElement.ifs && contentElement.ifs.length > 0) 315 || (contentElement.fors && contentElement.fors.length > 0) 316 || (contentElement.vars && contentElement.vars.length > 0) 317 //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? 318 || (contentElement.comments && contentElement.comments.length > 0) 319 ;//TODO if ContentElement supports more dynamic elements (e.g. child-ContentElement objects ...) then add appropriate checks here! 320 }; 321 322 //"buffered" field that signifies if this ContentElement has dynamic content 323 // (--> i.e. has to be evaluated on each rendering, or -if not- can be statically rendered once) 324 this.internalHasDynamicContent = checkHasDynamicContent(this); 325 326 /** 327 * Error for parsing problems with detailed location information (i.e. where the parsing problem occured). 328 * 329 * @property {String} name the error name, that triggered the ScriptEvalError 330 * @property {String} message the message of the error that triggered the ScriptEvalError 331 * @property {String} stack the error stack (if available) 332 * 333 * @property {String} details the detailed message of the ScriptEvalError including the positional information and the error that triggered it 334 * @property {Number} offset the offset (number of characters) of the ContentElement where the error occurred (in relation to its parent/owning Element) 335 * @property {Number} start the starting position for the content (number of characters) within the ContentElement's <code>rawText</code> 336 * @property {Number} end the end position for the content (number of characters) within the ContentElement's <code>rawText</code> 337 * 338 * @class 339 * @name ScriptEvalError 340 */ 341 var ScriptEvalError = function(error, strScript, contentElement, parsingElement){ 342 343 var err = Error.apply(this, arguments); 344 err.name = this.name = 'ScriptEvalError'; 345 346 this.stack = err.stack; 347 this.message = err.message; 348 349 if(typeof this.stack === 'string'){ 350 //remove first line of stack (which would only contain a reference to this constructor) 351 this.stack = this.stack.replace(/^.*?\r?\n/, this.name + ': '); 352 } 353 354 var offset = 0; 355 // if(parsingElement.contentOffset){ 356 // console.error('elem.offset: '+parsingElement.contentOffset); 357 //// offset = parsingElement.contentOffset; 358 // } 359 offset += contentElement.getOffset(); 360 361 this.offset = offset; 362 363 this.start = this.offset + parsingElement.getStart(); 364 this.end = this.offset + parsingElement.getEnd(); 365 366 this.errorDetails = parser_context.parserCreatePrintMessage( 367 'ContentElement.ScriptEvalError: Error evaluating script ' 368 +JSON.stringify(strScript) 369 +' for ' + parsingElement.getTypeName() 370 +' element:\n', 371 this.message, 372 this 373 ); 374 375 /** 376 * Get the detailed error message with origin information. 377 * 378 * @public 379 * @returns {String} the detailed error message 380 * @see #details 381 * 382 * @var {Function} ScriptEvalError#getDetails 383 */ 384 this.getDetails = function(){ 385 return this.errorDetails; 386 }; 387 388 return this; 389 }; 390 391 /** 392 * HELPER: this creates a function for embedded JavaScript code: 393 * using a function pre-compiles codes - this avoids (re-) parsing the code 394 * (by the execution environment) each time that the template is rendered. 395 * 396 * @param {String} strFuncBody 397 * the JavaScript code for the function body 398 * @param {String} strFuncName 399 * the name for the function 400 * @returns {Function} the evaluated function with one input argument (see <code>DATA_NAME</code>) 401 * 402 * @private 403 * @function 404 * @memberOf ContentElement# 405 */ 406 var createJSEvalFunction = function(strFuncBody, strFuncName){ 407 408 //COMMENT: using new Function(..) may yield less performance than eval('function...'), 409 // since the function-body using the Function(..)-method is re-evaluated on each invocation 410 // whereas when the eval('function...')-method behaves as if the function was declared statically 411 // like a normal function-expression (after its first evaluation here). 412 // 413 // var func = new Function(parser_context.element.DATA_NAME, strFuncBody); 414 // func.name = strFuncName; 415 416 417 // //TEST use import/export VARs instead of data-object access: 418 // // 419 // //IMPORT 420 // // * make properties of DATA available as local variables 421 // // * synchronize the DATA properties to local variables (with property getters/setters) 422 // //EXPORT 423 // // * on exit: commit values of local variables to their corresponding DATA-fields (and remove previously set "sync"-code) 424 // // 425 // var dataFieldName = parser_context.element.DATA_NAME; 426 // 427 // //TODO do static "import" without eval(): only import VARs that were declared by @var() before! 428 // // ... also (OPTIMIZATION): during JS-parsing, gather/detect VARIABLE occurrences -> only import VAR if it gets "mentioned" in the func-body! (need to detect arguments vs. variables for this!) 429 // var iteratorName = '__$$ITER$$__';//<- iterator name (for iterating over DATA fields) 430 // var varIteratorStartSrc = 'for(var '+iteratorName+' in '+dataFieldName+'){\ 431 // if('+dataFieldName+'.hasOwnProperty('+iteratorName+')){';//<- TODO? add check, if field-name starts with @? 432 // var varIteratorEndSrc = '}}'; 433 // 434 // var importDataSrc = varIteratorStartSrc 435 // //create local variable, initialized with the DATA's value 436 // + 'eval("var "+'+iteratorName+'.substring(1)+" = '+dataFieldName+'[\'"+'+iteratorName+'+"\'];");' 437 // //"synchronize" the DATA object to to the created local variable 438 // + 'Object.defineProperty('+dataFieldName+', '+iteratorName+',{\ 439 // configurable : true,\ 440 // enumerable : true,\ 441 // set: eval("var dummy1 = function set(value){\\n "+'+iteratorName+'.substring(1)+" = value;\\n };dummy1"),\ 442 // get: eval("var dummy2 = function get(){\\n return "+'+iteratorName+'.substring(1)+";\\n };dummy2")\ 443 // });' 444 // + varIteratorEndSrc; 445 // 446 // //TODO do not define "export" with the function itself, since there may be problems due to return statements etc. 447 // // ... instead: do the "export" after the function was invoked, i.e. obj.evalScript() etc. in renderer 448 // var exportDataSrc = varIteratorStartSrc 449 // //DISABLED: use defineProperty() instead (see below) ... this would use the "proxy"/"sync" mechanism.. 450 //// + 'eval("'+dataFieldName+'[\'"+'+iteratorName+'+"\'] = "+'+iteratorName+'.substring(1)+";");' 451 // 452 // //reset to DATA property to normal behavior 453 // // i.e. remove proxy-behavior by removing the getter/setter 454 // // and setting to current value 455 // + 'Object.defineProperty('+dataFieldName+', '+iteratorName+',{\ 456 // value : '+dataFieldName+'['+iteratorName+'],\ 457 // writable : true,\ 458 // configurable : true,\ 459 // enumerable : true\ 460 // });' 461 // + varIteratorEndSrc; 462 // 463 // var func = eval( 'var dummy=function '+strFuncName+'('+parser_context.element.DATA_NAME+'){' 464 // + importDataSrc + strFuncBody +';'+exportDataSrc+'};dummy' );//<- FIXME WARING: export does not work correctly, if there is a return-statement in the outermost scope of the strFuncBody! 465 466 467 // //NOTE: need a dummy variable to catch and return the create function-definition in the eval-statement 468 // // (the awkward 'var dummy=...;dummy'-construction avoids leaking the dummy-var into the 469 // // global name-space, where the last ';dummy' represent the the return-statement for eval(..) ) 470 var func = eval( 'var dummy=function '+strFuncName+'('+parser_context.element.DATA_NAME+'){'+strFuncBody+'};dummy' ); 471 472 return func; 473 }; 474 475 //init iter-variables 476 var i=0,size=0; 477 var parsedJS = null, preparedJSCode = null, forPropNameRef = null, forListNameRef = null; 478 var forIterInit = null, forIterFunc = null; 479 var renderPartialsElement = null, helperElement = null, ifElement = null, forElement = null, subContentElement = null; 480 481 //prepare render-partial-elements 482 for(i=0, size = this.partials.length; i < size; ++i){ 483 renderPartialsElement = this.partials[i]; 484 485 //for @render(ctrl,name, DATA): 486 // initialize the DATA-argument, if present: 487 if( renderPartialsElement.hasCallData() ){ 488 //TODO use original parser/results instead of additional parsing pass 489 parsedJS = parser.parseJS( 490 this.definition.substring( renderPartialsElement.getCallDataStart(), renderPartialsElement.getCallDataEnd() ), 491 'embeddedStatementTail',//<- "internal" parser rule for parsing fragments: >>JS_STATEMENT EOF<< 492 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... 493 , renderPartialsElement.getStart() + this.getOffset() + '@render('.length 494 ); 495 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true); 496 497 try{ 498 renderPartialsElement.argsEval = createJSEvalFunction('return ('+preparedJSCode+');', 'argsEval'); 499 } catch (err){ 500 var error = new ScriptEvalError(err, preparedJSCode, this, renderPartialsElement); 501 //attach a dummy function that prints the error each time it is invoked: 502 renderPartialsElement.argsEval = function(){ console.error(error.getDetails()); }; 503 //... and print the error now, too: 504 console.error(error.getDetails()); 505 } 506 } 507 } 508 509 //prepare helper-elements 510 for(i=0, size = this.helpers.length; i < size; ++i){ 511 helperElement = this.helpers[i]; 512 513 //for @helper(name, DATA): 514 // initialize the DATA-argument, if present: 515 if( helperElement.hasCallData() ){ 516 //TODO use original parser/results instead of additional parsing pass 517 parsedJS = parser.parseJS( 518 this.definition.substring( helperElement.getCallDataStart(), helperElement.getCallDataEnd() ), 519 'embeddedStatementTail',//<- "internal" parser rule for parsing fragments: >>JS_STATEMENT EOF<< 520 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... 521 , helperElement.getStart() + this.getOffset() + '@helper(' 522 ); 523 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true); 524 525 try{ 526 helperElement.argsEval = createJSEvalFunction('return ('+preparedJSCode+');', 'argsEval'); 527 } catch (err){ 528 var error = new ScriptEvalError(err, preparedJSCode, this, helperElement); 529 //attach a dummy function that prints the error each time it is invoked: 530 helperElement.argsEval = function(){ console.error(error.getDetails()); }; 531 //... and print the error now, too: 532 console.error(error.getDetails()); 533 } 534 } 535 } 536 537 //prepare if-elements 538 for(i=0, size = this.ifs.length; i < size; ++i){ 539 ifElement = this.ifs[i]; 540 541 //TODO use original parser/results instead of additional parsing pass 542 parsedJS = parser.parseJS( 543 ifElement.ifExpr, 544 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... 545 , ifElement.getStart() + this.getOffset() + '@if('.length 546 ); 547 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences); 548 549 try{ 550 ifElement.ifEval = createJSEvalFunction('return ('+preparedJSCode+');', 'ifEval'); 551 } catch (err){ 552 var error = new ScriptEvalError(err, preparedJSCode, this, ifElement); 553 //attach a dummy function that prints the error each time it is invoked: 554 ifElement.ifEval = function(){ console.error(error.getDetails()); }; 555 //... and print the error now, too: 556 console.error(error.getDetails()); 557 } 558 } 559 560 //prepare for-elements 561 for(i=0, size = this.fors.length; i < size; ++i){ 562 forElement = this.fors[i]; 563 564 if(forElement.forControlType === 'FORITER'){ 565 566 // forElement.forIterationExpr = ...; 567 // forElement.forObjectExpr = ...; 568 569 forPropNameRef = forElement.forControlVarPos[0]; 570 forListNameRef = forElement.forControlVarPos[1]; 571 572 forElement.forPropName = this.definition.substring(forPropNameRef.getStart(), forPropNameRef.getEnd()); 573 forElement.forListName = this.definition.substring(forListNameRef.getStart(), forListNameRef.getEnd()); 574 575 //prepend variable-names with template-var-prefix if necessary: 576 if( ! forElement.forPropName.startsWith('@')){ 577 forElement.forPropName = '@' + forElement.forPropName; 578 } 579 if( ! forElement.forListName.startsWith('@')){ 580 forElement.forListName = '@' + forElement.forListName; 581 } 582 583 forElement.forIterPos = null; 584 585 if(!forIterInit){ 586 587 //the forIteration-function creates a list of all property names for the variable 588 // given in the FORITER statement 589 590 forIterInit = function (data) { 591 //TODO implement this using iteration-functionality of JavaScript (-> yield) 592 var list = new Array(); 593 for(var theProp in data[this.forListName]){ 594 list.push(theProp); 595 } 596 return list; 597 }; 598 599 //creates an iterator for the property-list: 600 forIterFunc = function (data) { 601 var iterList = this.forInitEval(data); 602 var iterIndex = 0; 603 return { 604 hasNext : function(){ 605 return iterList.length > iterIndex; 606 }, 607 next : function(){ 608 return iterList[iterIndex++]; 609 } 610 }; 611 }; 612 } 613 614 forElement.forInitEval = forIterInit; 615 forElement.forIterator = forIterFunc; 616 } 617 else { 618 619 //offset within the for-expression 620 // (-> for locating the init-/condition-/increase-statements in case of an error) 621 var currentOffset = '@for('.length;//<- "@for(" 622 623 //TODO use original parser/results instead of additional parsing pass 624 if(forElement.forInitExpr){ 625 parsedJS = parser.parseJS( 626 forElement.forInitExpr, 627 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... 628 , forElement.getStart() + this.getOffset() + currentOffset 629 ); 630 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true); 631 632 currentOffset += forElement.forInitExpr.length; 633 } 634 else { 635 // -> empty init-statement 636 preparedJSCode = ''; 637 } 638 try{ 639 forElement.forInitEval = createJSEvalFunction(preparedJSCode+';', 'forInitEval'); 640 } catch (err){ 641 var error = new ScriptEvalError(err, preparedJSCode, this, forElement); 642 //attach a dummy function that prints the error each time it is invoked: 643 forElement.forInitEval = function(){ console.error(error.getDetails()); }; 644 //... and print the error now, too: 645 console.error(error.getDetails()); 646 } 647 648 //increase by 1 for semicolon-separator: 649 ++currentOffset; 650 651 if(forElement.forConditionExpr){ 652 parsedJS = parser.parseJS( 653 forElement.forConditionExpr, 654 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... 655 , forElement.getStart() + this.getOffset() + currentOffset 656 ); 657 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true); 658 659 currentOffset += forElement.forConditionExpr.length; 660 } 661 else { 662 //-> empty condition-element 663 preparedJSCode = 'true'; 664 } 665 try { 666 forElement.forConditionEval = createJSEvalFunction('return ('+preparedJSCode+');', 'forConditionEval'); 667 } catch (err){ 668 var error = new ScriptEvalError(err, preparedJSCode, this, forElement); 669 //attach a dummy function that prints the error each time it is invoked: 670 forElement.forConditionEval = function(){ console.error(error.getDetails()); }; 671 //... and print the error now, too: 672 console.error(error.getDetails()); 673 } 674 675 676 //increase by 1 for semicolon-separator: 677 ++currentOffset; 678 679 if(forElement.forIncrementExpr){ 680 parsedJS = parser.parseJS( 681 forElement.forIncrementExpr, 682 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... 683 , forElement.getStart() + this.getOffset() + currentOffset 684 ); 685 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences, true); 686 } 687 else { 688 //-> empty "increase" expression 689 preparedJSCode = ''; 690 } 691 692 try{ 693 forElement.forIncrementEval = createJSEvalFunction(preparedJSCode+';', 'forIncrementEval'); 694 } catch (err){ 695 var error = new ScriptEvalError(err, preparedJSCode, this, forElement); 696 //attach a dummy function that prints the error each time it is invoked: 697 forElement.forIncrementEval = function(){ console.error(error.getDetails()); }; 698 //... and print the error now, too: 699 console.error(error.getDetails()); 700 } 701 } 702 } 703 704 //recursively parse content-fields: 705 for(i=0, size = all.length; i < size; ++i){ 706 subContentElement = all[i]; 707 708 if(typeof subContentElement.scriptContent === 'string'){ 709 710 var isScriptStatement = subContentElement.isScriptStatement(); 711 712 var parsedJS; 713 if(isScriptStatement===true){ 714 parsedJS = parser.parseJS( 715 subContentElement.scriptContent, 716 'embeddedStatementTail', 717 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... 718 , subContentElement.getStart() + this.getOffset() + '@('.length 719 ); 720 } 721 else { 722 parsedJS = parser.parseJS( 723 subContentElement.scriptContent, 724 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... 725 , subContentElement.getStart() + this.getOffset() + '@{'.length 726 ); 727 } 728 729 subContentElement.scriptContent = parsedJS; 730 731 preparedJSCode = renderer.renderJS(parsedJS.rawTemplateText, parsedJS.varReferences); 732 733 if(isScriptStatement===true){ 734 preparedJSCode = 'return ('+preparedJSCode+');'; 735 } 736 737 try{ 738 subContentElement.scriptEval = createJSEvalFunction(preparedJSCode, 'scriptEval'); 739 } catch (err){ 740 var error = new ScriptEvalError(err, preparedJSCode, this, subContentElement); 741 //attach a dummy function that prints the error each time it is invoked: 742 subContentElement.scriptEval = function(){ console.error(error.getDetails()); }; 743 //... and print the error now, too: 744 console.error(error.getDetails()); 745 } 746 747 this.internalHasDynamicContent = true; 748 } 749 750 if(typeof subContentElement.content === 'string'){ 751 752 subContentElement.content = new ContentElement({ 753 name: SUB_ELEMENT_NAME, 754 content: subContentElement.content, 755 offset: this.getOffset() + subContentElement.contentOffset 756 }, view, parser, renderer 757 ); 758 759 this.internalHasDynamicContent = this.internalHasDynamicContent || subContentElement.content.hasDynamicContent(); 760 } 761 762 //IF-elements can have an additional ELSE-content field: 763 if(subContentElement.hasElse() && typeof subContentElement.elseContent.content === 'string'){ 764 765 subContentElement.elseContent.content = new ContentElement({ 766 name: SUB_ELEMENT_NAME, 767 content: subContentElement.elseContent.content, 768 offset: this.getOffset() + subContentElement.elseContent.contentOffset 769 }, view, parser, renderer 770 ); 771 772 this.internalHasDynamicContent = this.internalHasDynamicContent || subContentElement.elseContent.content.hasDynamicContent(); 773 } 774 } 775 776 return this; 777 } 778 779 780 /** 781 * Gets the name of a {@link mmir.ContentElement} object (content, header, footer, dialogs, ...). 782 * 783 * @function 784 * @returns {String} Name - used by yield tags in layout 785 * @public 786 */ 787 ContentElement.prototype.getName = function(){ 788 return this.name; 789 }; 790 791 /** 792 * Gets the owner for this ContentElement, i.e. the {@link mmir.View} object. 793 * 794 * @function 795 * @returns {mmir.View} the owning View 796 * @public 797 */ 798 ContentElement.prototype.getView = function(){ 799 return this.view; 800 }; 801 802 /** 803 * Gets the controller for this ContentElement. 804 * 805 * @function 806 * @returns {mmir.Controller} the Controller of the owning view 807 * @public 808 */ 809 ContentElement.prototype.getController = function(){ 810 return this.getView().getController(); 811 }; 812 813 /** 814 * Gets the definition of a {@link mmir.ContentElement} object. 815 * 816 * TODO remove this? 817 * 818 * @function 819 * @returns {String} The HTML content. 820 * @public 821 */ 822 ContentElement.prototype.toHtml = function(){ 823 // return this.definition; 824 return this.toStrings().join(''); 825 }; 826 827 /** 828 * Renders this object into the renderingBuffer. 829 * 830 * @param renderingBuffer {Array} of Strings (if <code>null</code> a new buffer will be created) 831 * @param data {Any} (optional) the event data with which the rendering was invoked 832 * @returns {Array<String>} of Strings the renderingBuffer with the contents of this object added at the end 833 * 834 * @public 835 */ 836 ContentElement.prototype.toStrings = function(renderingBuffer, data){ 837 838 return this.renderer.renderContentElement(this, data, renderingBuffer); 839 840 }; 841 842 /** 843 * @public 844 * @returns {String} the raw text from which this content element was parsed 845 * @see #getDefinition 846 * 847 * @public 848 */ 849 ContentElement.prototype.getRawText = function(){ 850 return this.definition; 851 }; 852 /** 853 * @deprecated use {@link #getRawText} instead 854 * @returns {String} the raw text from which this content element was parsed 855 * @see #getRawText 856 * 857 * @public 858 */ 859 ContentElement.prototype.getDefinition = function(){ 860 return this.definition; 861 }; 862 /** 863 * @returns {Number} the start position for this content Element within {@link #getRawText} 864 * @public 865 */ 866 ContentElement.prototype.getStart = function(){ 867 return this.start; 868 }; 869 /** 870 * @returns {Number} the end position for this content Element within {@link #getRawText} 871 * @public 872 */ 873 ContentElement.prototype.getEnd = function(){ 874 return this.end; 875 }; 876 877 //FIXME add to storage? (this should only be relevant for parsing, which is not necessary in case of store/restore...) 878 ContentElement.prototype.getOffset = function(){ 879 return this.parentOffset; 880 }; 881 882 /** 883 * @returns {Boolean} returns <code>true</code> if this ContentElement conatains dynamic content, 884 * i.e. if it needs to be "evaluated" for rendering 885 * (otherwise, its plain text representation can be used for rendering) 886 * @public 887 */ 888 ContentElement.prototype.hasDynamicContent = function(){ 889 return this.internalHasDynamicContent; 890 }; 891 892 /** 893 * create a String representation for this content element. 894 * @returns {String} the string-representation 895 * @public 896 * 897 * @requires StorageUtils 898 * @requires RenderUtils 899 */ 900 ContentElement.prototype.stringify = function(){ 901 902 //TODO use constants for lists 903 904 //primitive-type properties: 905 // write values 'as is' for these properties 906 var propList = [ 907 'name', 908 'definition', 909 'start', 910 'end', 911 'internalHasDynamicContent' 912 ]; 913 914 //Array-properties 915 var arrayPropList = [ 916 'allContentElements' //element type: ParsingResult (stringify-able) 917 ]; 918 919 920 // //SPECIAL: store view by getter function initView: use the view's name view {View} -> 'viewName' {String}, 'ctrlName' {String} 921 // 922 // //USED BY RENDERER: 923 //// allContentElements 924 //// definition 925 //// getRawText() == definition 926 //// getController() (by view) 927 // 928 // //SPECIAL: store renderer by getter function initRenderer 929 // 930 // //function properties: 931 // var funcPropList = [ 932 // 'initView', 933 // 'initRenderer' 934 // ]; 935 936 937 //function for iterating over the property-list and generating JSON-like entries in the string-buffer 938 var appendStringified = parser_context.appendStringified; 939 940 var sb = ['require("storageUtils").restoreObject({ classConstructor: "contentElement"', ',']; 941 942 appendStringified(this, propList, sb); 943 944 //non-primitives array-properties with stringify() function: 945 appendStringified(this, arrayPropList, sb, null, function arrayValueExtractor(name, arrayValue){ 946 947 var buf =['[']; 948 for(var i=0, size = arrayValue.length; i < size; ++i){ 949 buf.push(arrayValue[i].stringify()); 950 buf.push(','); 951 } 952 //remove last comma 953 if(arrayValue.length > 0){ 954 buf.splice( buf.length - 1, 1); 955 } 956 buf.push(']'); 957 958 return buf.join(''); 959 }); 960 961 //TODO is there a better way to store the view? -> by its name and its contoller's name, and add a getter function... 962 if(this['view']){ 963 //getter/setter function for the view/controller 964 // (NOTE: needs to be called before view/controller can be accessed!) 965 sb.push( 'initView: function(){'); 966 967 // store view-name: 968 sb.push( ' var viewName = '); 969 sb.push( JSON.stringify(this.getView().getName()) ); 970 971 // store controller-name: 972 sb.push( '; var ctrlName = '); 973 sb.push( JSON.stringify(this.getController().getName()) ); 974 975 // ... and the getter/setter code: 976 sb.push( '; this.view = require("presentationManager").get'); 977 sb.push(this['view'].constructor.name);//<- insert getter-name dependent on the view-type (e.g. View, Partial) 978 sb.push('(ctrlName, viewName); this.getView = function(){return this.view;}; return this.view; },' ); 979 980 981 sb.push( 'getView: function(){ return this.initView();}'); 982 983 //NOTE: need to add comma in a separate entry 984 // (-> in order to not break the removal method of last comma, see below) 985 sb.push( ',' ); 986 } 987 988 //TODO is there a better way to store the renderer? -> by a getter function... 989 if(this['renderer']){ 990 //getter/setter function for the (default) renderer 991 // (NOTE: needs to be called before view/controller can be accessed!) 992 sb.push( 'initRenderer: function(){'); 993 // ... and the getter/setter code: 994 sb.push( ' this.renderer = require("renderUtils"); }' ); 995 996 //NOTE: need to add comma in a separate entry 997 // (-> in order to not break the removal method of last comma, see below) 998 sb.push( ',' ); 999 } 1000 1001 if(this['renderer'] || this['view']){ 1002 //add initializer function 1003 // (NOTE: needs to be called before view/controller or renderer can be accessed!) 1004 sb.push( 'init: function(){'); 1005 1006 if(this['renderer']){ 1007 sb.push( ' this.initRenderer(); ' ); 1008 } 1009 // if(this['view']){ 1010 // sb.push( ' this.initView(); ' ); 1011 // } 1012 sb.push( ' }' ); 1013 1014 //NOTE: need to add comma in a separate entry 1015 // (-> in order to not break the removal method of last comma, see below) 1016 sb.push( ',' ); 1017 } 1018 1019 //if last element is a comma, remove it 1020 if(sb[sb.length - 1] === ','){ 1021 sb.splice( sb.length - 1, 1); 1022 } 1023 1024 sb.push(' })'); 1025 return sb.join(''); 1026 }; 1027 1028 return ContentElement; 1029 1030 });//END: define(..., function(){