Source: mvc/parser/templateParseUtils.js


define([ 'mmirf/parserModule','mmirf/parsingResult','mmirf/templateProcessor'
		,'mmirf/templateLexer','mmirf/ES3Lexer','mmirf/ES3Parser','mmirf/contentLexer','mmirf/contentParser'
		,'mmirf/scriptLexer','mmirf/scriptParser','mmirf/antlr3'
	],

	/**
	 * A Utility class for parsing (eHTML) templates.<br>
	 * Utility functions for parsing templates (and template elements, e.g. JS-parts of template expressions)
	 *
	 * @class
	 * @name ParserUtils
	 * @memberOf mmir.parser
	 * @static
	 * @hideconstructor
	 *
	 * @public
	 * @example mmir.require('mmirf/parseUtils').parse(str, view)
	 */
	function( parser, ParsingResult, templateProcessor
		, MmirTemplateLexer, ES3Lexer, ES3Parser, MmirScriptContentLexer, MmirScriptContentParser
		, MmirScriptLexer, MmirScriptParser, org
){

	/**
	 * add extend-function for MmirTemplateLexer objects:
	 *  attaches the main processing functionality for parsing the template files
	 *
	 *  (NOTE the extend-function is called in the constructor, see MmirTemplate.g and MmirTemplateLexer.js)
	 *
	 * @type Function
	 * @memberOf MmirTemplateLexer.prototype
	 */
	MmirTemplateLexer.__mmirExtend = templateProcessor;

	/**
	 * @memberOf ES3Parser.prototype
	 * @function getVarReferences
	 */
	ES3Parser.prototype.getVarReferences = function(){

		var size = this.ampersatIdentifiers.length;

		if(size === 0){
			return null;
		}

		var varRefs = new Array(size);
		for(var i=0; i < size; ++i){
			var ref = this.ampersatIdentifiers[i];

			var refObj = new ParsingResult(ref);
//			refObj.start = ref.start;

			//correct end-position (token's stop-index is exactly the last char-index, whereas ParsingResult's end-position is token.stopIndex + 1)
			refObj.end = refObj.getEnd() + 1;

			refObj.type = parser.element.VAR_REFERENCE;

			varRefs[i] = refObj;
		}
		return varRefs;
	};

	////////////////////////////////////helper for debugging / printing error details ////////////////////////

	/**
	 * -2: internal debug
	 * -1: interanl info
	 *  0: debug
	 *  1: info
	 *  2: warn
	 *  3: error
	 * TODO make this set-able (export getter/setter? use configurationManager?)
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var errorLevel = 2;


	/**
	 * HELPER print internal debug messages during parsing (VERY VERBOSE)
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _print(msg){//FIXME
		if ( errorLevel <= -2 ) console.log(msg);
	};
	parser.print = _print;

	/**
	 * HELPER print internal, informational messages during parsing (VERBOSE)
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _printInfo(prefix, msg){//FIXME
		if (  errorLevel <= -1 ) console.info(parser.parserCreatePrintMessage(prefix,msg));
	};
	parser.printInfo = _printInfo;

	/**
	 * HELPER print debug messages during parsing
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _parserPrintDebug(prefix, msg, source){//FIXME
		if (  errorLevel <= 0  ) console.debug(parser.parserCreatePrintMessage(prefix,msg, source));
	};
	parser.parserPrintDebug = _parserPrintDebug;

	/**
	 * HELPER print informational messages during parsing
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _parserPrintInfo(prefix, msg, source){//FIXME
		if (  errorLevel <= 1  ) console.info(parser.parserCreatePrintMessage(prefix,msg, source));
	};
	parser.parserPrintInfo = _parserPrintInfo;

	/**
	 * HELPER print warnings during parsing
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _parserPrintWarning(prefix, msg, source){//FIXME
		if (  errorLevel <= 2  ) console.warn(parser.parserCreatePrintMessage(prefix,msg, source));
	};
	parser.parserPrintWarning = _parserPrintWarning;

	/**
	 * HELPER print errors during parsing
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function _parserPrintError(prefix, msg, source){
		if (  errorLevel <= 3  ) console.error(parser.parserCreatePrintMessage(prefix,msg, source));
	};
	parser.parserPrintError = _parserPrintError;

	/**
	 * HELPER: attach internal print-functions to all classes (ie. prototypes) in the list
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var _attachInternalPrintFunc = function(list){
		var _prototype;
		for(var i=0, size=list.length; i < size; ++i){
			_prototype = list[i].prototype;
			_prototype.printInfo  = parser.printInfo;
			_prototype.printDebug = parser.print;
		}
	};

	//attach the internal debug/print functions to all lexers/parsers:
	// (-> see import-list in define() above)
	_attachInternalPrintFunc([
		MmirTemplateLexer, ES3Lexer, ES3Parser, MmirScriptContentLexer, MmirScriptContentParser
		, MmirScriptLexer, MmirScriptParser
	]);

	/**
	 * @type mmir.view.View
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var _currentParsedView = null;//FIXME make this an argument in the printXXX functions (e.g. the current mechanism will not work, if templates are parsed concurrently/in parallel/using threads)

	/**
	 * Creates a message with parsing-information.
	 *
	 * In case the <code>msg</code> is an error message containing relative/incorrect location information,
	 * an heuristic will be used to fix the location information; in addition the references location
	 * will be extracted from the source-String and a "pointer-String" will be generated, e.g.
	 * <pre>
	 * 	source:   "  	@{  mmmm.['sd']=wer ;}@"
	 * 	pointer:  "  	        ^"
	 * </pre>
	 *
	 * @function
	 * @param {String} prefix
	 * 					a prefix for the message
	 * @param {String} msg
	 * 					the original message (may contain location-information "line <line_i>:<position_j>")
	 * @param {Object} [tokenSource] OPTIONAL
	 * 					the token-source, from where the error/message was triggered
	 * 					If the argument has the field <code>tokenSource.offset</code> (Number)
	 * 					 if will be used to correct/fix the location information in the original message.
	 * 					If the argument has the fields <code>tokenSource.start</code> (Number) and
	 * 					 <code>tokenSource.end</code> (Number), then this will be used to correct/fix
	 * 					 the location information in the original message text.
	 * @param {Object} [viewObj] OPTIONAL
	 * 					currently not used!
	 * 					(will replace _currentParsedView in the future!)
	 *
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var parserCreatePrintMessage = (function(){//return function(prefix, msg, tokenSource, viewObj)

		/**
		 *
		 * Get the index in the String str, where line number lineNo
		 * starts.
		 *
		 * New lines begin after \n, \r\n, or \r.
		 *
		 * If lineNo is <= 1, the function returns always 0.
		 *
		 * If the lineNo is greater than the count of lines in str, the string length itself is returned.
		 *
		 * <p>
		 * NOTE used by {@link #parserCreatePrintMessage}
		 *
		 * @function
		 * @param {String} str the string
		 * @param {Number} lineNo the line number (first line is 1)
		 *
		 * @private
		 * @memberOf mmir.parser.ParserUtils.parserCreatePrintMessage
		 */
		var getIndexForLine = (function(){

			var detectLinebreak = /(\r?\n|\r)/igm;

			return function(str, lineNo){
				if(lineNo <= 1){
					return 0;
				}
				var match;
				var count = 1;
				while(match = detectLinebreak.exec(str)){
					//ASSERT: lineNo >= 2
					if(++count == lineNo){
						break;
					}
				}

				//reset regexpr:
				detectLinebreak.lastIndex = 0;

				if(match){
					return match.index + match[1].length;
				}

				//request line-no. >= 2 AND loop "detect enough" linebreaks => the request line index starts after strings ends => return string's length
				return str.length;
			};
		})();//END getIndexForLine

		/**
		 *
		 * Get the line in the String str, in which the char at index is included.
		 *
		 * New lines begin after \n, \r\n, or \r,
		 * e.g. for line X:
		 * <pre>
		 *  ...\r\n
		 *        ^
		 * </pre>
		 * the line number will be X (i.e. the line-break itself is still included in the current line).
		 * <p>
		 * If index is < 0, the function returns always 1.
		 * <p>
		 * If the index is greater than str.length, -1 is returned.
		 * <p>
		 * NOTE used by {@link #extractErrorPosition}
		 *
		 * @function
		 * @param {String} str the string
		 * @param {Number} index the char index for which to find the line number (first line is 1)
		 *
		 * @private
		 *
		 * @memberOf mmir.parser.ParserUtils.parserCreatePrintMessage
		 *
		 */
		var getLineForIndex = (function(){

			var detectLinebreak = /(\r?\n|\r)/ig;

			return function(str, index){
				if(index < 0){
					return 1;
				}
				if(index >= str.length){
					return -1;
				}
				//ASSERT index is at least within line 1
				var match;
				var count = 1;
				var isNextLineFound = false;
			var currentPos = -1;
			var lastPos = 0;
				while(match = detectLinebreak.exec(str)){
				currentPos = match.index + match[1].length;
					if(currentPos > index){
						isNextLineFound = true;
						break;
					}
				lastPos = currentPos;
					++count;
				}

				//reset regexpr:
				detectLinebreak.lastIndex = 0;

				return {
					line : count,
					index: index - lastPos
				};
			};
		})();//END getLineForIndex

		/**
		 *
		 * NOTE used by {@link #parserCreatePrintMessage}
		 *
		 * @private
		 * @function
		 * @memberOf mmir.parser.ParserUtils.parserCreatePrintMessage
		 */
		var extractErrorPosition = (function(){

			var detectLineNo = /line (\d+):(-?\d+)/i;

			return function extractErrorPositionImpl(msg, offset, originalContent, tokenSource){
//				console.log('\nTEST1_extractErrorPositionImpl with arguments '+arguments.length+'\n');

				var result = detectLineNo.exec(msg);

				//reset regexpr:
				detectLineNo.lastIndex = 0;

//				console.log('\nTEST2_result for "'+msg+'": '+result+'\n');
				var pos = null;
				if(result){

					var line = parseInt(result[1],10);
					var index = parseInt(result[2],10);

					var isCorrected = false;

					if(tokenSource){

						//if we have "invalid" position-info.:
						//  -> the error probably occured at the very beginning of the parsed expression
						//  -> try to extract position from parent parser/lexer
						if(line === 0 || index === -1){
							line  = tokenSource.getLine();
							index = tokenSource.getCharPositionInLine();
						}

						//if there is an offest supplied by the tokenSource -> use it:
						if(tokenSource.offset){

							var iOffset = tokenSource.offset;
//							if(line === 1){
//								//
//								//this position information is derived from a script-eval (-> ConentElement.ScriptEvalError)
//								// -> need to increase offset by 1 or 2, since all script-elements have
//								//    an additional, internal offset of 1 or 2 (this is only an heuristical value...)
//								// e.g. @( ...
//								//      @{ ...
//								//   @for( ...
//								iOffset += 2;
//							}

							var contentOffset = getLineForIndex(originalContent, iOffset);

							//if it is "relatively" the first line, we need to adjust to index
							//   (i.e. the position within the line)
							if(line === 1){
								index += contentOffset.index;
							}

							//adjust the line, i.e. make "relative" -> "absolute" line number
							line += contentOffset.line - 1;

							isCorrected = true;
						}

					}

					pos = {
							line: line,
							index: index
					};
//					console.log('\nTEST3_pos: '+JSON.stringify(pos)+', offset: '+offset+'\n');

					if(offset && offset !== 0){
//						console.log('\nTEST4_offset: '+offset+'\n');

						var newLine = line;
						var newIndex = index;
						if( ! isCorrected){
							var lineOffset = getLineForIndex(originalContent, offset);
							if(line < 2){
								newIndex = lineOffset.index + index;
								pos.originalIndex = index;
								pos.index = newIndex;
							}
							newLine = lineOffset.line + line - 1;
							pos.originalLine = line;
							pos.line = newLine;
						}

						var fixed = msg.substring(0,result.index + 'line '.length) + newLine + ':' + newIndex + msg.substring(result.index + result[0].length);
						pos.text = fixed;
//						pos.originalContent = originalContent;
//						pos.offset = offset + pos.index;
					}
					else {
						pos.text = msg;
					}
				}
				else if(tokenSource && tokenSource.start && tokenSource.end){

					pos = getLineForIndex(originalContent, tokenSource.start);
					pos.text = ' near /';
				}

				return pos;
			};
		})();//END extractErrorPosition

		/**
		 * Create a message for parsing-information.
		 *
		 * In case the <code>msg</code> is an error message containing relative/incorrect location information,
		 * an heuristic will be used to fix the location information; in addition the references location
		 * will be extracted from the source-String and a "pointer-String" will be generated, e.g.
		 * <pre>
		 * 	source:   "  	@{  mmmm.['sd']=wer ;}@"
		 * 	pointer:  "  	        ^"
		 * </pre>
		 *
		 * @private
		 *
		 * @param {String} prefix
		 * 					a prefix for the message
		 * @param {String} msg
		 * 					the original message (may contain location-information "line <line_i>:<position_j>")
		 * @param {Object} [tokenSource] OPTIONAL
		 * 					the token-source, from where the error/message was triggered
		 * 					If the argument has the field <code>tokenSource.offset</code> (Number)
		 * 					 if will be used to correct/fix the location information in the original message.
		 * 					If the argument has the fields <code>tokenSource.start</code> (Number) and
		 * 					 <code>tokenSource.end</code> (Number), then this will be used to correct/fix
		 * 					 the location information in the original message text.
		 * @param {Object} [viewObj] OPTIONAL
		 * 					currently not used!
		 * 					(will replace _currentParsedView in the future!)
		 */
		return function parserCreatePrintMessageImpl(prefix, msg, tokenSource, viewObj){//FIXME
			var currentView = _currentParsedView;
			if(currentView != null){

				var rootView = null;
				var details = '';
				if(currentView.getController){
					details += 'CTRL("' + currentView.getController().getName() + '")';
				}

				if(currentView.getView){
					if(details.length > 0){
						details += '->';
					}
					details += 'VIEW("' + currentView.getView().getName() + '")';
					rootView = currentView.getView();
				}

				if(details.length > 0){
					details += '->';
				}
				details += currentView.constructor.name;

				if(currentView.getName){
					details += '("' + currentView.getName() + '")';
				}

				if(rootView && typeof currentView.getStart !== 'undefined'){

					var pos = extractErrorPosition(msg, currentView.getOffset(), rootView.getDefinition(), tokenSource);
		//			console.log('\nTEST_A_pos: '+JSON.stringify(pos)+', offset: '+currentView.getStart() +'\n');
					if(pos){

						msg = pos.text;

						//msg += '\n\t at line '+pos.line+', index '+pos.index;
						var content = rootView.getDefinition();
						var line = null;
						var offset = currentView.getStart();


						if(content){
							var start = getIndexForLine(content, pos.line);
							var end = start;
							var len = content.length;
							while(end < len && (content[end] != '\r' && content[end] != '\n')){
								++end;
							}

							line = content.substring(start,end);
						}

						if(line){

							//marker for "pointing" the error
							var marker = [];
							for(var i=0; i < pos.index; ++i){
								if(line[i] == '\t'){
									//need to include tabs themselves, since they
									//  take more than 1 char-positions when displayed:
									marker.push('\t');
								}
								else {
									marker.push(' ');
								}
							}
							//add marker symbol, that points to error in the line above:
							marker.push('^');

							msg += ' at line '+pos.line+':';
							msg += '\n "'+line+'"';        //<- the line with the error
							msg += '\n  '+marker.join(''); //<- the marker line (will only be correctly aligned for fixed-width fonts)
						}
					}
				}

				return prefix + 'in ' + details + ' - ' + msg;
			}
			else {
				return prefix+msg;
			}
		};//END parserCreatePrintMessage

	})();//END var parserCreatePrintMessage = ...

	parser.parserCreatePrintMessage = parserCreatePrintMessage;

	//////////////////////////////////// END: helper for debugging, error details etc. ////////////////////////

	/**
	 * Object containing the instance of the class ParserUtils
	 *
	 * @type ParserUtils
	 * @private
	 *
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var instance = null;

	/**
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	var isDebug = true;//TODO read/set from configuration

	MmirTemplateLexer.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] TemplateLexer: ', msg, this);
	};
//		MmirTemplateParser.prototype.emitErrorMessage = function(msg) {
//			parser.parserPrintError('[ERROR] TemplateParser: ',msg);
//		};

	ES3Lexer.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] JavaScriptLexer_ES3: ', msg, this);
	};
	ES3Parser.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] JavaScriptParser_ES3: ', msg, this.getTokenStream().getTokenSource());
	};

	MmirScriptLexer.prototype.emitErrorMessage = function(msg) {
		var mode = this.isStatementMode()? 'Statement' : 'Block';
		parser.parserPrintError('[ERROR] Script'+mode+'Lexer: ',msg, this);
	};
	MmirScriptParser.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] ScriptParser: ',msg, this.getTokenStream().getTokenSource());
	};

	MmirScriptContentLexer.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] ContentLexer: ',msg, this);
	};
	MmirScriptContentParser.prototype.emitErrorMessage = function(msg) {
		parser.parserPrintError('[ERROR] ContentParser: ',msg, this.getTokenStream().getTokenSource());
	};

	/**
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function internalParse(text) {

		var input = new org.antlr.runtime.ANTLRStringStream(text);//FIXME change, how dependency 'mmirf/antlr3' is exported?
		var lexer = new MmirTemplateLexer(input);

		lexer.isDebug = isDebug;

		var tokens = new org.antlr.runtime.CommonTokenStream(lexer);//FIXME change, how dependency 'mmirf/antlr3' is exported?

		var result 				= {};
		result.rawTemplateText 	= tokens.toString();
		result.scripts 			= lexer.includeScripts;
		result.styles 			= lexer.includeStyles;
		result.localizations 	= lexer.locales;
		result.ifs	 			= lexer.ifs;
		result.fors 			= lexer.fors;
		result.yields 			= lexer.yields;
		result.contentFors 		= lexer.yieldContents;
		result.helpers	 		= lexer.helpers;
		result.partials 		= lexer.renderPartials;
		result.escapes	 		= lexer.escape;
		result.scriptStatements	= lexer.scriptStatements;
		result.scriptBlocks		= lexer.scriptBlocks;
		result.vars				= lexer.vars;
		result.comments			= lexer.comments;
		//end: parsing results


		lexer = null;

		return result;
	}

	/**
	 * @private
	 * @memberOf mmir.parser.ParserUtils#
	 */
	function internalParseJS(text, entryRuleName, offset) {

		var input = new org.antlr.runtime.ANTLRStringStream(text);
		var lexer = new ES3Lexer(input);
		lexer.isDebug = isDebug;
		lexer.offset = offset;

		var tokens = new org.antlr.runtime.CommonTokenStream(lexer);
		var parser = new ES3Parser(tokens);
		parser.offset = offset;

		if(!entryRuleName){
//			var parseResult =
			parser.program();//<- parse with main rule 'program' in ES3Parser
		}
		else {
//				var parseResult =
				parser[entryRuleName]();//<- parse with main rule 'program' in ES3Parser
		}
		var result 				= new Object();
		result.rawTemplateText 	= tokens.toString();

		var varRefs = parser.getVarReferences();
		if(varRefs){
			result.varReferences = varRefs;
		} else {
			result.varReferences = [];
		}

		//TODO handle potentially global var-declaration (i.e. assignments without preceding var, where the variable is undefined yet)

		//end: parsing results

		lexer = null;
		parser = null;

		return result;
	}

