Benutzer:P.Copp/scripts/preprocessor.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
/**************************************************************************************************
 * preprocessor.js
 * Wikitext preprocessor, based on MediaWiki's parser (Preprocessor_DOM.php r55795)
 * http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/parser/Preprocessor_DOM.php
 * <nowiki>
 */

importScript( 'Benutzer:P.Copp/scripts/wiki.js' );

/**************************************************************************************************
 * Local defines
 */
if( !window.LocalDefines ) LocalDefines = {};
if( !window.Wiki ) Wiki = {};

LocalDefines.extensiontags = [
	'categorytree',
	'charinsert',
	'hiero',
	'imagemap',
	'inputbox',
	'poem',
	'ref',
	'references',
	'source',
	'syntaxhighlight',
	'timeline'
];

/**************************************************************************************************
 * preprocessToObject()
 *
 * Turns a wikitext string into a document tree
 * The returned data structure is a bit more compact than a real XML DOM, so
 * some memory is saved, when the extra stuff is not needed.
 * Use PPFrameXML to expand the compact form into an XML string
 * with the same structure as returned by MediaWiki
 *
 * The returned object has the following structure:
 * domnode = {
 *     type  : ('root'|'link'|'template'|'tplarg'|'h'|'comment'|'ignore'|'ext'),
 *     offset: int,
 *     len   : int,
 *     parts : [  [('text'|node)*],  ...  ],
 *     index, level : int, //only for heading nodes
 *     extname: 'name', //only for ext nodes
 * }
 *
 * Dependencies: LocalDefines.extensiontags, Wiki.defaulttags
 */
