Main function that looks for a tabstops in provided <code>text</code>
and returns a processed version of <code>text</code> with expanded
placeholders and list of tabstops found.
@param {String} text Text to process
@param {Object} options List of processor options:<br>

<b>replaceCarets</b> : <code>Boolean</code> — replace all default
caret placeholders (like <i>{%::emmet-caret::%}</i>) with <i>${0:caret}</i><br>

<b>escape</b> : <code>Function</code> — function that handle escaped
characters (mostly '$'). By default, it returns the character itself
to be displayed as is in output, but sometimes you will use
<code>extract</code> method as intermediate solution for further
processing and want to keep character escaped. Thus, you should override
<code>escape</code> method to return escaped symbol (e.g. '\\$')<br>

<b>tabstop</b> : <code>Function</code> – a tabstop handler. Receives
a single argument – an object describing token: its position, number
group, placeholder and token itself. Should return a replacement
string that will appear in final output

<b>variable</b> : <code>Function</code> – variable handler. Receives
a single argument – an object describing token: its position, name
and original token itself. Should return a replacement
string that will appear in final output

@returns {Object} Object with processed <code>text</code> property
and array of <code>tabstops</code> found
@memberOf tabStops
function(text, options) {
			// prepare defaults
			var utils = require('utils');
			var placeholders = {carets: ''};
			var marks = [];
			
			options = _.extend({}, defaultOptions, options, {
				tabstop: function(data) {
					var token = data.token;
					var ret = '';
					if (data.placeholder == 'cursor') {
						marks.push({
							start: data.start,
							end: data.start + token.length,
							group: 'carets',
							value: ''
						});
					} else {
						// unify placeholder value for single group
						if ('placeholder' in data)
							placeholders[data.group] = data.placeholder;
						
						if (data.group in placeholders)
							ret = placeholders[data.group];
						
						marks.push({
							start: data.start,
							end: data.start + token.length,
							group: data.group,
							value: ret
						});
					}
					
					return token;
				}
			});
			
			if (options.replaceCarets) {
				text = text.replace(new RegExp( utils.escapeForRegexp( utils.getCaretPlaceholder() ), 'g'), '${0:cursor}');
			}
			
			// locate tabstops and unify group's placeholders
			text = this.processText(text, options);
			
			// now, replace all tabstops with placeholders
			var buf = utils.stringBuilder(), lastIx = 0;
			var tabStops = _.map(marks, function(mark) {
				buf.append(text.substring(lastIx, mark.start));
				
				var pos = buf.length;
				var ph = placeholders[mark.group] || '';
				
				buf.append(ph);
				lastIx = mark.end;
				
				return {
					group: mark.group,
					start: pos,
					end:  pos + ph.length
				};
			});
			
			buf.append(text.substring(lastIx));
			
			return {
				text: buf.toString(),
				tabstops: _.sortBy(tabStops, 'start')
			};
		}