//		var getVarReferences = function(parser){
//
//			var size = parser.ampersatIdentifiers.length;
//
//			if(size === 0){
//				return null;
//			}
//
//			var varRefs = new Array(size);
//			for(var i=0; i < size; ++i){
//				var ref = parser.ampersatIdentifiers[i];
//
//				var refObj = new mmir.parser.ParsingResult(ref);
////				refObj.start = ref.start;
//
//				//correct end-position (token's stop-index is exactly the last char-index, whereas ParsingResult's end-position is token.stopIndex + 1)
//				refObj.end = refObj.getEnd() + 1;
//
//				refObj.type = parser.element.VAR_REFERENCE;
//
//				varRefs[i] = refObj;
//			}
//			return varRefs;
//		};

		/**
		 * Constructor-Method of Singleton mmir.parser.ParserUtils
		 *
		 * @constructs ParserUtils
		 * @memberOf mmir.parser.ParserUtils.prototype
		 * @private
		 * @ignore
		 *
		 */
		function constructor(){
			//private members (currently none)

			/** @lends mmir.parser.ParserUtils.prototype */
			return {
				//public members:

				/**
				 * Parse a text as view template (e.g. *.ehtml files).
				 *
				 * @param {String} rawTemplateString the text that should be parsed
				 * @param {Object} [view] (optional) the view to which the <tt>rawTemplateString</tt> belongs (only used for error messages)
				 * @returns {mmir.parser.ParsingResult} the parsing result
				 *
				 * @public
				 * @memberOf mmir.parser.ParserUtils.prototype
				 */
				parse: function(rawTemplateString, view){

					if(view){
						_currentParsedView = view;
					}
					else {
						_currentParsedView = null;
					}

					return internalParse(rawTemplateString);
				},

				/**
				 * Parse a text as JavaScript.
				 *
				 * @param {String} rawTemplateString the text that should be parsed
				 * @param {String} [parseEntryRuleName] (optional) specifies the JavaScript element that should be parsed for
				 * @param {Object} [view] (optional) the view to which the <tt>rawTemplateString</tt> belongs (only used for error messages)
				 * @returns {mmir.parser.ParsingResult} the parsing result
				 *
				 * @public
				 * @memberOf mmir.parser.ParserUtils.prototype
				 */
				parseJS: function(rawTemplateString, parseEntryRuleName, view, inViewOffset){

					//in case only 2 or 3 arguments are present: is 2nd the View object?
					if(!inViewOffset && typeof parseEntryRuleName !== 'string' && parseEntryRuleName !== null && typeof parseEntryRuleName === 'object'){

						if(typeof view === 'number'){
							inViewOffset = view;
						}

						view = parseEntryRuleName;
						parseEntryRuleName = null;


					}

					if(view){
						_currentParsedView = view;
					}
					else {
						_currentParsedView = null;
					}

					return internalParseJS(rawTemplateString, parseEntryRuleName, inViewOffset);
				}
			};//END: return{}

		}//END: constructor()

		instance = new constructor();

		//FIXME should the renderer be exported to parser.ParserUtils here?
		parser.ParserUtils = instance;

		return instance;

	});//END define(..., function(){