Wiki.preprocessToObject = function( text, forInclusion ) {
	if( text === false ) return text;

	var lastindex = 0;
	var stack = [];
	var top = new Node( 'root', 0 );
	var headings = 0;
	var skipnewline = false;
	var tag = null;
	var enableonlyinclude = false;
	var search = false;
	var match;

	//Line 145-156
	if( forInclusion
	    && text.indexOf( '<onlyinclude>' ) > -1
		&& text.indexOf( '</onlyinclude>' ) > -1 )
	{
		enableonlyinclude = true;
		tag = new Node( 'ignore', 0 );
		search = /<onlyinclude>|^$/;
	}
	var ignoredtag = forInclusion ? /includeonly/i : /noinclude|onlyinclude/i;
	var ignoredelement = forInclusion ? 'noinclude' : 'includeonly';

	//Construct our main regex
	var tags = '(' + Wiki.defaulttags.concat( LocalDefines.extensiontags ).join( '|' ) + ')';
	var specials = '\\{\\{+|\\[\\[+|\\}\\}+|\\]\\]+|\\||(\n)(=*)|(^=+)';
	var regex = RegExp( specials + '|<' + tags + '(?:\\s[^>]*)?\\/?>|<\\/'
	                    + tags + '\\s*>|<!--|-->|$', 'ig' );

	while( match = regex.exec( text ) ) {
		var s = match[0];

		//If we're in searching mode, skip all tokens until we find a matching one
		if( search ) {
			if( s.match( search ) ) {
				var search = false;
				if( tag.type != 'comment' ) {
					add( tag, text.substring( lastindex, match.index ) );
					lastindex = match.index + s.length;
					if( tag.type != 'ignore' ) tag.parts.push( tag.cur = [] );
					add( tag, s );
					processToken( 'tag', finish( tag, match.index + s.length ) );
				}
			}
			continue;
		}

		if( s == '<!--' ) { //Comment found
			var span = getCommentSpan( match.index );
			processToken( 'text', text.substring( lastindex, span[0] ) );
			lastindex = span[1];
			tag = new Node( 'comment', span[0], text.substring( span[0], span[1] ) );
			processToken( 'tag', finish( tag, span[1] ) );
			var search = /-->|^$/;
			//If we put a trailing newline in the comment, make sure we don't double output it
			if( text.charAt( span[1] - 1 ) == '\n' ) skipnewline = true;
			continue;
		}

		//Process all text between the last and the current token
		if( match.index > lastindex )
			processToken( 'text', text.substring( lastindex, match.index ) );
		lastindex = match.index + s.length;
		if( !s ) break; //End of text

		if( match[1] || match[3] ) { //Line start/end
			if( skipnewline || match[3] ) skipnewline = false;
			else{
				processToken( 'lineend', '', match.index );
				processToken( 'text', '\n' );
			}
			//processToken( 'linestart' );
			if( match[2] || match[3] )
				processToken( '=', match[2] || match[3], match.index + ( match[1] ? 1 : 0 ) );
			continue;
		}

		if( match[4] ) { //Open <tag /?> found
			if( match[4].match( ignoredtag ) ) {
				processToken( 'tag', finish( new Node( 'ignore', match.index, s ), lastindex ) );
				continue;
			}
			var lc = match[4].toLowerCase();
			if( lc == 'onlyinclude' ) {
				//This can only happen, if we're in template mode (forInclusion=true) and
				//the token we found is sth. like '<ONLYINCLUDE >'(i.e. unusual case or whitespace)
				//Output it literally then, to match MediaWiki's behavior
				processToken( 'text', s );
			} else {
				if( lc == ignoredelement ) tag = new Node( 'ignore', match.index, s );
				else {
					tag = new Node( 'ext', match.index, s );
					tag.extname = lc;
				}
				if( s.charAt( s.length - 2 ) == '/' ) {
					//Immediately closed tag (e.g. <nowiki />)
					processToken( 'tag', finish( tag, match.index + s.length ) );
				} else {
					//Search for the matching closing tag
					var search = RegExp( '<\\/' + lc + '\\b|^$', 'i' );
					//For ext nodes, we split the opening tag, content and closing tag into
					//separate parts. This is to simplify further processing since we already have
					//the information after all
					if( lc != ignoredelement ) tag.parts.push( tag.cur = [] );
				}
			}
			continue;

		} else if( match[5] ) { //Close </tag> found
			if( match[5].match( ignoredtag ) ) {
				processToken( 'ignore',
				              finish( new Node( 'ignore', match.index, s ), lastindex ) );
			} else if( enableonlyinclude && s == '</onlyinclude>' ) {
				//For onlyinclude, the closing tag is the start of the ignored part
				var tag = new Node( 'ignore', match.index, s );
				var search = /<onlyinclude>|^$/;
			} else {
				//We don't have a matching opening tag, so output the closing literally
				processToken( 'text', s );
			}
			continue;
		} else if( s == '-->' ) { //Comment endings without openings are output normally
			processToken( 'text', s );
			continue;
		}
		//Special token found: '|', {+, [+, ]+, }+
		var ch = s.charAt( 0 );
		processToken( ch, s, match.index );
	}
	//End of input. Put an extra line end to make sure all headings get closed properly
	processToken( 'lineend', text.length );
	processToken( 'end', text.length );
	return stack[0];

	//Handle some token and put it in the stack
	function processToken( type, token, offset ) {
		switch( type ) {
			case 'text'   :
			case 'ignore' :
			case 'tag'    : return add( top, token );
			case 'lineend':	//Check if we can close a heading
				if( top.type == 'h' ) {
					var next = stack.pop();
					if( top.closing ) {					
						//Some extra info for headings
						top.index = ++headings;
						top.level = Math.min( top.count, top.closing, 6 );
						add( next, finish( top, offset ) );
					} else {
						//No correct closing, break the heading and continue
						addBrokenNode( next, top );
					}
					top = next;
				}
				return;
			case '=':
				//Check if we can open a heading
				var len = token.length;
				//Line 352-355: Single '=' within a template part isn't treated as heading
				if( len == 1 && top.type == '{' && top.parts.length > 1
				    && typeof top.cur.splitindex == 'undefined' ) {
					add( top, token );
				} else {
					stack.push( top );
					top = new Node( 'h', offset, token, len );
					//Line 447-455: More than two '=' means we already have a correct closing
					top.closing = Math.floor( ( len - 1 ) / 2 );
				}
				return;
			case '|':
				//For brace nodes, start a new part
				if( top.type == '[' || top.type == '{' ) top.parts.push( top.cur = [] );
				else add( top, token );
				return;
			case '{'      :
			case '['      :
				stack.push( top );
				top = new Node( type, offset, '', token.length );
				return;
			case '}'      :
			case ']'      :
				//Closing brace found, try to close as many nodes as possible
				var open = type == '}' ? '{' : '[';
				var len = token.length;
				while( open == top.type && len >= 2 ) {
					while( len >= 2 && top.count >= 2 ) {
						//Find the longest possible match
						var mc = Math.min( len, top.count, open == '{' ? 3 : 2 );
						top.count -= mc;
						len       -= mc;
						//Record which type of node we found
						if( open == '{' ) top.type = mc == 2 ? 'template' : 'tplarg';
						else top.type = 'link';
						if( top.count >= 2 ) {
							//if we're still open, create a new parent and embed the node there
							var child = top;
							top = new Node( open, child.offset, child, child.count );
							//Correct the child offset by the number of remaining open braces
							child.offset += top.count;
							finish( child, offset + token.length - len );
						}
					}
					if( top.count < 2 ) {
						//Close the current node
						var next = stack.pop();
						//There might be one remaining brace open, add it to the parent first
						if( top.count == 1 ) add( next, open );
						top.offset += top.count;
						add( next, finish( top, offset + token.length - len ) );
						top = next;
					}
				}
				//Remaining closing braces are added as plain text
				if( len ) add( top, ( new Array( len + 1 ) ).join( type ) );
				return;
			case 'end'    :
				//We've reached the end, expand any remaining open pieces
				stack.push( top );
				for( var i = 1; i < stack.length; i++ )
					addBrokenNode( stack[0], stack[i] );
				finish( stack[0], offset );
		}
	}

	//Helper function to calculate the start and end position of a comment
	//We need this, because comments sometimes include the preceding and trailing whitespace
	//See lines 275-313
	function getCommentSpan( start ) {
		var endpos = text.indexOf( '-->', start + 4 );
		if( endpos == -1 ) return [start,text.length];
		for( var lead = start - 1; text.charAt( lead ) == ' '; lead-- );
		if( text.charAt( lead ) != '\n' ) return [start,endpos+3];
		for( var trail = endpos + 3; text.charAt( trail ) == ' '; trail++ );
		if( text.charAt( trail ) != '\n' ) return [start,endpos+3];
		return [lead+1,trail+1];
	}

	//DOM Node
	function Node( type, offset, content, count ) {
		this.type = type;
		this.offset = offset;
		this.parts = [[]];
		//cur and count are only for internal processing.
		//They will be cleaned up later by finish()
		this.cur = this.parts[0];
		if( content ) add( this, content );
		if( count ) this.count = count;
	}

	//Append text or a child to a node
	function add( node, el ) {
		if( !el ) return;
		var newstr = typeof el == 'string';
		var oldstr = typeof node.cur[node.cur.length - 1] == 'string';

		if( newstr && oldstr ) node.cur[node.cur.length - 1] += el;
		else node.cur.push( el );

		//For template nodes, record if and where an equal sign was found
		if( newstr && node.type == '{' && typeof node.cur.splitindex == 'undefined'
			&& el.indexOf( '=' ) > -1 ) node.cur.splitindex = node.cur.length - 1;

		//For heading nodes, record if we have a correct closing
		//A heading must end in one or more equal signs, followed only by
		//whitespace or comments
		if( node.type == 'h' ) {
			if( newstr ) {
				var match = el.match( /(=+)[ \t]*$/ );
				if( match ) node.closing = match[1].length;
				else if( !el.match( /^[ \t]*$/ ) ) node.closing = false;
			} else if( el.type != 'comment' ) node.closing = false;
		}
	}

	//Break and append a child to a node
	function addBrokenNode( node, el ) {
		//First add the opening braces
		if( el.type != 'h' ) add( node, ( new Array( el.count + 1 ) ).join( el.type ) );
		//Then the parts, separated by '|'
		for( var i = 0; i < el.parts.length; i++ ) {
			if( i > 0 ) add( node, '|' );
			for( var j = 0; j < el.parts[i].length; j++ ) add( node, el.parts[i][j] );
		}
	}

	//Clean up the extra stuff we put into the node for easier processing
	function finish( node, endOffset ) {
		node.len = endOffset - node.offset;
		node.lineStart = text.charAt( node.offset - 1 ) == '\n';
		delete node.cur;
		delete node.count;
		delete node.closing;
		return node;
	}
};

/**************************************************************************************************
 * PPFrame : Basic expansion frame, transforms a document tree back to the original wikitext
 */

function PPFrame() { this.self = PPFrame; }
PPFrame.prototype = {
	onEvent : function( evt, node, result, info ) {
	},

	expand : function( obj ) {
		if( typeof obj == 'string' ) {
			var result = this.expandString( obj );
			this.onEvent( 'text', obj, result );
			return result;
		}
		var type = obj.type.charAt( 0 ).toUpperCase() + obj.type.substring( 1 );
		var func = this['expand' + type];
		if( !func ) throw 'Unknown node type: ' + obj.type;
		this.onEvent( 'enter' + type, obj );
		var result = func.call( this, obj );
		this.onEvent( 'leave' + type, obj, result );
		return result;
	},

	expandString : function( s ) { return s; },
	expandRoot : function( obj ) { return this.expandPart( obj.parts[0] ); },
	expandLink : function( obj ) {
		return this.expand( '[[' ) + this.expandParts( obj.parts, '|' ) + this.expand( ']]' );
	},
	expandTemplate : function( obj ) {
		return this.expand( '{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}' );
	},
	expandTplarg : function( obj ) {
		return this.expand( '{{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}}' );
	},
	expandH : function( obj ) { return this.expandPart( obj.parts[0] ); },
	expandComment : function( obj ) { return this.expand( obj.parts[0][0] ); },
	expandIgnore : function( obj ) { return this.expand( obj.parts[0][0] ); },
	expandExt : function( obj ) { return this.expandParts( obj.parts ); },

	expandPart : function( part ) {
		var result = '';
		for( var i = 0; i < part.length; i++ ) result += this.expand( part[i] );
		return result;
	},
	expandParts : function( parts, joiner ) {
		var result = '';
		for( var i = 0; i < parts.length; i++ ) {
			if( joiner && i > 0 ) result += this.expand( joiner );
			result += this.expandPart( parts[i] );
		}
		return result;
	},

	splitPart : function( part ) {
		var i = part.splitindex;
		if( typeof i == 'undefined' ) return false;
		var pos = part[i].indexOf( '=' );
		var name = part.slice( 0, i );
		name.push( part[i].substring( 0, pos ) );
		var value = [part[i].substring( pos + 1 )].concat( part.slice( i + 1 ) );
		return [name,value];
	},

	extractParams : function( obj ) {
		var params = { //numbered and named arguments must be stored separately
			numbered : {},
			named    : {},
			obj      : obj
		};
		var num = 1;
		for( var i = 1; i < obj.parts.length; i++ ) {
			var split = this.splitPart( obj.parts[i] );
			if( split ) {
				var name = this.expandArgName( obj, split[0], i );
				params.named[name] = { value : split[1], part : i };
			} else params.numbered[num++] = { part : i };
		}
		return params;
	},
	getParam : function( params, name ) {		
		for( var i = 0; i < 2; i++ ) {
			var type = i ? 'named' : 'numbered';
			var param = params[type][name];
			if( !param ) continue;
			if( typeof param.value == 'string' ) return param.value; //cached
			//Param exists, but not yet expanded. Expand it and put the result in the cache
			param.value = i
				? this.expandArgValue( params.obj, param.value, param.part )
				: this.expandArg( params.obj, param.part );
			return param.value;
		}
		return false;
	},

	expandArgName : function( obj, part, num ) {
		this.onEvent( 'enterArgName', obj, null, [part,num] );
		var result = this.expandPart( part ).trim();
		this.onEvent( 'leaveArgName', obj, result, [part,num] );
		return result;
	},
	expandArgValue : function( obj, part, num ) {
		this.onEvent( 'enterArgValue', obj, null, [part,num] );
		var result = this.expandPart( part ).trim();
		this.onEvent( 'leaveArgValue', obj, result, [part,num] );
		return result;
	},
	expandArg : function( obj, num ) {
		if( typeof obj.parts[num] == 'undefined' ) return '';
		this.onEvent( 'enterArg', obj, null, num );
		var result = this.expandPart( obj.parts[num] );
		this.onEvent( 'leaveArg', obj, result, num );
		return result;
	}
};

Wiki.ppFrame = new PPFrame();

/**************************************************************************************************
 * PPFrameXML : Transforms a document tree to an XML string
 */
function PPFrameXML() { this.self = PPFrameXML; }
PPFrameXML.prototype = new PPFrame();

PPFrameXML.prototype.expandString = function( s ) {
	return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
};
PPFrameXML.prototype.expandRoot = function( obj ) {
	return this.XML( 'root', this.expandPart( obj.parts[0] ) );
};
PPFrameXML.prototype.expandTemplate = function( obj ) {
	var attr = obj.lineStart ? ' lineStart="1"' : '';
	return this.XML( 'template', this.expandTemplateParts( obj.parts ), attr );
};
PPFrameXML.prototype.expandTplarg = function( obj ) {
	var attr = obj.lineStart ? ' lineStart="1"' : '';
	return this.XML( 'tplarg', this.expandTemplateParts( obj.parts ), attr );
};
PPFrameXML.prototype.expandH = function( obj ) {
	return this.XML( 'h', this.expandPart( obj.parts[0] ),
	                 ' level="' + obj.level + '" i="' + obj.index + '"' );
};
PPFrameXML.prototype.expandComment = function( obj ) {
	return this.XML( 'comment', this.expand( obj.parts[0][0] ) );
};
PPFrameXML.prototype.expandIgnore = function( obj ) {
	return this.XML( 'ignore', this.expand( obj.parts[0][0] ) );
};
PPFrameXML.prototype.expandExt = function( obj ) {
	var m = obj.parts[0][0].match( /<([^\s\/>]*)(.*)>/ );
	var content = this.XML( 'name', this.expand( m[1] ) );
	content += this.XML( 'attr', this.expand( m[2].replace( /\/$/, '' ) ) );
	if( obj.parts[1] ) content += this.XML( 'inner', this.expand( obj.parts[1][0] ) );
	if( obj.parts[2] && obj.parts[2][0] )
		content += this.XML( 'close', this.expand( obj.parts[2][0] ) );
	return this.XML( 'ext', content );
};

PPFrameXML.prototype.expandTemplateParts = function( parts ) {
	var result = this.XML( 'title', this.expandPart( parts[0] ) );
	var num = 1;
	for( var i = 1; i < parts.length; i++ ) {
		var split = this.splitPart( parts[i] );
		if( split ) {
			var content = this.XML( 'name', this.expandPart( split[0] ) ) + this.expand( '=' );
			content += this.XML( 'value', this.expandPart( split[1] ) );
		} else {
			var content = this.XML( 'name', '', ' index="' + ( num++ ) + '"' );
			content += this.XML( 'value', this.expandPart( parts[i] ) );
		}
		result += this.XML( 'part', content );
	}
	return result;
};
PPFrameXML.prototype.XML = function( name, content, attr ) {
	return '<' + name + ( attr || '' ) + ( content ? '>' + content + '</' + name + '>' : '/>' );
};

Wiki.ppFrameXML = new PPFrameXML();

/**************************************************************************************************
 * Experimental stuff
 */

/**************************************************************************************************
 * PPTemplateFrame: Transform a document tree into expanded wikitext. Tries to approximate the
 * behavior of MediaWiki's template expansion. Not all variables and parser functions have been
 * implemented, though, others may work a bit differently from their original.
 */
 
function PPTemplateFrame() { this.self = PPTemplateFrame; }
PPTemplateFrame.prototype = new PPFrame();

PPTemplateFrame.prototype.setContextPage = function( page ) {
	this.contextPage = page;
	if( !this.page ) this.page = page;
};
PPTemplateFrame.prototype.createNewChild = function( obj, page ) {
	var child = new this.self();
	child.setContextPage( this.contextPage );
	child.page = page;
	child.callback = this.callback;
	child.parent = {
		frame  : this,
		params : this.extractParams( obj )
	};
	return child;
};

//like expand(), but loads template texts etc. asynchronously
//Note: On every ajax call the expansion starts from the beginning, so we get kind of a quadratic performance here
PPTemplateFrame.prototype.expandAsync = function( obj, callback, statusCallback, maxloads ) {
	if( !maxloads ) var maxloads = 100; //Upper limit for the number of ajax calls to the server
	var frame = this;
	this.callback = function() {
		maxloads--;
		if( !maxloads ) throw 'Too many ajax calls';
		var result = false;
		try{ var result = frame.expand( obj ); }
		catch( e ) {
			if( e && e.forceMode ) {
				if( statusCallback ) statusCallback( e.msg, e.args );
			} else throw e;
		}
		if( result !== false ) callback( result );
	};
	this.callback();
};
PPTemplateFrame.prototype.getAjaxItem = function( obj, funcname, args ) {
	var cached = obj[funcname + 'Cached'].apply( obj, args );
	if( cached !== null ) return cached;
	if( this.callback ) {
		args.push( this.callback );
		obj[funcname].apply( obj, args );
		throw { forceMode : true, msg : funcname, args : obj };
	}
	return obj[funcname].apply( obj, args );
};

PPTemplateFrame.prototype.getTemplateDom = function( page ) {
	return this.getAjaxItem( page, 'getTemplateDom', [] );
};

PPTemplateFrame.prototype.expandTemplate = function( obj ) {
	//Double brace expansion
	var name = this.expandArg( obj, 0 ).trim();
	var fname = Wiki.getMagicWord( name );
	if( fname && Wiki.magicIsVar( fname ) ) return this.expandVar( fname, obj );
	var pos = name.indexOf( ':' );
	if( pos > -1 ) {
		var fname = Wiki.getMagicWord( name.substring( 0, pos ) );
		if( fname == 'subst' ) return this.expandBrokenTemplate( obj );
		//TODO: Modifiers {{MSG:...}} etc.
		if( fname && Wiki.magicIsFunc( fname ) )
			return this.expandFunc( fname, name.substring( pos + 1 ).trim(), obj );
	}
	if( name.charAt( 0 ) == '/' ) name = this.contextPage.ptitle + name; //TODO: {{../}} etc.
	var page = Wiki.getPage( name, 10 );
	if( page ) {
		this.getAjaxItem( page, 'getText', [true] );
		if( page.getVal( 'redirect' ) ) page = Wiki.getPage( page.getVal( 'redirect' ) );
		var dom = this.getTemplateDom( page );
		if( !dom ) {
			this.onEvent( 'brokenTemplate', obj );
			return this.expand( '[[:' + page.ptitle + ']]' );
		} else return this.expandTemplatePage( page, dom, obj );
	} else return this.expandBrokenTemplate( obj );
};

PPTemplateFrame.prototype.expandTplarg = function( obj ) {
	//Triple brace expansion
	var name = this.expandArg( obj, 0 ).trim();
	if( this.parent ) {
		var value = this.parent.frame.getParam( this.parent.params, name );
		if( value !== false ) {
			this.onEvent( 'param', obj, value, name );
			return value;
		}
	}
	//No matching param found, try the default
	if( obj.parts.length > 1 ) return this.expandArg( obj, 1 );
	this.onEvent( 'brokenTplarg', obj );
	return this.expand( '{{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}}' );
};

PPTemplateFrame.prototype.expandIgnore =
PPTemplateFrame.prototype.expandComment = function() { return ''; };

PPTemplateFrame.prototype.expandVar = function( name, obj ) {
	var result = Wiki.getVarResult( name, this.contextPage );
	this.onEvent( 'var', obj, result, name );
	return result;
};
PPTemplateFrame.prototype.expandFunc = function( name, arg, obj ) {
	var func = this.parserFunctions[name];
	if( !func ) return this.expandBrokenTemplate( obj );
	this.onEvent( 'enterFunc', obj, null, [name,arg] );
	var result = func.call( this, obj, arg );
	//see Parser.php line 3026
	if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) result = '\n' + result;
	this.onEvent( 'leaveFunc', obj, result, [name,arg] );
	return result;
};

PPTemplateFrame.prototype.expandTemplatePage = function( page, dom, obj ) {
	this.onEvent( 'enterTemplatePage', obj, null, [page,dom] );
	var child = this.createNewChild( obj, page );
	var result = child.expand( dom );
	//see Parser.php line 3026
	if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) result = '\n' + result;
	this.onEvent( 'leaveTemplatePage', obj, result, [page,dom] );
	return result;
};

PPTemplateFrame.prototype.expandBrokenTemplate = function( obj ) {
	this.onEvent( 'brokenTemplate', obj );
	return this.expand( '{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}' );
};

PPTemplateFrame.prototype.expandExt = function( obj ) {
	if( obj.parts[1] && ( obj.extname == 'ref' || obj.extname == 'poem' ) ) {
		var open = this.expandPart( obj.parts[0] );
		var innerdom = Wiki.preprocessToObject( obj.parts[1].join( '' ), false );
		return open + this.expandPart( innerdom.parts[0] ) + this.expandPart( obj.parts[2] );
	} else return this.expandParts( obj.parts );
};

PPTemplateFrame.prototype.parserFunctions = {
	'if' : function( obj, arg ) {
		if( arg ) return this.expandArg( obj, 1 ).trim();
		return this.expandArg( obj, 2 ).trim();
	},
	'ifeq' : function( obj, arg ) {
		if( arg == this.expandArg( obj, 1 ).trim() )
			return this.expandArg( obj, 2 ).trim();
		return this.expandArg( obj, 3 ).trim();
	},
	'titleparts' : function( obj, arg ) {
		var title = Wiki.getPage( arg );
		if( !title ) return arg;
		var bits = title.ptitle.split( '/', 25 );
		var offset = Math.max( 0, ( parseInt( this.expandArg( obj, 2 ), 10 ) || 0 ) - 1 );
		var end      = parseInt( this.expandArg( obj, 1 ), 10 ) || 0;
		end = end > 0 ? offset + end : bits.length + end;
		return bits.slice( offset, end ).join( '/' );
	},
	'switch' : function( obj, arg ) {
		var found = false;
		var defaultfound = false;
		var switchdefault = false;
		for( var i = 1; i < obj.parts.length; i++ ) {
			var split = this.splitPart( obj.parts[i] );
			if( split ) {
				var left = this.expandArgName( obj, split[0], i );
				if( found || left == arg ) return this.expandArgValue( obj, split[1], i );
				else if( defaultfound || left == '#default' ) {
					switchdefault = split[1];
					var defaultindex = i;
				}
			} else {
				var left = this.expandArg( obj, i ).trim();
				if( left == arg ) found = true;
				else if( left == '#default' ) defaultfound = true;
			}
		}
		if( !split ) return left;
		if( switchdefault !== false )
			return this.expandArgValue( obj, switchdefault, defaultindex );
		return '';
	},
	'expr': function( obj, arg ) {
		try{ return ExpressionParser.eval( arg ) + ''; }
		catch( e ){ return '<strong class="error">'+e+'</strong>'; }
	},
	'ifexpr': function( obj, arg ) {
		try{ var value = ExpressionParser.eval( arg ); }
		catch( e ){ return '<strong class="error">'+e+'</strong>'; }
		if( value ) return this.expandArg( obj, 1 ).trim();
		return this.expandArg( obj, 2 ).trim();
	},
	'lc': function( obj, arg ) { return arg.toLowerCase(); },
	'uc': function( obj, arg ) { return arg.toUpperCase(); },
	'lcfirst': function( obj, arg ) { return Wiki.lcfirst( arg ); },
	'ucfirst': function( obj, arg ) { return Wiki.ucfirst( arg ); },
	'iferror': function( obj, arg ) {
		if( arg.match( /<(?:strong|span|p|div)\s[^>]*\bclass="[^">]*\berror\b[^">]*"/ ) )
			return this.expandArg( obj, 1 ).trim();
		if( obj.parts.length > 2 ) return this.expandArg( obj, 2 ).trim();
		return arg;
	},
	'urlencode': function( obj, arg ) {	return Wiki.urlencode( arg ); },
	'anchorencode': function( obj, arg ) { return Wiki.anchorencode( arg );	},
	'formatnum': function( obj, arg ) {
		if( this.expandArg( obj, 1 ).indexOf( 'R' ) > -1 )
			return Wiki.parseFormattedNumber( arg );
		return Wiki.formatNum( arg );
	},
	'ifexist': function( obj, arg ) {
		var page = Wiki.getPage( arg );
		if( page && this.getAjaxItem( page, 'exists', [] ) ) return this.expandArg( obj, 1 ).trim();
		return this.expandArg( obj, 2 ).trim();
	},
	'time': function( obj, arg, local ) {
		var date = false;
		if( obj.parts.length > 1 ) {
			var secs = Date.parse( this.expandArg( obj, 1 ).trim() );
			if( secs ) date = new Date( secs );
		} else date = local ? Wiki.getLocalDate() : Wiki.getUTCDate();
		if( date ) date = Wiki.formatDate( date, arg );
		if( date ) return date;
		var call = '{{#time' + ( local ? 'l' : '' ) + ':' + arg;
		if( obj.parts.length > 1 ) call += '|' + this.expandArg( obj, 1 );		
		return this.getAjaxItem( Wiki, 'getFunc', [call + '}}'] );
	},
	'timel': function( obj, arg ) {
		return this.parserFunctions['time'].call( this, obj, arg, true );
	},
	'tag': function( obj, arg ) {
		var tagname = arg.toLowerCase();
		var result = '<' + tagname;
		var inner = this.expandArg( obj, 1 );
		if( !Wiki.tagExists( tagname ) )
			return '<span class="error">Unknown extension tag "' + tagname + '"</span>';
		for( var i = 2; i < obj.parts.length; i++ ) {
			var split = this.splitPart( obj.parts[i] );
			if( !split ) continue;
			result += ' ' + this.expandArgName( obj, split[0], i ) + '="';
			result += this.expandArgValue( obj, split[1], i )
			          .replace( /^\s*["'](.*)["']\s*$/, '$1' ) + '"';
		}
		return result + '>' + inner + '</' + tagname + '>';
	},
	'padright': function( obj, arg ) {
		var len = obj.parts.length > 1 ? parseInt( this.expandArg( obj, 1 ).trim(), 10 ) : 0;
		var pad = obj.parts.length > 2 ? this.expandArg( obj, 2 ).trim() : '0';
		return Wiki.padString( arg, len, pad, 'right' );
	},
	'padleft': function( obj, arg ) {
		var len = obj.parts.length > 1 ? parseInt( this.expandArg( obj, 1 ).trim(), 10 ) : 0;
		var pad = obj.parts.length > 2 ? this.expandArg( obj, 2 ).trim() : '0';
		return Wiki.padString( arg, len, pad, 'left' );
	},
	'ns': function( obj, arg ) {
		var index = parseInt( arg, 10 );
		if( !index && arg !== '0' )
			index = Wiki.getNS( arg.toLowerCase().replace( /[ _]+/g, '_' ) );
		if( index === false ) return '[[:Template:Ns:' + arg + ']]';//FIXME
		return wgFormattedNamespaces[index];
	},
	'nse': function( obj, arg ) {
		return Wiki.titleencode( this.parserFunctions['ns'].call( this, obj, arg ) );
	},
	'localurl': function( obj, arg ) {
		var title = Wiki.getPage( arg );
		var query = this.expandArg( obj, 1 ).trim();
		if( !title ) return '[[:Template:Localurl:' + arg + ']]';//FIXME
		if( title.ns == -2 ) title = new Wiki.Page( 6, title.title );
		var dbk = Wiki.titleencode( title.ptitle );
		if( !query ) return wgArticlePath.replace( /\$1/, dbk );
		else return wgScriptPath + '/index.php?title=' + dbk + '&' + ( query == '-' ? '' : query );
	},
	'localurle': function( obj, arg ) {
		return Wiki.escapeXML( this.parserFunctions.localurl.call( this, obj, arg ) );
	},
	'fullurl': function( obj, arg ) {
		//TODO: Fragments
		return wgServer + this.parserFunctions.localurl.call( this, obj, arg );
	},
	'fullurle': function( obj, arg ) {
		return Wiki.escapeXML( this.parserFunctions.fullurl.call( this, obj, arg ) );
	},
	'int': function( obj, arg ) {
		if( !arg ) return '[[:Template:Int:' + arg + ']]';//FIXME
		var params = [];
		for( var i = 1; i < obj.parts.length; i++ )
			params.push( this.expandArg( obj, i ).trim() );
		var msg = this.getAjaxItem( Wiki, 'getMessage', [wgUserLanguage, arg] );
		return Wiki.insertParams( msg, params );
	},
/*	'displaytitle': function( obj, arg ) {
		return '{{DISPLAYTITLE:'+arg+'}}';
	},
	'defaultsort': function( obj, arg ) {
		return '{{DEFAULTSORT:'+arg+'}}';
	},*/
	'rel2abs': function( obj, arg ) {
		var from = this.expandArg( obj, 1 ).trim();
		if( !from ) from = this.contextPage.ptitle;
		var to = arg.replace( /[ \/]+$/, '' );
		if( !to || to == '.' ) return from;
		if( !to.match( /^\.?\.?\/|^\.\.$/ ) ) from = '';
		var fullpath = '/' + from + '/' + to + '/';
		fullpath = fullpath.replace( /\/(\.\/)+/g, '/' );
		fullpath = fullpath.replace( /\/\/+/g, '/' );
		fullpath = fullpath.replace( /^\/+|\/+$/g, '' );
		var bits = fullpath.split( '/' );
		var newbits = [];
		for( var i = 0; i < bits.length; i++ ) {
			if( bits[i] == '..' ) {
				if( !newbits.length )
					return '<strong class="error">Error: Invalid depth in path: "' + fullpath
						+ '" (tried to access a node above the root node)</strong>';
				newbits.pop();
			} else newbits.push( bits[i] );
		}
		return newbits.join( '/' );
	},
	'plural': function( obj, arg ) {
		var num = parseInt( Wiki.parseFormattedNumber( arg ), 10);
		if( num == 1 ) return this.expandArg( obj, 1 );
		return this.expandArg( obj, 2 ) || this.expandArg( obj, 1 );
	},
	'namespace' : function( obj, arg ) {
		var title = Wiki.getPage( arg );
		if( !title ) return '';
		return wgFormattedNamespaces[title.ns];
	},
	'fullpagename' : function( obj, arg ) {
		var title = Wiki.getPage( arg );
		if( !title ) return '';
		return title.ptitle;
	},
	'talkpagename' : function( obj, arg ) {
		var title = Wiki.getPage( arg );
		if( !title ) return '';
		return ( new Wiki.Page( title.ns | 1, title.title ) ).ptitle;
	}
};

/**************************************************************************************************
 * helper functions
 */

Wiki.defaulttags = ['nowiki', 'gallery', 'math', 'pre', 'noinclude', 'includeonly', 'onlyinclude'];

Wiki.tagExists = function( name ) {
	for( var i = 0; i < Wiki.defaulttags.length; i++ )
		if( name == Wiki.defaulttags[i] ) return true;
	for( var i = 0; i < LocalDefines.extensiontags.length; i++ )
		if( name == LocalDefines.extensiontags[i] ) return true;
	return false;
};

Wiki.getVarResult = function( name, title ) {
	var t = title.title, p = title.ptitle, ns = title.ns;
	switch( name ) {
		case 'pagename'			: return t;
		case 'pagenamee'		: return Wiki.titleencode( t );
		case 'namespace'		: return wgFormattedNamespaces[ns];
		case 'namespacee'		: return Wiki.titleencode( wgFormattedNamespaces[ns] );
		case 'talkspace'		: return wgFormattedNamespaces[ns | 1];
		case 'talkspacee'		: return Wiki.titleencode( wgFormattedNamespaces[ns | 1] );
		case 'subjectspace'		: return wgFormattedNamespaces[ns & -2];
		case 'subjectspacee'	: return Wiki.titleencode( wgFormattedNamespaces[ns & -2] );
		case 'fullpagename'		: return p;
		case 'fullpagenamee'	: return Wiki.titleencode( p );
		case 'subpagename'		:
		case 'subpagenamee'		: //TODO: Namespaces without subpages
								  var title = t.substring( t.lastIndexOf( '/' ) + 1 );
								  if( name == 'subpagename' ) return title;
								  else return Wiki.titleencode( title );
		case 'basepagename'		:
		case 'basepagenamee'	: var pos = t.indexOf( '/' );
								  var title = pos > -1 ? t.substring( 0, pos ) : t;
								  if( name == 'basepagename' ) return title;
								  else return Wiki.titleencode( title );
		case 'talkpagenamee'	:
		case 'talkpagename'		: var title = new Wiki.Page( ns | 1, t ).ptitle;
								  if( name == 'talkpagename' ) return title;
								  else return Wiki.titleencode( title );
		case 'subjectpagename'	:
		case 'subjectpagenamee'	: var title = new Wiki.Page( ns & -2, t ).ptitle;
								  if( name == 'talkpagename' ) return title;
								  else return Wiki.titleencode( title );
	}

	if( name.indexOf( 'current' ) === 0 ) {
		var date = Wiki.getUTCDate();
		var name = name.substring( 7 );
	} else if( name.indexOf( 'local' ) === 0 ) {
		var date = Wiki.getLocalDate();
		var name = name.substring( 5 );
	} else return '{{'+name.toUpperCase()+'}}';
	switch( name ) {	
		case 'year'         : return Wiki.formatDate( date, 'Y' );
		case 'month'        : return Wiki.formatDate( date, 'm' );
		case 'month1'       : return Wiki.formatDate( date, 'n' );
		case 'monthname'    : return Wiki.formatDate( date, 'F' );
		case 'monthnamegen' : return Wiki.formatDate( date, 'xg' );
		case 'monthabbrev'  : return Wiki.formatDate( date, 'M' );		
		case 'day'          : return Wiki.formatDate( date, 'j' );
		case 'day2'         : return Wiki.formatDate( date, 'd' );
		case 'dow'          : return Wiki.formatDate( date, 'N' );
		case 'dayname'      : return Wiki.formatDate( date, 'l' );
		case 'hour'         : return Wiki.formatDate( date, 'H' );
		case 'time'         : return Wiki.formatDate( date, 'H:i' );
		case 'timestamp'    : return Wiki.formatDate( date, 'YmdHis' );
	}
	return '{{'+name.toUpperCase()+'}}';
};

/**************************************************************************************************
 * ExpressionParser
 * ported from http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/ParserFunctions/Expr.php
 */

ExpressionParser = {
	eval : function( expr ) {
		var operands = [];
		var operators = [];
		
		var p = 0;
		var end = expr.length;
		var expectExpression = true;
		var numeric = '0123456789.';
		var whitespace = ' \t\n\r';

		while( p < end ) {
			if( operands.length > 100 || operators.length > 100 ) throw 'Stack exhausted';
			var ch = expr.charAt( p );
			var ch2= expr.substr( p, 2 );

			if( whitespace.indexOf( ch ) > -1 ) {
				p++;
				continue;
			} else if ( numeric.indexOf( ch ) > -1 ) {
				if( !expectExpression ) throw 'Unexpected number';
				var num = expr.substr( p ).match( /^[0123456789\.]*/ )[0];
				operands.push( parseFloat( num ) );
				p += num.length;
				expectExpression = false;
				continue;
			} else if( ch.match( /[A-Za-z]/ ) ) {
				var word = expr.substr( p ).match( /^[A-Za-z]*/ )[0].toLowerCase();
				p += word.length;
				switch( word ) {
					case 'e'	:
						if( !expectExpression ) break;
						operands.push( Math.E );
						expectExpression = false;
						continue;
					case 'pi'   :
						if( !expectExpression ) throw 'Unexpected number';
						operands.push( Math.PI );
						expectExpression = false;
						continue;
					case 'not'  :
					case 'sin'  :
					case 'cos'  :
					case 'tan'  :
					case 'asin' :
					case 'acos' :
					case 'atan' :
					case 'exp'  :
					case 'ln'   :
					case 'abs'  :
					case 'floor':
					case 'trunc':
					case 'ceil' :
						if( !expectExpression ) throw 'Unexpected '+word+' operator';
						operators.push( word );
						continue;
					case 'mod'  :
					case 'and'  :
					case 'or'   :
					case 'round':
					case 'div'  : break;
					default     : throw 'Unrecognised word "'+word+'"';
				}
			} else if( ch2 == '<=' || ch2 == '>=' || ch2 == '<>' || ch2 == '!=' ) {
				var word = ch2;
				p += 2;
			} else if( ch == '+' || ch == '-' ) {
				p++;
				if( expectExpression ) {
					operators.push( ch );
					continue;
				} else var word = ch+ch;
			} else if( ch == '*' || ch == '/' || ch == '^' || ch == '=' || ch == '<' || ch == '>' ) {
				p++;
				var word = ch;
			} else if( ch == '(' ) {
				if( !expectExpression ) throw 'Unexpected ( operator';
				operators.push( ch );
				p++;continue;
			} else if( ch == ')' ) {
				var i = operators.length - 1;
				while( i >= 0 && operators[i] != '(' ) {
					this.doOperation( operators[i], operands );
					operators.pop();i--;
				}
				if( i < 0 ) throw 'Unexpected closing bracket';
				operators.pop();
				expectExpression = false;
				p++;
				continue;
			} else throw 'Unrecognised punctuation character "'+ch+'"';

			if( expectExpression ) throw 'Unexpected '+word+' operator';
			var i = operators.length - 1;
			while( i >= 0 && this.precedence[word] <= this.precedence[operators[i]] ) {
				this.doOperation( operators[i], operands );
				operators.pop();i--;
			}
			operators.push( word );
			expectExpression = true;
		}
		
		var i = operators.length - 1;
		while( i >= 0 ) {
			if( operators[i] == '(' ) throw 'Unclosed bracket';
			this.doOperation( operators[i], operands );
			i--;
		}
		return operands[0];
	},
	doOperation : function( op, stack ) {
		if( stack.length < this.arity[op] ) throw 'Missing operand for '+op;
		var right = stack.pop();
		switch( op ) {
			case '-'	: stack.push( -right );return;
			case '+'	: stack.push( right );return;
			case '*'	: stack.push( stack.pop() * right );return;
			case 'div'  :
			case '/'	: if( right == 0 ) throw 'Division by zero';
						  stack.push( stack.pop() / right );return;
			case 'mod'	: if( right == 0 ) throw 'Division by zero';
						  right = right > 0 ? Math.floor( right ) : Math.ceil( right );
						  var left = stack.pop();
						  left = left >= 0 ? Math.floor( left ) : Math.ceil( left );
						  stack.push( left % right );return;
			case '++'	: stack.push( stack.pop() + right );return;
			case '--'	: stack.push( stack.pop() - right );return;
			case 'and'	: stack.push( stack.pop() && right ? 1 : 0 );return;
			case 'or'	: stack.push( stack.pop() || right ? 1 : 0 );return;
			case '='    : stack.push( stack.pop() == right ? 1 : 0 );return;
			case 'not'  : stack.push( right ? 0 : 1 );return;
			case 'round': var digits = Math.floor( right );
						  stack.push( Math.round(stack.pop() * Math.pow(10,digits))/Math.pow(10,digits) );
						  return;
			case '<'	: stack.push( stack.pop() < right ? 1 : 0 );return;
			case '>'	: stack.push( stack.pop() > right ? 1 : 0 );return;
			case '<='	: stack.push( stack.pop() <= right ? 1 : 0 );return;
			case '>='	: stack.push( stack.pop() >= right ? 1 : 0 );return;
			case '<>'	:
			case '!='	: stack.push( stack.pop() == right ? 0 : 1 );return;
			case 'e' 	: stack.push( stack.pop() * Math.pow(10,right) );return;
			case 'sin'	: stack.push( Math.sin( right ) );return;
			case 'cos'	: stack.push( Math.cos( right ) );return;
			case 'tan'	: stack.push( Math.tan( right ) );return;
			case 'asin'	: if( right < -1 || right > 1 ) throw 'Invalid argument for asin: < -1 or > 1';
						  stack.push( Math.asin( right ) );return;
			case 'acos'	: if( right < -1 || right > 1 ) throw 'Invalid argument for acos: < -1 or > 1';
						  stack.push( Math.acos( right ) );return;
			case 'atan'	: stack.push( Math.atan( right ) );return;
			case 'exp' 	: stack.push( Math.exp( right ) );return;
			case 'ln'	: if( right <= 0 ) throw 'Invalid argument for ln: <= 0';
						  stack.push( Math.log( right ) );return;
			case 'abs' 	: stack.push( Math.abs( right ) );return;
			case 'floor': stack.push( Math.floor( right ) );return;
			case 'ceil' : stack.push( Math.ceil( right ) );return;
			case 'trunc': stack.push( right >= 0 ? Math.floor( right ) : Math.ceil( right ) );return;
			case '^'	: stack.push( Math.pow(stack.pop(), right) );return;
		}
	},
	precedence : {
		'-':10,'+':10,'e':10,'sin':9,'cos':9,'tan':9,'asin':9,'acos':9,'atan':9,'exp':9,'ln':9,'abs':9,
		'floor':9,'trunc':9,'ceil':9,'not':9,'^':8,'*':7,'/':7,'div':7,'mod':7,'++':6,'--':6,
		'round':5,'=':4,'<':4,'>':4,'<=':4,'>=':4,'<>':4,'!=':4,'and':3,'or':2,'pi':0,'(':-1,')':-1
	},
	arity : {
		'-':1,'+':1,'e':2,'sin':1,'cos':1,'tan':1,'asin':1,'acos':1,'atan':1,'exp':1,'ln':1,'abs':1,
		'floor':1,'trunc':1,'ceil':1,'not':1,'^':2,'*':2,'/':2,'div':2,'mod':2,'++':2,'--':2,
		'round':2,'=':2,'<':2,'>':2,'<=':2,'>=':2,'<>':2,'!=':2,'and':2,'or':2
	}
};