Benutzer:P.Copp/scripts/parser.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
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/*************************************************************************************************
 * parser.js
 *
 * Wikitext parsing library for MediaWiki
 * To be documented...
 */

 if( !String.prototype.trim ) {
	String.prototype.trim = function() { return this.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
}
mw.api = new function() {
	// returns an array [getdata, postdata]
	function encodeQuery( obj ) {
		var getData = '';
		var postData = '';
		for( var name in obj ) if( has.call( obj, name ) ) {
			if( typeof obj[name] === 'object' ) {
				obj[name] = obj[name].join( '|' );
			}
			var pair = encodeURIComponent( name ) + '=' + encodeURIComponent( obj[name] );
			if( pair.length + getData.length > 2000 ) {
				postData += ( postData && '&' ) + pair;
			} else {
				getData += ( getData && '&' ) + pair;
			}
		}
		return [getData, postData];
	}
	function mergeQuery( q1, q2 ) {
		if( !q2 ) return q1;
		var res = q1 || {};
		for( var key in q2 ) if( has.call( q2, key ) ) {
			if( has.call( res, key ) ) {
				if( !typeof res[key] == 'object' ) res[key] = [res[key]];
				if( !typeof q2[key] == 'object' ) q2[key] = [q2[key]];
				var unique = {};
				for( var i = 0; i < res[key].length; i++ ) unique[res[key][i]] = true;
				for( var i = 0; i < q2[key].length; i++ ) {
					if( !unique[q2[key][i]] ) res[key].push( q2[key][i] );
				}
			} else res[key] = q2[key];
		}
		return res;
	};
	function walkTree( root, list, end ) {
		if( !end || end < 0 ) var end = ( end || 0 ) + list.length;
		var node = root;
		for( var i = 0; i < end; i++ ) {
			if( !has.call( node, list[i] ) ) node[list[i]] = {};
			node = node[list[i]];
		};
		return node;
	};

	// Ajax function
	this.call = function( query, callback ) {
		var data = encodeQuery( query );
		jQuery.ajax( {
			url : wgScriptPath + '/api.php?format=json&' + data[0],
			type : data[1] || query.action === 'edit' ? 'POST' : 'GET',
			success : callback,
			data : data[1] ? data[1] : undefined
		} );
	};
	var BatchQuery = this.BatchQuery = function() {
		this.items = {};
		this.isEmpty = true;
	};
	BatchQuery.prototype = {
		addItem : function( /* ... */ ) {
			this.isEmpty = false;
			var obj = walkTree( this.items, arguments, -1 );
			obj[arguments[arguments.length - 1]] = true;
		},
		load : function( callback, cache ) {
			if( this.isEmpty ) {
				callback();
				return;
			}
			this.queries = 0;
			this.callback = callback;
			this.cache = cache || mw.api.cache;
			this.doQueries();
		},
		doQueries : function() {
			var uniq = ( Math.random() * Math.pow( 2, 32 ) ).toString( 16 );
			var metaQ = [];
			var titleQ = [];
			var infoQ = false;
			for( var name in this.items ) if( has.call( this.items, name ) ) {
				switch( name ) {
					case 'page':
						this.addQueries( titleQ, this.items[name], 'titles', {
							action : 'query',
							prop : 'info|imageinfo',
							iiprop : 'url|mime|size'
						} );
						break;
					case 'text':
						this.addQueries( titleQ, this.items[name], 'titles', {
							action : 'query',
							rvprop : 'timestamp|content',
							prop : 'revisions|info|imageinfo',
							redirects : '',
							iiprop : 'url|mime|size'
						} );
						break;
					case 'message':
						for( var lang in this.items[name] ) if( this.items[name].hasOwnProperty( lang ) ) {
							this.addQueries( metaQ, this.items[name][lang], 'ammessages', {
								action : 'query',
								meta : ['allmessages'],
								amlang : lang
							} );
						}
						break;
					case 'magicwords':
						infoQ = mergeQuery( infoQ, {
							action : 'query',
							meta : ['siteinfo'],
							siprop : ['magicwords','general']
						} );
						break;
					case 'languages':
						infoQ = mergeQuery( infoQ, {
							action : 'query',
							meta : ['siteinfo'],
							siprop : ['languages']
						} );
						break;
					case 'parsedText':
						var arr = ['<div>'];
						for( var text in this.items[name] ) if( this.items[name].hasOwnProperty( text ) ) {
							arr.push( '\n' + text );
						}
						arr.push( '\n</div>' );
						this.doQuery( {
							action : 'parse',
							prop : 'text',
							text : arr.join( uniq ),
							uniq : uniq
						} );
						break;
					case 'expandedText':
						var arr = [];
						for( var text in this.items[name] ) if( this.items[name].hasOwnProperty( text ) ) {
							arr.push( text );
						}
						this.doQuery( {
							action : 'expandtemplates',
							text : arr.join( uniq ),
							uniq : uniq
						} );
						break;
				}
			}
			if( titleQ[0] ) {
				titleQ[0].intoken = 'edit';
			}
			metaQ[0] = mergeQuery( metaQ[0], infoQ );
			for( var i = 0; i < metaQ.length; i++ ) {
				this.doQuery( mergeQuery( metaQ[i], titleQ[i] ) );
			}
			for( ; i < titleQ.length; i++ ) this.doQuery( titleQ[i] );
		},
		addQueries : function( arr, keys, name, query ) {
			query[name] = [];
			for( var key in keys ) if( has.call( keys, key ) ) {
				query[name].push( key );
				if( query[name].length == 50 ) {
					arr.push( mergeQuery( false, query ) );
					query[name] = [];
				}
			}
			if( query[name].length ) arr.push( query );
		},
		doQuery : function( query ) {
			if( !query ) return;
			this.queries++;
			var that = this;
			mw.api.call( query, function( res ) {
				that.cache.storeResult( res, query );
				if( !--that.queries && that.callback ) {
					that.callback();
				}
			} );
		}
	};

	var Cache = this.Cache = function() {
		this.items = {};
	}
	Cache.prototype = {
		getItem : function( type /* ,... */ ) {
			if( type === 'text' ) {
				return this.getText( arguments[1] );
			}
			var obj = walkTree( this.items, arguments, -1 );
			var key = arguments[arguments.length - 1];
			return has.call( obj, key ) ? obj[key] : null;
		},
		getText : function( title ) {
			var pi = this.getItem( 'page', title );
			if( pi && pi.redirect ) {
				pi = this.getItem( 'page', pi.redirect );
			}
			return pi && pi.missing !== '' && pi.invalid !== '' && ( pi.revisions || null ) && ( pi.revisions[0]['*'] || '' );
		},
		getTarget : function( title ) {
			var pi = this.getItem( 'page', title );
			return ( pi && pi.redirect ) || title;
		},
		getItemNow : function( /* ..., callback */ ) {
			var cb = arguments[arguments.length - 1];
			arguments.length--;
			var item = this.getItem.apply( this, arguments );
			if( item === null ) {
				var that = this, args = arguments;
				var bq = new BatchQuery;
				bq.addItem.apply( bq, args );
				bq.load( function() {
					cb( that.getItem.apply( that, args ) );
				}, this );
			} else {
				cb( item );
			}
		},
		storeItem : function( /* ... */ ) {
			var obj = walkTree( this.items, arguments, -2 );
			var key = arguments[arguments.length - 2];
			var val = arguments[arguments.length - 1];
			if( typeof val === 'object' && has.call( obj, key ) ) {
				obj[key] = jQuery.extend( obj[key], val );
			} else {
				obj[key] = val;
			}
		},
		storeResult : function( res, query ) {
			if( !res ) return;
			if( res.query ) {
				var q = res.query;
				if( q.pages ) {
					for( var pid in q.pages ) if( has.call( q.pages, pid ) ) {
						var p = q.pages[pid];
						this.storeItem( 'page', p.title, p );
						if( has.call( p, 'edittoken' ) ) {
							this.storeItem( 'token', p.edittoken );
						}
					}
				}
				if( q.redirects ) {
					var r = q.redirects;
					for( var i = 0; i < r.length; i++ ) {
						this.storeItem( 'page', r[i].from, { redirect : r[i].to } );
					}
				}
				if( q.normalized ) {
					var n = q.normalized;
					for( var i = 0; i < n.length; i++ ) {
						this.storeItem( 'page', n[i].from, { normalized : n[i].to } );
					}
				}
				if( q.general ) {
					this.storeItem( 'general', q.general );
				}
				if( q.magicwords ) {
					this.storeItem( 'magicwords', q.magicwords );
				}
				if( q.languages ) {
					var langs = {};
					for( var i = 0; i < q.languages.length; i++ ) {
						langs[q.languages[i].code] = q.languages[i]['*'];
					}
					this.storeItem( 'languages', langs );
				}
				if( q.allmessages ) {
					var lang = ( query && query.amlang ) || wgContentLanguage;
					for( var i = 0; i < q.allmessages.length; i++ ) {
						this.storeItem( 'message', lang, q.allmessages[i].name, q.allmessages[i]['*'] );
					}
				}
			}
			if( res.expandtemplates ) {
				var inputTexts = query.text.split( query.uniq );
				var outputTexts = res.expandtemplates['*'].split( query.uniq );
				if( inputTexts.length != outputTexts.length ) throw 'Unmatched results for expandtemplates';
				for( var i = 0; i < inputTexts.length; i++ ) {
					this.storeItem( 'expandedText', inputTexts[i], outputTexts[i] );
				}
			}
			if( res.parse ) {
				var inputTexts = query.text.split( query.uniq );
				var outputTexts = res.parse['text']['*'].split( query.uniq );
				if( inputTexts.length != outputTexts.length ) throw 'Unmatched results for parse';
				for( var i = 0; i < inputTexts.length; i++ ) {
					this.storeItem( 'parsedText', inputTexts[i].substring( 1 ), outputTexts[i].replace( /^\n/, '' ) );
				}
			}
			if( has.call( query, 'titles' ) ) {
				var titles = query.titles.split( '|' );
				for( var i = 0; i < titles.length; i++ ) {
					if( !has.call( this.items, 'page' ) || !has.call( this.items.page, titles[i] ) ) {
						this.storeItem( 'page', titles[i], {} );
					}
				}
			}
		}
	};
	this.cache = new Cache;
	var has = Object.prototype.hasOwnProperty;
}();

mw.parser = new function() {
	// Default namespaces
	var canonicalNsIds = {
		'media'                : -2,
		'special'              : -1,
		''                     :  0,
		'talk'                 :  1,
		'user'                 :  2,
		'user_talk'            :  3,
		'project'              :  4,
		'project_talk'         :  5,
		'file'                 :  6,
		'file_talk'            :  7,
		'mediawiki'            :  8,
		'mediawiki_talk'       :  9,
		'template'             : 10,
		'template_talk'        : 11,
		'help'                 : 12,
		'help_talk'            : 13,
		'category'             : 14,
		'category_talk'        : 15
	};
	var has = Object.prototype.hasOwnProperty;

	var Base = this.Base = function( title ) {
		this.contextTitle = this.getPage( title || wgPageName ).ptitle;
		this.utcDate = new Date();
		var offset = this.utcDate.getTimezoneOffset();
		this.utcDate.setTime( this.utcDate.getTime() + offset * 60000 );
		this.batch = new mw.api.BatchQuery;
	};
	Base.prototype = {
		extensionTags : [
			'categorytree',
			'charinsert',
			'gallery',
			'hiero',
			'imagemap',
			'inputbox',
			'math',
			'nowiki',
			'poem',
			'pre',
			'ref',
			'references',
			'source',
			'syntaxhighlight',
			'timeline'
		],
		linkTrailRX     : /[a-zäöüß]*/g,
		urlProtocols    : wgUrlProtocols.split( '|' ),
		articlePath     : wgArticlePath,
		capitalLinks    : true,
		contentLanguage : wgContentLanguage,
		userLanguage    : wgUserLanguage,
		isRTL           : document.body.className.indexOf( ' ltr ' ) === -1,
		script          : wgScript,
		server          : wgServer,
		stylePath       : stylepath,
		thumbSize       : 220, // TODO: extract from mw.user.options
		version         : wgVersion,
		cache           : mw.api.cache,
		namespaceNames  : wgFormattedNamespaces,
		namespaceIds    : jQuery.extend( {}, canonicalNsIds, wgNamespaceIds ),
		numberTransformTable : { ',' : '.', '.' : ',' },

		getExtensionTags   : function() { return this.extensionTags; },
		getLinkTrailRX     : function() { return this.linkTrailRX; },
		getUrlProtocols    : function() { return this.urlProtocols; },
		getContextTitle    : function() { return this.contextTitle; },
		getArticlePath     : function() { return this.articlePath; },
		getCapitalLinks    : function() { return this.capitalLinks; },
		getContentLanguage : function() { return this.contentLanguage; },
		getRTL             : function() { return this.isRTL; },
		getScript          : function() { return this.script; },
		getServer          : function() { return this.server; },
		getStylePath       : function() { return this.stylePath; },
		getThumbSize       : function() { return this.thumbSize; },
		getUTCDate         : function() { return this.utcDate; },
		getVersion         : function() { return this.version; },

		getItem : function( /* ... */ ) {
			var item = this.cache.getItem.apply( this.cache, arguments );
			if( item === null ) {
				this.batch.addItem.apply( this.batch, arguments );
			}
			return item;
		},
		getMessage : function( lang, key, params ) {
			var msg = this.getItem( 'message', lang, key );
			if( msg && params ) {
				// Replace message parameters "$1"...
				if( typeof params === 'string' ) {
					return msg.replace( /\$1/g, params );
				} else {
					for( var i = 0; i < params.length; i++ ) {
						var rx = new RegExp( '\\$'+ ( i + 1 ), 'g' );
						msg = msg.replace( rx, params[i] );
					}
				}
			}
			return msg;
		},
		getContentMessage : function( key, params ) {
			return this.getMessage( this.contentLanguage, key, params );
		},
		getUserMessage : function( key, params ) {
			return this.getMessage( this.userLanguage, key, params );
		},
		getExpandedText : function( text ) {
			return this.getItem( 'expandedText', text );
		},
		getFileDimensions : function( page ) {
			var pi = this.getItem( 'page', page.ptitle );
			if( pi && has.call( pi, 'imagerepository' ) ) {
				var ii = pi.imageinfo;
				return ii ? { width : ii[0].width, height : ii[0].height } : false;
			} else if( pi === null ) {
				return { width : 71, height : 92 };
			} else {
				return false;
			}
		},
		getFileUrl : function( page ) {
			var pi = this.getItem( 'page', page.ptitle );
			return pi && pi.imageinfo && pi.imageinfo[0].url;
		},
		getLanguageName : function( code ) {
			var langs = this.getItem( 'languages' );
			return langs && has.call( langs, code ) && langs[code];
		},
		getLocalDate : function() {
			if( !this.localDate ) {
				var general = this.getItem( 'general' );
				this.localDate = general && new Date( this.utcDate.getTime() + general.timeoffset * 60000 );
			}
			return this.localDate;
		},
		getPageExists : function( page ) {
			if( page.iw || !page.title || page.ns === -1 ) {
				return true;
			} else {
				// TODO: Media NS
				var pi = this.getItem( 'page', page.ptitle );
				return pi && pi.missing !== '' && pi.invalid !== '';
			}
		},
		getRedirectTarget : function( page ) {
			var pi = this.getItem( 'page', page.ptitle );
			return pi && ( pi.redirect === '' || pi.redirect );
		},
		getParsedText : function( text ) {
			return this.getItem( 'parsedText', text );
		},
		getPageText : function( page ) {
			return this.getItem( 'text', page.ptitle );
		},
		getMagicWords : function() {
			if( this.mag ) {
				return this.mag;
			}
			var mi = this.getItem( 'magicwords' );
			if( !mi ) {
				return null;
			}
			this.mag = {
				'var'        : [{}, {}],
				func         : [{}, {}],
				mod          : [{}, {}],
				underscore   : [{}, {}],
				image        : [{}, {}],
				other        : [{}, {}],
				imageRX      : [],
				underscoreRX : []
			};
			for( var i = 0; i < mi.length; i++ ) {
				var flags = magicWordFlags[mi[i].name];
				var cs = mi[i]['case-sensitive'] === '' ? 1 : 0;
				for( var j = 0; j < mi[i].aliases.length; j++ ) {
					var alias = cs ? mi[i].aliases[j] : mi[i].aliases[j].toLowerCase();
					if( !flags ) {
						this.mag.other[cs][alias] = mi[i].name;
					}
					if( flags & MAG_VARIABLE ) {
						this.mag['var'][cs][alias] = mi[i].name;
					}
					if( flags & MAG_PFUNC ) {
						this.mag.func[cs][alias.replace( /:$/, '' )] = mi[i].name;
					}
					if( flags & MAG_PFUNC_HASH ) {
						this.mag.func[cs]['#' + alias.replace( /:$/, '' )] = mi[i].name;
					}
					if( flags & MAG_MODIFIER ) {
						this.mag.mod[cs][alias] = mi[i].name;
					}
					if( flags & MAG_UNDERSCORE ) {
						this.mag.underscore[cs][alias] = mi[i].name;
						this.mag.underscoreRX.push( magic2RX( alias, cs ) );
					}
					if( flags & MAG_IMG_PARAM ) {
						this.mag.image[cs][alias.replace( /\$1/, '' )] = mi[i].name.substring( 4 );
						this.mag.imageRX.push( magic2RX( alias, cs, 'triple' ) );
					}
				}
			}
			this.mag.imageRX = new RegExp( '^(?:' + this.mag.imageRX.join( '|' ) + ')$' );
			this.mag.underscoreRX = new RegExp( this.mag.underscoreRX.join( '|' ), 'g' );
			return this.mag;
		},
		matchImageParam  : function( s ) {
			var mag = this.getMagicWords();
			if( !mag ) {
				return null;
			}
			var match = s.match( mag.imageRX );
			if( !match ) {
				return ['caption', s];
			}
			for( var i = 1; i < match.length; i += 3 ) {
				if( match[i] || match[i + 2] ) {
					return [this.matchMagic( 'image', match[i] + match[i + 2] ), match[i + 1].trim()];
				}
			}
		},
		matchMagic : function( type, s ) {
			var mag = this.getMagicWords();
			if( !mag ) {
				return null;
			}
			if( has.call( mag[type][1], s ) ) {
				return mag[type][1][s];
			}
			var lc = s.toLowerCase();
			return has.call( mag[type][0], lc ) && mag[type][0][lc];
		},
		getNamespaceName : function( index ) {
			return has.call( this.namespaceNames, index ) && this.namespaceNames[index];
		},
		getNamespaceIndex : function( name ) {
			return has.call( this.namespaceIds, name ) && this.namespaceIds[name];
		},
		getThumbUrl : function( page, width ) {
			var url = this.getFileUrl( page );
			if( !url ) {
				return url;
			}
			var ext = page.title.substring( page.title.lastIndexOf( '.' ) + 1 );
			var force = ext === 'svg' || ext === 'bmp';
			if( this.getFileDimensions( page ).width <= width && !force ) {
				return url;
			}
			var pos = url.lastIndexOf( '/' );
			pos = url.lastIndexOf( '/', pos - 1 );
			pos = url.lastIndexOf( '/', pos - 1 );
			return url.substring( 0, pos ) + '/thumb' + url.substring( pos ) + '/' + width + 'px-'
				+ this.phpUrlencode( page.title ).replace( /\+/g, '_' ) + ( force ? '.png' : '' );
		},
		formatNum : function( s, nocommafy ) {
			var s = s + '';

			//First step: commafy
			if( !nocommafy ) {
				var rx = /(^|[^\d\.])(\d{4,})/g, m;
				var result = '', index = 0;
				while( m = rx.exec( s ) ) {
					result += s.substring( index, m.index + m[1].length );
					index = m.index + m[0].length;
					result += commafy( m[2] );
				}
				s = result + s.substring( index );
			}

			//Second step: transform to local representation
			if( !this.rvTable ) {
				this.rvTable = {};
				for( i in this.numbertransformTable ) if( has.call( this.numberTransformTable, i ) ) {
					this.rvTable[this.numberTransformTable[i]] = i;
				}
			}
			var local = '';
			for( var i = 0; i < s.length; i++ ) {
				var c = s.charAt( i );
				local += has.call( this.rvTable, c ) ? this.rvTable[c] : c;
			}
			return local;
			function commafy( s ) {
				for( var i = s.length % 3, bits = []; i <= s.length; i += 3 ) {
					if( i > 0 ) {
						bits.push( s.substring( i - 3, i ) );
					}
				}
				return bits.join( ',' );
			}
		},
		parseFormattedNumber : function( s ) {
			var plain = '';
			for ( var i = 0; i < s.length; i++ ) {
				var c = s.charAt( i );
				plain += has.call( this.numberTransformTable, c )
					? this.numberTransformTable[c]
					: c;
			}
			return plain.replace( /,/g, '' );
		},
		lc : function( s ) { return s.toLowerCase(); },
		uc : function( s ) { return s.toUpperCase(); },
		lcfirst : function( s ) {
			if( !s ) {
				return s;
			}
			return this.lc( s.charAt( 0 ) ) + s.substring( 1 );
		},
		ucfirst : function( s ) {
			if( !s ) {
				return s;
			}
			return this.uc( s.charAt( 0 ) ) + s.substring( 1 );
		},
		// Construct a page info object from a string. Only some basic validity checks are performed,
		getPage : function( s, defaultNS, subpage ) {
			if( !s ) {
				return false;
			}
			try{
				s = decodeURIComponent( s );
			} catch( e ) {}

			// check for illegal chars
			if( s.match( /[<>\[\]\|\{\}\x00-\x1F\xFF\x7F]/ ) ) {
				return false;
			}

			// normalize whitespace
			// TODO: check if this can be replaced by a simple [_\s]
			s = s.replace( /[ _\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/g, ' ' ).trim();
			// strip bidi-characters
			s = s.replace( /[\u200e\u200f\u202a-\u202e]+/g, '' );

			// Fragments
			var fragment = false;
			var pos = s.indexOf( '#' );
			if( pos > -1 ) {
				fragment = s.substring( pos + 1 );
				s = s.substring( 0, pos ).trim();
			}

			if( subpage && s.charAt( 0 ) === '/' ) {
				s = this.getContextTitle() + s;
			}

			// Leading colon overrides the specified default namespace
			if( s.charAt( 0 ) === ':' ) {
				s = s.substring( 1 );
				defaultNS = 0;
			}

			// Namespace and interwiki
			var ns = false, iw = false;
			var pos = s.indexOf( ':' );
			if( pos > -1 ) {
				var prefix = this.lc( s.substring( 0, pos ).trim() ).replace( / /g, '_' );
				ns = this.getNamespaceIndex( prefix );
				if( ns === false && this.getLanguageName( prefix ) ) { // TODO: other interwikis
					ns = 0;
					iw = prefix;
				}
			}
			if( ns === false ) {
				ns = defaultNS || 0;
				var title = s.trim();
			} else {
				var title = s.substring( pos + 1 ).trim();
			}
			if( this.getCapitalLinks() ) {
				title = this.ucfirst( title );
			}
			if( !title && !iw && fragment === false ) {
				return false;
			}
			var nsName = this.getNamespaceName( ns );
			var ptitle = nsName ? nsName + ':' + title : title;
			return { ns : ns, title : title, ptitle : ptitle, iw : iw, fragment : fragment };
		}
	};

	var Preprocessor = this.Preprocessor = function() { Base.apply( this, arguments ); };
	Preprocessor.prototype = jQuery.extend( new Base, {
		// Mimic the behavior of PHP's urlencode function
		phpUrlencode : function( s ) {
			return encodeURIComponent(s)
				.replace( /%20/g, '+' ).replace( /!/g, '%21' )
				.replace( /'/g, '%27' ).replace( /\(/g, '%28' )
				.replace( /\)/g, '%29' ).replace( /\*/g, '%2A' )
				.replace( /~/g, '%7E' );
		},
		// This one imitates MediaWiki's wfUrlencode except that spaces are already changed to underscores
		titleEncode : function( s ) {
			return encodeURI( s.replace( / /g, '_' ) )
				.replace( /\+/g, '%2B' ).replace( /'/g, '%27' )
				.replace( /~/g, '%7E' ).replace( /#/g, '%23' )
				.replace( /&/g, '%26' ).replace( /=/g, '%3D' )
				.replace( /\?/g, '%3F' );
		},
		anchorEncode : function( s ) {
			return this.phpUrlencode( s ).replace( /%/g, '.' ).replace( /\+/g, '_' ).replace( /\.3A/g, ':' );
		},

		padString : function( s, len, pad, dir ) {
			if( !pad ) {
				return s;
			}
			var remain = Math.min( len, 500 ) - ( s + '' ).length;
			var padding = '';
			while( remain > 0 ) {
				padding += pad.substring( 0, remain );
				remain -= pad.length;
			}
			return dir === 'right' ? s + padding : padding + s;
		},

		/**************************************************************************************************
		 * 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
		 * }
		 */
		preprocess : function( text, forInclusion ) {
			if( typeof text !== 'string' ) {
				return text;
			}

			var start = 0;
			var stack = [];
			var top = new PPNode( 'root', text, 0 );
			var headings = 0;
			var enableOnlyInclude = false;
			var match, pos, node;

			//Line 145-156
			if( forInclusion ) {
				pos = text.indexOf( '<onlyinclude>' );
				if( pos > -1 && text.indexOf( '</onlyinclude>' ) > -1 ) {
					enableOnlyInclude = true;
					node = new PPNode( 'ignore', text, 0 );
					node.append( text.substring( 0, pos + 13 ) );
					top.append( node.finish( pos + 13 ) );
					start = pos + 13;
				}
			}
			var ignoredtag = forInclusion ? /includeonly/i : /noinclude|onlyinclude/i;
			var ignoredelement = forInclusion ? 'noinclude' : 'includeonly';

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

			for( ; ; ) {
				regex.lastIndex = start;
				match = regex.exec( text );
				var s = match[0];

				if( s == '<!--' ) { //Comment found
					var span = getCommentSpan( match.index );
					top.append( text.substring( start, span[0] ) );
					start = span[1];
					node = new PPNode( 'comment', text, span[0] );
					node.append( text.substring( span[0], span[1] ) );
					top.append( node.finish( span[1] ) );
					continue;
				}

				//Process all text between the last and the current token
				if( match.index > start ) {
					top.append( text.substring( start, match.index ) );
				}
				start = regex.lastIndex;

				if( ( match[1] || !s ) && top.type == 'h' ) {
					//Newline or EOT found
					//Check if we can close a heading
					var next = stack.pop();
					if( top.closing ) {
						//Some extra info for headings
						top.index = ++headings;
						top.level = Math.min( top.count, top.closing, 6 );
						next.append( top.finish( match.index ) );
					} else {
						//No correct closing, break the heading and continue
						top.breakAndAppendTo( next );
					}
					top = next;
				}
				if( !s ) {
					break; //End of text
				}
				if( match[1] || match[3] ) {
					if( match[1] ) {
						top.append( '\n' );
					}
					if( match[2] || match[3] ) {
						//Check if we can open a heading
						var len = ( match[2] || match[3] ).length;
						//Line 352-355: Single '=' within a template part isn't treated as heading
						if( len > 1 || top.type != '{' || top.parts.length == 1 || top.cur.split ) {
							stack.push( top );
							top = new PPNode( 'h', text, match.index + ( match[1] ? 1 : 0 ), len );
							//Line 447-455: More than two '=' means we already have a correct closing
							top.closing = Math.floor( ( len - 1 ) / 2 );
						}
						top.append( match[2] || match[3] );
					}
					continue;
				}

				if( match[4] ) { //Open <tag /?> found
					if( match[4].match( ignoredtag ) ) {
						node = new PPNode( 'ignore', text, match.index );
						node.append( s );
						top.append( node.finish( start ) );
						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
						top.append( s );
					} else {
						if( lc === ignoredelement ) {
							node = new PPNode( 'ignore', text, match.index );
						} else {
							node = new PPNode( 'ext', text, match.index );
							node.extname = lc;
						}
						node.append( s );
						if( s.charAt( s.length - 2 ) == '/' ) {
							//Immediately closed tag (e.g. <nowiki />)
							top.append( node.finish( start ) );
						} else {
							//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 ) {
								node.parts.push( node.cur = [] );
							}
							//Search for the matching closing tag
							var endRX = RegExp( '<\\/' + lc + '\\s*>|$', 'ig' );
							endRX.lastIndex = start;
							var endMatch = endRX.exec( text );
							node.append( text.substring( start, endMatch.index ) );
							if( lc !== ignoredelement ) {
								node.parts.push( node.cur = [] );
							}
							node.append( endMatch[0] );
							start = endRX.lastIndex;
							top.append( node.finish( start ) );
						}
					}
					continue;
				} else if( match[5] ) { //Close </tag> found
					if( match[5].match( ignoredtag ) ) {
						node = new PPNode( 'ignore', text, match.index );
						node.append( s );
						top.append( node.finish( start ) );
					} else if( enableOnlyInclude && s == '</onlyinclude>' ) {
						//For onlyinclude, the closing tag is the start of the ignored part
						node = new PPNode( 'ignore', text, match.index );
						pos = text.indexOf( '<onlyinclude>', start );
						if( pos === -1 ) {
							pos = text.length - 13;
						}
						node.append( text.substring( match.index, pos + 13 ) );
						top.append( node.finish( pos + 13 ) );
						start = pos + 13;
					} else {
						//We don't have a matching opening tag, so output the closing literally
						top.append( s );
					}
					continue;
				}
				//Special token found: '|', {+, [+, ]+, }+
				var ch = s.charAt( 0 );
				if( ch == '|' ) {
					//For brace nodes, start a new part
					if( top.type == '[' || top.type == '{' ) {
						top.parts.push( top.cur = [] );
					} else {
						top.append( s );
					}
				} else if( ch == '{' || ch == '[' ) {
					stack.push( top );
					top = new PPNode( ch, text, match.index, s.length );
				} else { // '}' or ']'
					//Closing brace found, try to close as many nodes as possible
					var open = ch == '}' ? '{' : '[';
					var len = s.length;
					while( top.type == open && 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
							top.type = open == '[' ? 'link' : mc == 2 ? 'template' : 'tplarg';
							if( top.count >= 2 ) {
								//if we're still open, create a new parent and embed the node there
								var child = top;
								top = new PPNode( open, text, child.offset, child.count );
								top.append( child );
								//Correct the child offset by the number of remaining open braces
								child.offset += top.count;
								child.finish( match.index + s.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 ) {
								next.append( open );
							}
							top.offset += top.count;
							next.append( top.finish( match.index + s.length - len ) );
							top = next;
						}
					}
					//Remaining closing braces are added as plain text
					if( len ) {
						top.append( ( new Array( len + 1 ) ).join( ch ) );
					}
				}
			}
			//We've reached the end, expand any remaining open pieces
			stack.push( top );
			for( var i = 1; i < stack.length; i++ ) {
				stack[i].breakAndAppendTo( stack[0] );
			}
			return stack[0].finish( match.index );
			//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];
			}
		},
		expand : function( obj ) {
			if( obj === undefined ) {
				throw 'Impossible';
			}
			if( !obj.type ) {
				return this.expandText( obj );
			}
			var func = this[obj.type];
			try{
				var result = func.call( this, obj );
			} catch( e ) {
				if( !this.safeMode && e === safeModeMissing ) {
					var result = this.getReplacement( obj );
				} else {
					throw e;
				}
			}
			return result;
		},
		root : function( obj ) { return this.expandPart( obj.parts[0] ); },
		link : function( obj ) {
			return this.expandText( '[[' ) + this.expandParts( obj.parts, '|' ) + this.expandText( ']]' );
		},
		template : function( obj ) {
			return this.expandText( '{{' ) + this.expandParts( obj.parts, '|' ) + this.expandText( '}}' );
		},
		tplarg : function( obj ) {
			return this.expandText( '{{{' ) + this.expandParts( obj.parts, '|' ) + this.expandText( '}}}' );
		},
		h : function( obj ) { return this.expandPart( obj.parts[0] ); },
		comment : function( obj ) { return this.expandText( obj.parts[0][0] ); },
		ignore : function( obj ) { return this.expandText( obj.parts[0][0] ); },
		ext : function( obj ) { return this.expandParts( obj.parts ); },
		getReplacement : function( obj ) {
			return ( new Preprocessor ).expand( obj );
		},
		expandText : function( s ) { return s; },
		expandPart : function( part, side ) {
			var start = side === 'r' ? part.split + 1 : 0;
			var end = side === 'l' ? part.split : part.length;
			if( start === end - 1 ) {
				return this.expand( part[start] );
			}
			var result = '';
			for( var i = start; i < end; 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 ) {
					result += this.expandText( joiner );
				}
				result += this.expandPart( parts[i] );
			}
			return result;
		},
		extractParams : function( obj ) {
			var params = { //numbered and named arguments must be stored separately
				numbered : {},
				named    : {},
				obj      : obj
			};
			var num = 0;
			for( var i = 1; i < obj.parts.length; i++ ) {
				if( obj.parts[i].split ) {
					var name = this.expandArgSafe( obj, i, 'l' );
					params.named[name] = i;
				} else {
					params.numbered[++num] = 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 === undefined ) {
					continue;
				}
				if( param.length !== undefined ) {
					return param; //cached
				}
				//Param exists, but not yet expanded. Expand it and put the result in the cache
				params[type][name] = this.expandArgSafe( params.obj, param, i ? 'r' : '' );
				return params[type][name];
			}
			return false;
		},
		expandArg : function( obj, num, side ) {
			var part = obj.parts[num];
			if( !part ) {
				return '';
			}
			return side ? this.expandPart( part, side ).trim() : this.expandPart( part );
		},
		expandArgSafe : function( obj, num, side ) {
			if( this.safeMode ) {
				return this.expandArg( obj, num, side );
			}
			this.safeMode = true;
			try{
				var result = this.expandArg( obj, num, side );
			} catch( e ) {
				this.safeMode = false;
				throw e;
			}
			this.safeMode = false;
			return result;
		}
	} );
	var pp = new Preprocessor;
	this.getPage = function() { return pp.getPage.apply( pp, arguments ); };
	this.expandWiki = function() { return pp.expand.apply( pp, arguments ); };
	this.preprocess = function() { return pp.preprocess.apply( pp, arguments ); };

	/**************************************************************************************************
	 * PreprocessorXML : Transforms a document tree to an XML string
	 */
	var PreprocessorXML = this.PreprocessorXML = function() { Preprocessor.apply( this, arguments ); };
	PreprocessorXML.prototype = jQuery.extend( new Preprocessor, {
		expandText : function( s ) {
			return s.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
		},
		root : function( obj ) {
			return XML( 'root', this.expandPart( obj.parts[0] ) );
		},
		template : function( obj ) {
			var attr = obj.lineStart ? ' lineStart="1"' : '';
			return this.XML( 'template', this.expandTemplateParts( obj.parts ), attr );
		},
		tplarg : function( obj ) {
			var attr = obj.lineStart ? ' lineStart="1"' : '';
			return this.XML( 'tplarg', this.expandTemplateParts( obj.parts ), attr );
		},
		h : function( obj ) {
			return this.XML( 'h', this.expandPart( obj.parts[0] ),
							 ' level="' + obj.level + '" i="' + obj.index + '"' );
		},
		comment : function( obj ) {
			return this.XML( 'comment', this.expand( obj.parts[0][0] ) );
		},
		ignore : function( obj ) {
			return this.XML( 'ignore', this.expand( obj.parts[0][0] ) );
		},
		ext : 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 );
		},
		expandTemplateParts : function( parts ) {
			var result = this.XML( 'title', this.expandPart( parts[0] ) );
			var num = 1;
			for( var i = 1; i < parts.length; i++ ) {
				if( parts[i].split ) {
					var content = this.XML( 'name', this.expandPart( parts[i], 'l' ) ) + this.expand( '=' );
					content += this.XML( 'value', this.expandPart( parts[i], 'r' ) );
				} else {
					var content = this.XML( 'name', '', ' index="' + ( num++ ) + '"' );
					content += this.XML( 'value', this.expandPart( parts[i] ) );
				}
				result += this.XML( 'part', content );
			}
			return result;
		}
	} );
	this.expandXML = function( obj ) {
		var pp = new PreprocessorXML;
		if( !obj.type ) {
			obj = pp.preprocess( obj );
		}
		return pp.expand.apply( pp, arguments );
	};

	/**************************************************************************************************
	 * TemplateExpander: 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.
	 */

	var TemplateExpander = this.TemplateExpander = function() {
		Preprocessor.apply( this, arguments );
		this.stack = [];
		this.domCache = {};
	};
	TemplateExpander.prototype = jQuery.extend( new Preprocessor, {
		template : function( obj ) {
			// Double brace expansion
			var result = false;
			var name = this.expandArgSafe( obj, 0 ).trim();
			// Modifiers: subst, safesubst, msg...
			var pos = name.indexOf( ':' );
			if( pos > -1 ) {
				var fname = this.safe( 'matchMagic', [ 'mod', name.substring( 0, pos + 1 ) ] );
				if( fname === 'subst' ) {
					return Preprocessor.prototype.template.apply( this, arguments );
				} else if( fname ) {
					name = name.substring( pos + 1 ).trim();
				}
			}
			// Variables
			var fname = this.safe( 'matchMagic', [ 'var', name ] );
			if( fname ) {
				result = this.expandFunc( obj, fname, false );
				if( result !== false ) {
					return result;
				}
			}
			// Functions
			var pos = name.indexOf( ':' );
			if( pos > -1 ) {
				var fname = this.safe( 'matchMagic', [ 'func', name.substring( 0, pos ) ] );
				if( fname ) {
					result = this.expandFunc( obj, fname, name.substring( pos + 1 ).trim() );
					if( result !== false ) {
						return result;
					}
				}
			}
			// Templates
			var page = this.getPage( name, 10, true );
			if( page && page.title && !page.iw && page.ns >= 0 ) {
				return this.expandTemplatePage( obj, page );
			} else {
				return Preprocessor.prototype.template.apply( this, arguments );
			}
		},

		tplarg : function( obj ) {
			// Triple brace expansion
			var name = this.expandArgSafe( obj, 0 ).trim();
			if( this.params ) {
				var current = this.params;
				this.params = this.stack.pop();
				try{
					var value = this.getParam( current, name );
				} catch( e ) {
					this.stack.push( this.params );
					this.params = current;
					throw e;
				}
				this.stack.push( this.params );
				this.params = current;
				if( value !== false ) {
					return value;
				}
			}
			// No matching param found, try the default
			if( obj.parts.length > 1 ) {
				return this.expandArg( obj, 1 );
			}
			return Preprocessor.prototype.tplarg.apply( this, arguments );
		},
		ignore : function() { return ''; },
		comment : function() { return ''; },
		expandFunc : function( obj, name, arg ) {
			var special = specialFuncs[name];
			if( special === 'page' ) {
				var arg = this.getPage( arg === false ? this.getContextTitle() : arg );
				if( !arg ) {
					return '';
				}
			} else if( special === 'utc' ) {
				var arg = this.getUTCDate();
				var name = 'var_' + name.substring( 7 );
			} else if( special === 'local' ) {
				var arg = this.safe( 'getLocalDate', [] );
				var name = 'var_' + name.substring( 5 );
			}
			var result = null;
			var func = this.parserFunctions[name];
			if( func ) {
				result = func.call( this, obj, arg );
			}
			if( result === null ) {
				var call = Preprocessor.prototype.template.call( this, obj );
				return this.safe( 'getExpandedText', [ call ] );
			} else if( result === false ) {
				return false;
			} else {
				//see Parser.php line 3026
				if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) {
					result = '\n' + result;
				}
				return result;
			}
		},
		expandTemplatePage : function( obj, page ) {
			if( !has.call( this.domCache, page.ptitle ) ) {
				var text = this.safe( 'getPageText', [ page ] );
				if( text === false && page.ns === 8 ) {
					var text = this.safe( 'getContentMessage', [ page.title.toLowerCase() ] );
				}
				if( text === false ) {
					return this.expandText( '[[:' + page.ptitle + ']]' );
				}
				this.domCache[page.ptitle] = this.preprocess( text, true );
			}
			var params = this.extractParams( obj );
			params.title = this.getRedirectTarget( page ) || page.ptitle;
			this.stack.push( this.params );
			this.params = params;
			try{
				var result = this.expand( this.domCache[page.ptitle] );
			} catch( e ) {
				this.params = this.stack.pop();
				throw e;
			}
			this.params = this.stack.pop();
			//see Parser.php line 3026
			if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) {
				result = '\n' + result;
			}
			return result;
		},
		ext : function( obj ) {
			var m = this.expandPart( obj.parts[0] ).match( /<([^\s\/>]*)([^>]*)>/ );
			var inner = obj.parts[1] ? this.expandPart( obj.parts[1] ) : false;
			var close = obj.parts[2] ? this.expandPart( obj.parts[2] ) : false;
			return this.expandExtension( m[1], m[2], inner, close );
		},
		expandExtension : function( name, attr, inner, close ) {
			return '<' + name + attr + '>' + ( inner || '' ) + ( close || '' );
		},
		safe : function( func, args ) {
			var result = this[func].apply( this, args );
			if( result === null ) {
				throw safeModeMissing;
			}
			return result;
		},
		getLocalUrl : function( page, query ) {
			var fragment = page.fragment ? '#' + this.anchorEncode( page.fragment ) : '';
			if( !page.title && !page.iw && !query ) {
				return '#' + this.anchorEncode( page.fragment );
			} else if( !query ) {
				return this.getArticlePath().replace( /\$1/, this.titleEncode( page.ptitle ) )
					+ fragment;
			} else {
				return this.getScript() + '?title=' + this.titleEncode( page.ptitle ) + '&' + query
					+ fragment;
			}
		},
		// from Language::sprintfDate. returns null if it finds characters it can't handle yet
		formatDate : function( date, format, rawToggle ) {
			if( !format ) {
				return format;
			}
			var result = '', num = false, raw = false;
			for( var i = 0; i < format.length; i++ ) {
				var code = format.charAt( i );
				if( code === 'x' ) {
					code += format.charAt( ++i );
				}
				switch( code ) {
					case 'xi':
					case 'xj':
					case 'xk':
					case 'xm':
					case 'xo':
					case 'xt':
					case 'xr':
					case 'xh':
					case 'z' :
					case 'W' :
					case 't' :
					case 'L' :
					case 'o' :
					case 'r' : //
						return null;
					case 'xg':
						result += this.safe( 'getContentMessage', [ monthGenMessages[date.getMonth()] ] );
						break;
					case 'D' :
						result += this.safe( 'getContentMessage', [ weekdayAbbrevMessages[date.getDay()] ] );
						break;
					case 'l' :
						result += this.safe( 'getContentMessage', [ weekdayMessages[date.getDay()] ] );
						break;
					case 'F' :
						result += this.safe( 'getContentMessage', [ monthMessages[date.getMonth()] ] );
						break;
					case 'M' :
						result += this.safe( 'getContentMessage', [ monthAbbrevMessages[date.getMonth()] ] );
						break;
					case 'xx': result += 'x'; break;
					case 'xn': raw = true; break;
					case 'xN': var rawToggle = !rawToggle; break;
					case 'd' : num = this.padString( date.getDate(), 2, '0' ); break;
					case 'j' : num = date.getDate(); break;
					case 'N' : num = date.getDay() || 7; break;
					case 'w' : num = date.getDay(); break;
					case 'm' : num = this.padString( date.getMonth() + 1, 2, '0' ); break;
					case 'n' : num = date.getMonth() + 1; break;
					case 'Y' : num = this.padString( date.getFullYear(), 4, '0' ); break;
					case 'y' : num = this.padString( date.getYear(), 2, '0' ); break;
					case 'a' : result += date.getHours() < 12 ? 'am' : 'pm'; break;
					case 'A' : result += date.getHours() < 12 ? 'AM' : 'PM'; break;
					case 'g' : num = date.getHours() % 12 || 12; break;
					case 'G' : num = date.getHours(); break;
					case 'h' : num = this.padString( date.getHours() % 12 || 12, 2, '0' ); break;
					case 'H' : num = this.padString( date.getHours(), 2, '0' ); break;
					case 'i' : num = this.padString( date.getMinutes(), 2, '0' ); break;
					case 's' : num = this.padString( date.getSeconds(), 2, '0' ); break;
					case 'c' : result += this.formatDate( date, 'Y-m-dTH:i:s+00:00', true ); break;
					case 'U' : num = Math.floor( date.getTime() / 1000 ); break;
					case '\\': result += format.charAt( ++i ) || '\\'; break;
					case '"' :
						var pos = format.indexOf( '"', i + 1 );
						if( pos > -1 ) {
							result += format.substring( i + 1, pos );
							i = pos;
						} else {
							result += '"';
						}
						break;
					default  : result += format.charAt( i );
				}
				if( num ) {
					if( raw || rawToggle ) {
						result += num;
						raw = false;
					} else {
						result += this.formatNum( num, true );
					}
					num = false;
				}
			}
			return result;
		},

		/**************************************************************************************************
		 * evalExpression
		 * ported from http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/ParserFunctions/Expr.php
		 */
		evalExpression : function( expr ) {
			var 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
			};
			var 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
			};
			expr = expr.replace( /&lt;/g, '<' ).replace( /&gt;/g, '>' ).replace( /&minus;/g, '-' ).replace( /−/g, '-' );
			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] !== '(' ) {
						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 && precedence[word] <= precedence[operators[i]] ) {
					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';
				}
				doOperation( operators[i], operands );
				i--;
			}
			return operands.length ? operands[0] : '';
			function doOperation( op, stack ) {
				if( stack.length < 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;
				}
			}
		},

		parserFunctions : {
			anchorencode : function( obj, arg ) { return this.anchorEncode( arg ); },
			basepagename : function( obj, page ) {
				var pos = page.title.indexOf( '/' );
				return pos > -1 ? page.title.substring( 0, pos ) : page.title;
			},
			basepagenamee : function( obj, page ) {
				return this.titleEncode( this.parserFunctions.basepagename.call( this, obj, page ) );
			},
			contentlanguage : function( obj, arg ) { return this.getContentLanguage(); },
			currentversion : function( obj, arg ) { return this.getVersion(); },
			directionmark : function( obj, arg ) { return this.getRTL() ? '\u200f' : '\u200e'; },
			defaultsort : function() { return ''; },
			displaytitle : function() { return ''; },
			expr: function( obj, arg ) {
				try{
					return this.evalExpression( arg ) + '';
				} catch( e ){
					return '<strong class="error">' + e + '</strong>';//TODO
				}
			},
			filepath : function( obj, arg ) {
				var page = this.getPage( arg, 6 );
				if( !page ) return '';
				return this.safe( 'getFileUrl', [ page ] );
			},
			formatnum: function( obj, arg ) {
				if( this.expandArgSafe( obj, 1 ).indexOf( 'R' ) > -1 ) { // TODO
					return this.parseFormattedNumber( arg );
				} else {
					return this.formatNum( arg );
				}
			},
			fullpagename : function( obj, page ) { return page.ptitle; },
			fullpagenamee	: function( obj, page ) { return this.titleEncode( page.ptitle ); },
			fullurl : function( obj, page ) {
				return this.getServer() + this.parserFunctions.localurl.call( this, obj, page );
			},
			fullurle : function( obj, arg ) {
				return mw.html.escape( this.parserFunctions.fullurl.call( this, obj, page ) );
			},
			gender : function( obj, arg ) {
				// TODO
				if( obj.parts.length > 3 ) {
					return this.expandArg( obj, 3 );
				}
				return this.expandArg( obj, 1 );
			},
			grammar : function( obj, arg ) { return this.expandArg( obj, 1 ); }, // TODO
			'if' : function( obj, arg ) {
				if( arg ) {
					return this.expandArg( obj, 1 ).trim();
				} else {
					return this.expandArg( obj, 2 ).trim();
				}
			},
			ifeq : function( obj, arg ) {
				var otherArg = this.expandArgSafe( obj, 1 ).trim();
				//PHP compares two strings numerical, if both are valid numerals. We have to mimic
				//this behavior here
				if( arg === otherArg || ( !isNaN( +arg ) && arg && otherArg && +arg === +otherArg ) ) {
					return this.expandArg( obj, 2 ).trim();
				} else {
					return this.expandArg( obj, 3 ).trim();
				}
			},
			iferror: function( obj, arg ) {
				if( arg.match( /<(?:strong|span|p|div)\s[^>]*\bclass="[^">]*\berror\b[^">]*"/ ) ) {
					return this.expandArg( obj, 1 ).trim();
				} else if( obj.parts.length > 2 ) {
					return this.expandArg( obj, 2 ).trim();
				} else {
					return arg;
				}
			},
			ifexist: function( obj, arg ) {
				var page = this.getPage( arg );
				if( page && this.safe( 'getPageExists', [ page ] ) ) {
					return this.expandArg( obj, 1 ).trim();
				} else {
					return this.expandArg( obj, 2 ).trim();
				}
			},
			ifexpr: function( obj, arg ) {
				try{
					var value = this.evalExpression( arg );
				} catch( e ){
					return '<strong class="error">' + e + '</strong>'; // TODO
				}
				if( value ) {
					return this.expandArg( obj, 1 ).trim();
				} else {
					return this.expandArg( obj, 2 ).trim();
				}
			},
			'int': function( obj, arg ) {
				if( !arg ) {
					return false;
				}
				var params = [];
				for( var i = 1; i < obj.parts.length; i++ ) {
					params.push( this.expandArgSafe( obj, i ).trim() );
				}
				return this.safe( 'getUserMessage', [ arg, params ] );
			},
			language : function( obj, arg ) {
				var name = this.safe( 'getLanguageName', [ arg.toLowerCase() ] );
				return name || arg;
			},
			lc: function( obj, arg ) { return arg.toLowerCase(); },
			lcfirst: function( obj, arg ) { return this.lcfirst( arg ); },
			localurl : function( obj, page ) {
				var query = this.expandArgSafe( obj, 1 ).trim();
				if( page.ns === -2 ) {
					var page = this.getPage( page.title, 6 );
				}
				return this.getLocalUrl( page, query );
			},
			localurle : function( obj, page ) {
				return mw.html.escape( this.parserFunctions.localurl.call( this, obj, page ) );
			},
			namespace : function( obj, page ) { return this.getNamespaceName( page.ns ); },
			namespacee : function( obj, page ) {
				return this.titleEncode( this.getNamespaceName( page.ns ) );
			},
			ns: function( obj, arg ) {
				var index = parseInt( arg, 10 );
				if( !index && arg !== '0' ) {
					index = this.getNamespaceIndex( arg.toLowerCase().replace( /[ _]+/g, '_' ) );
				}
				if( index === false ) {
					return false;
				} else {
					return this.getNamespaceName( index );
				}
			},
			nse: function( obj, arg ) {
				return this.titleEncode( this.parserFunctions.ns.call( this, obj, arg ) );
			},
			padleft: function( obj, arg ) {
				var len = obj.parts.length > 1 ? parseInt( this.expandArgSafe( obj, 1 ).trim(), 10 ) : 0;
				var pad = obj.parts.length > 2 ? this.expandArgSafe( obj, 2 ).trim() : '0';
				return this.padString( arg, len, pad, 'left' );
			},
			padright: function( obj, arg ) {
				var len = obj.parts.length > 1 ? parseInt( this.expandArgSafe( obj, 1 ).trim(), 10 ) : 0;
				var pad = obj.parts.length > 2 ? this.expandArgSafe( obj, 2 ).trim() : '0';
				return this.padString( arg, len, pad, 'right' );
			},
			pagename : function( obj, page ) { return page.title; },
			pagenamee : function( obj, page ) { return this.titleEncode( page.title ); },
			plural: function( obj, arg ) {
				// TODO
				var num = parseInt( this.parseFormattedNumber( arg ), 10 );
				if( num === 1 ) {
					return this.expandArg( obj, 1 ).trim();
				} else {
					return this.expandArgSafe( obj, 2 ).trim() || this.expandArg( obj, 1 ).trim();
				}
			},
			rel2abs: function( obj, arg ) {
				var from = this.expandArgSafe( obj, 1 ).trim();
				if( !from ) {
					from = this.getContextTitle();
				}
				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 )
							// TODO
							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( '/' );
			},
			subjectpagename : function( obj, page ) {
				return this.getPage( page.title, page.ns & -2 ).ptitle;
			},
			subjectpagenamee : function( obj, page ) {
				return this.titleEncode( this.parserFunctions.subjectpagename.call( this, obj, page ) );
			},
			subjectspace : function( obj, page ) { return this.getNamespaceName( page.ns & -2 ); },
			subjectspacee	: function( obj, page ) {
				return this.titleEncode( this.parserFunctions.subjectspace.call( this, obj, page ) );
			},
			subpagename : function( obj, page ) {
				//TODO: Namespaces without subpages
				return page.title.substring( page.title.lastIndexOf( '/' ) + 1 );
			},
			subpagenamee : function( obj, page ) {
				return this.titleEncode( this.parserFunctions.subpagename.call( this, obj, page ) );
			},
			'switch' : function( obj, arg ) {
				var found = false;
				var defaultFound = false;
				var switchDefault = false;
				for( var i = 1; i < obj.parts.length; i++ ) {
					if( obj.parts[i].split ) {
						var left = this.expandArgSafe( obj, i, 'l' );
						if( found || left === arg ) {
							return this.expandArg( obj, i, 'r' );
						} else if( defaultFound || left === '#default' /* TODO */ ) {
							switchDefault = i;
						}
					} else {
						var left = this.expandArgSafe( obj, i ).trim();
						if( left === arg ) {
							found = true;
						} else if( left === '#default' /* TODO */ ) {
							defaultFound = true;
						}
					}
				}
				if( !obj.parts[i - 1].split ) {
					return left;
				} else if( switchDefault !== false ) {
					return this.expandArg( obj, switchDefault, 'r' );
				} else {
					return '';
				}
			},
			tag: function( obj, arg ) {
				var tagName = arg.toLowerCase();
				var inner = this.expandArgSafe( obj, 1 );
				var tags = this.getExtensionTags();
				if( !jQuery.inArray( tagName, tags ) ) {
					return '<span class="error">Unknown extension tag "' + tagName + '"</span>'; // TODO
				}
				var attr = '';
				for( var i = 2; i < obj.parts.length; i++ ) {
					if( !obj.parts[i].split ) {
						continue;
					}
					attr += ' ' + this.expandArgSafe( obj, i, 'l' ) + '="';
					attr += this.expandArgSafe( obj, i, 'r' )
							  .replace( /^\s*["']([^"']*)["']\s*$/, '$1' ) + '"';
				}
				return this.expandExtension( tagName, attr, inner, '</' + tagName + '>' );
			},
			talkpagename : function( obj, page ) {
				return this.getPage( page.title, page.ns | 1 ).ptitle;
			},
			talkpagenamee : function( obj, page ) {
				return this.titleEncode( this.parserFunctions.talkpagename.call( this, obj, page ) );
			},
			talkspace : function( obj, page ) { return this.getNamespaceName( page.ns | 1 ); },
			talkspacee : function( obj, page ) { return this.titleEncode( this.getNamespaceName( page.ns | 1 ) ); },
			time: function( obj, arg, local ) {
				var date = false;
				if( obj.parts.length > 1 ) {
					var timeArg = this.expandArgSafe( obj, 1 ).trim();
					if( timeArg ) {
						var secs = Date.parse( timeArg );
						if( !secs ) {
							return null;
						}
						date = new Date( secs );
					}
				}
				if( !date ) {
					date = local ? this.safe( 'getLocalDate', [] ) : this.getUTCDate();
				}
				result = this.formatDate( date, arg );
				if( result ) {
					return result;
				} else {
					return null;
				}
			},
			timel: function( obj, arg ) {
				return this.parserFunctions.time.call( this, obj, arg, true );
			},
			titleparts : function( obj, arg ) {
				var page = this.getPage( arg );
				if( !page ) {
					return arg;
				}
				var bits = page.ptitle.split( '/', 25 );
				var offset = Math.max( 0, ( parseInt( this.expandArgSafe( obj, 2 ), 10 ) || 0 ) - 1 );
				var end = parseInt( this.expandArgSafe( obj, 1 ), 10 ) || 0;
				end = end > 0 ? offset + end : bits.length + end;
				return bits.slice( offset, end ).join( '/' );
			},
			uc: function( obj, arg ) { return this.uc( arg ); },
			ucfirst: function( obj, arg ) { return this.ucfirst( arg ); },
			urlencode: function( obj, arg ) {	return this.phpUrlencode( arg ); },
			var_day : function( obj, date ) { return this.formatDate( date, 'j' ); },
			var_day2 : function( obj, date ) { return this.formatDate( date, 'd' ); },
			var_dayname : function( obj, date ) { return this.formatDate( date, 'l' ); },
			var_dow : function( obj, date ) { return this.formatDate( date, 'N' ); },
			var_hour : function( obj, date ) { return this.formatDate( date, 'H' ); },
			var_month : function( obj, date ) { return this.formatDate( date, 'm' ); },
			var_month1 : function( obj, date ) { return this.formatDate( date, 'n' ); },
			var_monthabbrev : function( obj, date ) { return this.formatDate( date, 'M' ); },
			var_monthname : function( obj, date ) { return this.formatDate( date, 'F' ); },
			var_monthnamegen : function( obj, date ) { return this.formatDate( date, 'xg' ); },
			var_time : function( obj, date ) { return this.formatDate( date, 'H:i' ); },
			var_timestamp : function( obj, date ) { return this.formatDate( date, 'YmdHis' ); },
			var_week : function( obj, date ) { return this.formatDate( date, 'W' ); },
			var_year : function( obj, date ) { return this.formatDate( date, 'Y' ); }
		}
	} );
	this.expandTemplates = function( obj, title, cb ) {
		if( !obj.type ) {
			var expander = new TemplateExpander( title );
			obj = expander.preprocess( obj );
		}
		return iterate( TemplateExpander, 'expand', obj, title, cb );
	};

	/***********************************************************************************************
	 * Parser
	 */
	var Parser = this.Parser = function() {
		TemplateExpander.apply( this, arguments );
		this.markerPrefix = '\x7fUNIQ' + ( Math.random() * Math.pow( 2, 32 ) ).toString( 16 ) + '-';
		this.markerSuffix = '-QINU\x7f';
		this.markerRX = new RegExp( this.markerPrefix + '(\\d+)' + this.markerSuffix, 'g' );
		this.tocMarker = '<table id="' + this.markerPrefix + '"></table>';
		this.stripMarkers = [];
		this.autoNumber = 0;
		this.refs = {};
		this.refKey = 0;
		this.refsGroup = false;
		this.output = { underscores : {} };
	};
	Parser.prototype = jQuery.extend( new TemplateExpander, {
		expandExtension : function( /* ... */ ) {
			this.stripMarkers.push( arguments );
			return this.markerPrefix + ( this.stripMarkers.length - 1 ) + this.markerSuffix;
		},
		getReplacement : function( obj ) {
			return '';
		},
		parse : function( text ) {
			text = this.parseInline( text );
			text = this.doBlocks( text );
			text = this.fixEntities( text );
			this.output.text = text;
			return this.output;
		},
		parseInline : function( text ) {
			text = this.expand( this.preprocess( text, false ) );
			text = this.fixTags( text );
			text = this.doDoubleUnderscore( text );
			text = this.doQuotes( text );
			text = this.doLinks( text );
			return text;
		},
		doDoubleUnderscore : function( text ) {
			var that = this;
			var mag = this.getMagicWords();
			if( !mag ) {
				return text;
			}
			return text.replace( mag.underscoreRX, function( m ) {
				var name = that.matchMagic( 'underscore', m );
				if( name === 'toc' && !that.output.underscores[name] ) {
					var result = that.tocMarker;
				} else {
					var result = '';
				}
				that.output.underscores[name] = true;
				return result;
			} );
		},
		doQuotes : function( text ) {
			var m, rx = /<[^>]*>|''+|\n|$/g, result = '', last = 0, state = 0, fail = false;
			rx.lastIndex = 0;
			while( m = rx.exec( text ) ) {
				var l = m[0].length;
				result += text.substring( last, m.index );
				last = rx.lastIndex;
				if( !m[0] || m[0] === '\n' ) {
					if( state === 3 && !fail ) {
						result = result.substring( 0, result.lastIndexOf( '\n' ) + 1 );
						last = rx.lastIndex = text.lastIndexOf( '\n', m.index - 1 ) + 1;
						state = 0;
						fail = true;
					} else {
						fail = false;
						changeState( 0 );
						result += m[0];
						if( !m[0] ) {
							break;
						}
					}
				} else if( m[0].charAt( 0 ) == '<' ) {
					result += m[0];
				} else {
					if( l === 3 && fail ) {
						result += "'";
						l = 2;
						fail = false;
					}
					if( l === 4 ) {
						result += "'";
					}
					if( l > 5 ) {
						result += m[0].substring( 5 );
						l = 5;
					}
					changeState( state ^ ( l - 1 - ( l > 3 ) ) );
				}
			}
			return result;
			function changeState( newState ) {
				if( state & 1 ) {
					result += '</i>';
				}
				if( state & 2 && !( newState & 2 ) ) {
					result += '</b>';
				}
				if( newState & 2 && !( state & 2 ) ) {
					result += '<b>';
				}
				if( newState & 1 ) {
					result += '<i>';
				}
				state = newState;
			}
		},
		doLinks : function( text ) {
			var m, rx = /(<[^>]*>)|(\[\[)|(\]\])|$/g, result = '', last = 0;
			var link1 = false, link2 = false;
			var linkTrailRX = this.getLinkTrailRX();
			rx.lastIndex = 0;
			while( m = rx.exec( text ) ) {
				if( m[2] ) {
					if( link1 === false ) {
						result += this.doExtLinks( text.substring( last, m.index ) );
						link1 = m.index;
					} else if( link2 === false ) {
						link2 = m.index;
					} else {
						//We found the third opening in a row, so link1 is broken
						//There might have been correct links in between, though,  so we have to start over
						//at the last position
						last = link1;
						link1 = false;
						link2 = false;
						rx.lastIndex = last + 2;
					}
				} else if( m[3] && link1 !== false ) {
					if( link2 === false ) {
						var inner = text.substring( link1 + 2, m.index );
						last = m.index + 2;
						if( text.charAt( m.index + 2 ) == ']' && inner.indexOf( '[' ) > -1 ) {
							inner += ']';
							last++;
						}
						linkTrailRX.lastIndex = last;
						var trail = linkTrailRX.exec( text );
						last += trail[0].length;
						result += this.doDoubleBracket( inner, trail[0] );
						rx.lastIndex = m.index + m[0].length;
						link1 = false;
					} else {
						link2 = false;
					}
				} else if( !m[0] ) {
					if( link1 === false ) {
						result += this.doExtLinks( text.substring( last ) );
						break;
					} else {
						last = link1;
						link1 = false;
						link2 = false;
						rx.lastIndex = last + 2;
					}
				}
			}
			return result;
		},
		doDoubleBracket : function( inner, trail ) {
			if( inner.match( this.getUrlStartRX() ) ) {
				return '[' + this.doLinks( '[' + inner + ']' ) + ']' + trail;
			}
			var forcelink = inner.charAt( 0 ) == ':';
			var pos = inner.indexOf( '|' );
			var title = pos > -1 ? inner.substring( forcelink, pos ) : inner.substring( forcelink );
			// TODO: unstrip
			var page = this.getPage( this.decodeEntities( title ), 0, true );
			if( !page ) {
				return '[' + this.doLinks( '[' + inner + ']' ) + ']' + trail;
			}
			if( inner.substring( 0, 4 ) === 'file' ) {
				mw.parser;
			}
			if( !forcelink && page.ns == 6 ) {
				return this.makeImage( page, pos > -1 ? inner.substring( pos + 1 ) : '' ) + trail;
			} else if( !forcelink && page.ns == 14 ) {
				return this.makeCategory( page, pos > -1 ? inner.substring( pos + 1 ) : false ) + trail;
			} else if( !forcelink && page.iw ) {
				return this.makeInterwiki( page ) + trail;
			} else {
				return this.makeLink( page, ( pos > -1 ? inner.substring( pos + 1 ) : title ) +  trail );
			}
		},
		doExtLinks : function( text ) {
			var that = this;
			return text.replace( this.getExtLinkRX(), function( m, open, ourl, text, magType, magNum, isbn ) {
				if( ourl ) {
					var pos = ourl.search( /&[lg]t;/ );
					var url = pos > -1 ? ourl.substring( 0, pos ) : ourl;
					var prefix = '', trail = '';
					if( open && text ) {
						var type = 'text';
						var text = text.replace( /^ +|\]$/g, '' );
						if( pos > -1 ) {
							text = ourl.substring( pos ) + ' ' + text;
						}
						if( !text.trim() ) {
							text = '[' + that.formatNum( ++that.autoNumber ) + ']';
						}
					} else {
						var type = 'free';
						prefix = open;
						if( pos > -1 ) {
							trail = ourl.substring( pos );
						}
						var sep = ',;\\.:!?';
						if( url.indexOf( '(' ) === -1 ) {
							sep += ')';
						}
						for( var i = url.length - 1; sep.indexOf( url.charAt( i ) ) > -1; i-- );
						if( i < url.length - 1 ) {
							trail = url.substring( i + 1 ) + trail;
							url = url.substring( 0, i + 1 );
						}
						if( text ) {
							trail += text;
						}
						var text = url;
					}
					url = that.decodeEntities( url );
					url = url.replace( /[\[\]"\x00-\x20\x7f]/g, function( m ) { return that.phpUrlencode( m ); } );
					return prefix + that.makeExtLink( url, text, type ) + trail;
				} else if( magType ) {
					return that.makeMagicLink( magType, magNum );
				} else if( isbn ) {
					return that.makeISBNLink( isbn );
				} else {
					return m;
				}
			} );
		},

		makeImage : function( page, params ) {
			var ext = page.title.substring( page.title.length - 3 );
			if( ext === 'ogg' ) {
				return '';
			}
			var dim = this.getFileDimensions( page );
			var p = this.getImageParams( params );
			if( p.framed === '' || p.thumbnail === '' ) {
				if( p.alt === undefined && p.caption === '' ) {
					p.alt = page.title;
				} else {
					p.alt = '';
				}
			} else {
				if( p.alt === undefined ) {
					p.alt = p.caption || page.title;
				}
				p.title = p.caption;
			}
			if( p.width === undefined && dim ) {
				p.width = dim.width;
				if( p.thumbnail === '' || p.framed === '' || p.frameless === '' ) {
					if( p.upright !== undefined && !parseFloat( p.upright ) ) {
						p.upright = 0.75;
					}
					p.width = Math.min( p.width, Math.floor( ( this.getThumbSize() / 10 ) * ( parseFloat( p.upright ) || 1 ) ) * 10 );
				}
			}
			if( p.height && dim && ( p.width * dim.height > p.height * dim.width ) ) {
				var floatwidth = dim.width * p.height / dim.height;
				if( Math.round( Math.ceil( floatwidth ) * dim.height / dim.width ) > p.height ) {
					p.width = Math.floor( floatwidth );
				} else {
					p.width = Math.ceil( floatwidth );
				}
			}

			var prefix = '', postfix = '';
			if( p.align === 'center' ) {
				prefix = '<div class="center">';
				postfix = '</div>';
				p.align = 'none';
			}

			if( p.thumbnail === '' || p.framed === '' ) {
				return prefix + this.makeThumbImage( page, dim, p ) + postfix;
			}
			if( p.frameless === '' && dim && p.width > dim.width && ext !== 'svg' && ext !== 'bmp' ) {
				p.width = dim.width;
			}
			if( p.align ) {
				prefix += '<div class="float' + p.align + '">';
				postfix = '</div>' + postfix;
			}
			return prefix + this.makeTransformedImage( page, dim, p ) + postfix;
		},
		makeThumbImage : function( page, dim, p ) {
			if( p.align === undefined ) {
				p.align = 'right';
			}
			if( p.width === undefined ) {
				p.width = p.upright === undefined ? 180 : 130; // TODO: check if getThumbSize() should be used here
			}
			if( dim && p.framed === '' ) {
				p.width = dim.width;
			}
			var result = '<div class="thumb t' + p.align + '"><div class="thumbinner" style="width:' + ( p.width + 2 ) + 'px;">';
			result += this.makeTransformedImage( page, dim, p, 'thumbimage' );
			result += '  <div class="thumbcaption">';
			if( dim && p.framed === undefined ) {
				result += this.makeZoomIcon( page );
			}
			result += ( p.caption || '' ) + '</div></div></div>';
			return result;
		},
		makeTransformedImage : function( page, dim, p, imgclass ) {
			if( dim === false ) {
				return this.makeLink( page, p.caption || page.ptitle );
			}
			var link = {};
			if( p.link === undefined ) {
				link.href = this.getLocalUrl( page );
				link['class'] = 'image';
			} else if( p.link === '' ) {
				link = false;
			} else if( p.link.match( this.getUrlStartRX() ) ) {
				link.href = p.link;
			} else {
				var linkPage = this.getPage( p.link );
				if( linkPage ) {
					link.href = this.getLocalUrl( linkPage );
				 } else {
					link = false;
				}
			}
			if( link && p.title ) {
				link.title = p.title;
			}
			var img = {
				alt : p.alt || '',
				src : this.getThumbUrl( page, p.width ) || 'http://',
				width : p.width,
				height : Math.round( dim.height * p.width / dim.width )
			};
			if( imgclass ) {
				img['class'] = imgclass;
			}
			if( p.valign ) {
				img.style = 'vertical-align: ' + p.valign;
			}
			var result = link ? '<a' + this.encodeAttributes( link ) + '>' : '';
			result += '<img' + this.encodeAttributes( img ) + ' />';
			return result + ( link ? '</a>' : '' );
		},
		makeZoomIcon : function( page ) {
			var link = '<a' + this.encodeAttributes( {
				href : this.getLocalUrl( page ),
				'class' : 'internal',
				'title' : this.getUserMessage( 'thumbnail-more' )
			} ) + '>';
			return '<div class="magnify">' + link + '<img src="' + this.getStylePath()
				+ '/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>';
		},
		getImageParams : function( params ) {
			var text = this.doLinks( params ), start = -1, p = {};
			while( start < text.length ) {
				var end = this.find( text, '|', start + 1 );
				var match = this.matchImageParam( text.substring( start + 1, end ).trim() ) || ['caption', ''];
				if( match[0] === 'width' ) {
					var size = match[1].match( /^(\d*)x(\d*)/ );
					if( size ) {
						if( +size[1] ) {
							p.width = +size[1];
						}
						if( +size[2] ) {
							p.height = +size[2];
						}
					} else {
						var clean = match[1].replace( /\s*px\s*$/, '' );
						if( +clean ) {
							p.width = +clean;
						} else {
							p.caption = match[1];
						}
					}
				} else if( match[0] === 'left' || match[0] === 'right' || match[0] === 'center' || match[0] === 'none' ) {
					p.align = match[0];
				} else if( match[0] === 'baseline' || match[0] === 'sub' || match[0] === 'super' || match[0] === 'top'
						   || match[0] === 'text_top' || match[0] === 'middle' || match[0] === 'bottom' || match[0] === 'text_bottom' ) {
					p.valign = match[0];
				} else {
					p[match[0]] = match[1];
				}
				start = end;
			}
			if( p.manualthumb !== undefined ) {
				p.thumbnail = ''; //TODO: We don't support manualthumb now
			}
			return p;
		},
		makeCategory : function( page, sortkey ) {
			return ''; // TODO: record in output
		},
		makeInterwiki : function( page ) {
			return ''; // TODO: record in output
		},
		makeLink : function( page, text ) {
			var exists = !page.title || this.getPageExists( page ); // TODO: selflink, file links, special pages
			var link = {};
			link.href = this.getLocalUrl( page, exists === false ? 'action=edit&redlink=1' : null );
			if( exists === false ) {
				link['class'] = 'new';
			}
			if( page.title ) {
				if( exists === false ) {
					link.title = this.getUserMessage( 'red-link-title', page.ptitle );
				} else {
					link.title = page.ptitle;
				}
			}
			if( this.getRedirectTarget( page ) ) {
				link['class'] = 'mw-redirect';
			}
			return '<a' + this.encodeAttributes( link ) + '>' + text + '</a>';
		},
		makeISBNLink : function( isbn ) {
			var norm = isbn.replace( /[- ]+/g, '' ).replace( /x/g, 'X' );
			return '<a' + this.encodeAttributes( {
				href    : this.getLocalUrl( this.getPage( 'Special:Booksources/' + norm ) ),
				'class' : 'internal mw-magiclink-isbn'
			} ) + '>ISBN ' + isbn + '</a>';
		},
		makeMagicLink : function( type, num ) {
			return '<a' + this.encodeAttributes( {
				href    : this.getContentMessage( type === 'RFC' ? 'rfcurl' : 'pubmedurl', [ num ] ),
				'class' : 'external mw-magiclink-' + type.toLowerCase()
			} ) + '>' + type + ' ' + num + '</a>';
		},
		makeExtLink : function( url, text, type ) {
			return '<a' + this.encodeAttributes( {
				href : url,
				'class' : 'external ' + type,
				rel : 'nofollow'
			} ) + '>' + text + '</a>';
		},

		storeRef : function( group, name, content ) {
			if( !this.refs[group] ) {
				this.refs[group] = [[], {}];
			}
			var g = this.refs[group];
			if( !name || g[1][name] === undefined ) {
				g[0].push( { key : this.refKey++, text : content, count : 0 } );
			} else {
				var ref = g[0][g[1][name]];
				ref.count++;
				if( !ref.text ) {
					ref.text = content;
				}
			}
			if( name && g[1][name] === undefined ) {
				g[1][name] = g[0].length - 1;
			}
		},
		makeRefLink : function( group, name ) {
			var g = this.refs[group];
			var num = name ? g[1][name] : g[0].length - 1;
			var ref = g[0][num];
			var refId = 'cite_ref-' + ref.key + ( ref.count ? '-' + ref.count : '' );
			var refsId = 'cite_note-' + ref.key;
			var label = ( group && group + ' ' ) + this.formatNum( num + 1 );
			return this.parseInline( this.getContentMessage( 'cite_reference_link', [ refId, refsId, label ] ) || '' );
		},
		makeRefEntry : function( ref, num ) {
			if( !ref.count ) {
				var refsId = 'cite_note-' + ref.key;
				var refId = 'cite_ref-' + ref.key;
				return this.getContentMessage( 'cite_references_link_one', [ refsId, refId, ref.text ] );
			} else {
				var sep = this.getContentMessage( 'cite_references_link_many_sep' );
				var and = this.getContentMessage( 'cite_references_link_many_and' );
				var links = '';
				for( var i = 0; i <= ref.count; i++ ) {
					if( i ) {
						links += i < ref.count - 1 ? sep : and;
					}
					var refId = 'cite_ref-' + ref.key + ( i ? '-' + i : '' );
					var numLabel = this.makeRefNumLabel( num + 1, i, ref.count );
					links += this.getContentMessage( 'cite_references_link_many_format', [ refId, numLabel, this.makeRefAltLabel( i ) ] );
				}
				var refsId = 'cite_note-' + ref.key;
				return this.getContentMessage( 'cite_references_link_many', [ refsId, links, ref.text ] );
			}
		},
		makeRefNumLabel : function( base, sub, max ) {
			var l = ( max + '' ).length;
			return this.formatNum( base + '.' + this.padString( sub, l, '0' ) );
		},
		makeRefAltLabel : function( num ) {
			if( !this.refAltLabels ) {
				var msg = this.getContentMessage( 'cite_references_link_many_format_backlink_labels' ) || '';
				this.refAltLabels = msg.split( ' ' );
			}
			return this.refAltLabels[num] || '';
		},
		unstrip : function( text ) {
			var that = this;
			return text.replace( this.markerRX, function( m, num ) {
				return that.replaceExtension.apply( that, that.stripMarkers[+num] );
			} );
		},

		doBlocks : function( text ) {
			function addHTML( s ) {
				//add an HTML string to tidy
				//assumes that all tags are valid
				var m, last = 0;
				var rx = new RegExp( that.markerPrefix + '(\\d+)' + that.markerSuffix + '|<(/?)(\\w+)([^>]*)>|$', 'g' );
				rx.lastIndex = 0;
				while( m = rx.exec( s ) ) {
					if( m.index > last ) {
						html.addText( s.substring( last, m.index ) );
					}
					if( !m[0] ) {
						break;
					}
					if( m[1] ) {
						addHTML( that.replaceExtension.apply( that, that.stripMarkers[+m[1]] ) );
					} else if( m[2] ) {
						html.addEndTag( m[3] );
					} else {
						html.addStartTag( m[3], m[4].trim() );
					}
					last = rx.lastIndex;
				}
			}

			var that = this;
			var lines = text.split( '\n' );
			var html = new Tidy( this );
			var tables = [], cur;
			var lastprefix = '', lastsection = '';
			for( var i = 0; i < lines.length; i++ ) {
				var match = lines[i].match( /^(?:\s*(:*\{\|)|(----+)|(=+)(.*[^=])(=+)\s*|([;:#\*]+))(.*)$/ );

				//list stuff
				var prefix = ( match && html.getCurrentBlock() !== PRE && match[6] ) || '';
				for( var j = lastprefix.length - 1; j >= 0 && lastprefix.charAt( j ) !== prefix.charAt( j ); j-- ) {
					var ch = lastprefix.charAt( j );
					addHTML( ch === '*' ? '</ul>\n' : ch === '#' ? '</ol>\n' : '</dl>\n' );
				}
				lastprefix = prefix;

				if( prefix ) {
					if( j >= 0 && j + 1 === prefix.length ) {
						var ch = prefix.charAt( j );
						html.nextListItem( ch === ';' ? 'dt' : ch === ':' ? 'dd' : 'li' );
					}
					while( prefix.length > j + 1 ) {
						var ch = prefix.charAt( ++j );
						addHTML( ch === '*' ? '<ul><li>' : ch === '#' ? '<ol><li>' : ch === ':' ? '<dl><dd>' : '<dl><dt>' );
					}
					if( ch === ';' ) {
						var pos = this.find( match[7], ': ', 0 );
						if( pos < match[7].length ) {
							addHTML( match[7].substring( 0, pos ) + '</dt><dd>' + match[7].substring( pos + 1 ) + '</dd>\n' );
							continue;
						}
					}
					addHTML( match[7] + '\n' );
					continue;
				}

				//table stuff
				var line = lines[i].trim();
				if( !match ) {
					if( tables.length ) {
						cur = tables[tables.length - 1];
						var pipe = line.charAt( 0 ) === '|';
						if( pipe && line.charAt( 1 ) === '}' ) {
							tables.pop();
							if( cur.cell ) {
								addHTML( '</' + cur.cell + '>' );
							}
							if( cur.row ) {
								addHTML( '</tr>' );
							}
							if( !cur.hasRows ) {
								addHTML( '<tr><td></td></tr>' );
							}
							addHTML( '</table>' + ( new Array( cur.indent + 1 ) ).join( '</dd></dl>' ) + line.substring( 2 ) + '\n' );
							continue;
						} else if( pipe && line.charAt( 1 ) === '-' ) {
							cur.rowAttr = this.fixAttributes( line.substring( 2 ) );
							cur.hasRows = true; // this matches behavior of mw, although it is a bit illogical
							if( cur.cell ) {
								addHTML( '</' + cur.cell + '>' );
							}
							cur.cell = false;
							if( cur.row ) {
								addHTML( '</tr>' );
							}
							cur.row = false;
							continue;
						} else if( pipe || line.charAt( 0 ) === '!' ) {
							if( pipe && line.charAt( 1 ) === '+' ) {
								var tag = 'caption';
								var start = 2;
							} else {
								var tag = pipe ? 'td' : 'th';
								var start = 1;
							}
							do {
								if( cur.cell ) {
									addHTML( '</' + cur.cell + '>' );
								}
								if( !cur.row && tag !== 'caption' ) {
									addHTML( '<tr' + ( cur.rowAttr || '' ) + '>\n' );
									cur.row = cur.hasRows = true;
								}

								var end = this.find( line, '||', start );
								if( !pipe ) {
									end = Math.min( end, this.find( line, '!!', start ) );
								}
								var sep = this.find( line, '|', start );
								if( sep < end ) {
									var attr = this.fixAttributes( line.substring( start, sep ) );
									addHTML( '<' + tag + attr + '>' + line.substring( sep + 1, end ) );
								} else {
									addHTML( '<' + tag + '>' + line.substring( start, end ) );
								}
								cur.cell = tag;
								start = end + 2;
							} while( end < line.length );
							continue;
						}
					}
				} else if( match[1] ) {
					var indent = match[1].length - 2;
					var attr = this.fixAttributes( match[7], 'table' );
					tables.push( { indent : indent } );
					addHTML( ( new Array( indent + 1 ) ).join( '<dl><dd>' ) + '<table' + attr + '>\n' );
					continue;
				} else if( match[2] ) {
					addHTML( '<hr />' + match[7] + '\n' );
					continue;
				} else if( match[3] && !match[7] ) {
					var level = Math.min( match[3].length, match[5].length, 6 );
					addHTML( '<h' + level + '>' + match[3].substring( level ) + match[4] + match[5].substring( level ) + '</h' + level + '>' );
					continue;
				}
				var block = html.getCurrentBlock();
				if( block !== BLOCK_SKIP && block !== PARAGRAPH && block !== PRE ) {
					var space = lines[i].charAt( 0 ) === ' ';
					if( ( !line || space ) && block & PARAGRAPH ) {
						html.addEndTag( 'p' );
					}
					if( space ) {
						if( !( block & PRE ) ) {
							html.addStartTag( 'pre', '', IMPLICIT );
						}
						addHTML( lines[i].substring( 1 ) + '\n' );
						continue;
					}
					if( block & PRE ) {
						html.addEndTag( 'pre' );
					}
					if( !( block & PARAGRAPH ) ) {
						html.addStartTag( 'p', '', IMPLICIT );
					}
				}
				addHTML( lines[i] + '\n' );
			}
			var text = html.toString();
			if( this.output.underscores.toc ) {
				return text.replace( new RegExp( this.tocMarker ), this.makeToc( html.headings ) );
			} else if( !this.output.underscores.notoc ) {
				var tocPos = text.search( /<h[1-6]/ );
				if( tocPos > -1 ) {
					return text.substring( 0, tocPos ) + this.makeToc( html.headings ) + text.substring( tocPos );
				}
			}
			return text;
		},

		makeToc : function( headings ) {
			if( headings.length < 4 && !this.output.underscores.forcetoc ) {
				return '';
			}
			var toclevel = 0;
			var level = 0;
			var prev = 0;
			var levels = [];
			var counts = [];
			var toc = '<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>';
			toc += mw.html.escape( this.getUserMessage( 'toc' ) || '' );
			toc += '</h2></div>';
			for( var i = 0; i < headings.length; i++ ) {
				var h = headings[i];
				while( h.level < level ) {
					level = levels.pop();
					toclevel--;
				}
				if( h.level > level ) {
					levels.push( level );
					level = h.level;
					toclevel++;
				}
				if( toclevel > prev ) {
					toc += '<ul>';
					counts.push( 0 );
				} else {
					toc += '</li>';
				}
				while( toclevel < prev-- ) {
					toc += '</ul></li>';
					counts.pop();
				}
				counts[counts.length - 1]++;
				prev = toclevel;
				toc += '<li class="toclevel-' + toclevel + '">';
				toc += '<a href="#' + h.id + '"><span class="tocnumber">';
				toc += counts.join( '.' ) + '</span> <span class="toctext">' + h.name.replace( /<[^>]*>/g, '' );
				toc += '</span></a>';
			}
			while( toclevel-- ) {
				toc += '</li></ul>';
			}
			toc += '</td></tr></table>';
			return toc;
		},

		makeError : function( text ) {
			return '<strong class="error">' + text + '</strong>';
		},
		replaceExtension : function( name, attr, inner ) {
			var lc = name.toLowerCase();
			var func = this.extensionFunctions[lc];
			if( func ) {
				var attr = this.getAttributes( attr );
				var result = func.call( this, attr, inner );
			} else {
				var call = '<' + lc + attr + '>' + ( inner === false ? '' : inner + '</' + lc + '>' );
				var result = this.getParsedText( call ) || '';
			}
			return result;
		},
		extensionFunctions : {
			nowiki : function( attr, content ) {
				return content ? content.replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ) : '';
			},
			pre : function( attr, content ) {
				var attr = this.validateAttributes( attr );
				return '<pre' + this.encodeAttributes( attr ) + '>'
					+ ( content ? content.replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ) : '' )
					+ '</pre>';
			},
			ref : function( attr, content ) {
				var group = attr.group || this.refsGroup || '';
				if( this.refsGroup !== false ) {
					if( group !== this.refsGroup ) {
						return this.makeError( 'ref-group-mismatch' );
					}
					if( !attr.name ) {
						return this.makeError( 'ref-no-key' );
					}
					var g = this.refs[group];
					if( !g ) {
						return this.makeError( 'ref-missing-group' );
					}
					if( g[1][attr.name] !== undefined ) {
						g[0][g[1][attr.name]].text = content;
					}
					return '';
				} else if( !content && !attr.name ) {
					return this.makeError( 'ref-no-input' );
				} else {
					this.storeRef( group, attr.name, content );
					return this.makeRefLink( group, attr.name );
				}
			},
			references : function( attr, content ) {
				if( this.refsGroup !== false ) {
					return '';
				}
				var group = attr.group || '';
				if( content ) {
					this.refsGroup = group;
					this.unstrip( this.parseInline( content ) );
					this.refsGroup = false;
				}
				var g = this.refs[group];
				if( !g || !g[0].length ) {
					return '';
				}
				var result = this.getContentMessage( 'cite_references_prefix' );
				for( var i = 0; i < g[0].length; i++ ) {
					result += this.makeRefEntry( g[0][i], i ) + '\n';
				}
				result += this.getContentMessage( 'cite_references_suffix' );
				delete this.refs[group];
				return this.parseInline( result );
			}
		},

		find : function( text, sub, start ) {
			for( ; ; ) {
				var pos = text.indexOf( sub, start );
				if( pos < 0 ) {
					return text.length;
				}
				var start = text.indexOf( '>', pos + 1 );
				if( start < 0 ) {
					return pos;
				}
				var lt = text.indexOf( '<', pos + 1 );
				if( lt > -1 && lt < start ) {
					return pos;
				}
			}
		},
		fixTags : function( text ) {
			var that = this;
			return text.replace( /<(\/?)(\w*)([^<>]*)(>?)|(>)/g, function( m, slash, tag, attr, close, gt ) {
				if( gt ) {
					return '&gt;';
				} else {
					var lc = tag.toLowerCase();
					if( close && tagFlags[lc] & USER_ALLOWED ) {
						if( slash ) {
							return '</' + lc + '>';
						} else {
							var slash = attr.charAt( attr.length - 1 ) === '/' ? '/' : '';
							var attr = that.fixAttributes( attr, lc );
							return '<' + lc + attr + slash + '>';
						}
					} else {
						return '&lt;' + slash + tag + attr + ( close && '&gt;' );
					}
				}
			} );
		},
		fixEntities : function( text ) {
			return text.replace( /&nbsp;/g, '&#160;' ).replace( /&#124;/g, '|' ).replace( /&bull;/g, '•' );
		},
		fixAttributes : function( s, el ) {
			if( !s.trim() ) {
				return '';
			}
			var attr = this.validateAttributes( this.getAttributes( this.unstrip( s ) ) );
			return this.encodeAttributes( attr );
		},
		getAttributes : function( s ) {
			if( s.charAt( s.length - 1 ) === '/' ) {
				s = s.substring( 0, s.length - 1 );
			}
			var attr = {}, m;
			if( !s.trim() ) {
				return attr;
			}
			var rx = /(?:^|[\t\n\r ])([A-Za-z\d]+)(?:[\t\n\r ]*=[\t\n\r ]*(?:("[^<"]*)"|('[^<']*)'|([\w!#$%&()*,\-.\/:;?@[\]^`{|}~^]+)))?/g;
			while( m = rx.exec( s ) ) {
				var next = s.charAt( m.index + m[0].length );
				if( next && ' \t\n\r'.indexOf( next ) === -1 ) {
					rx.lastIndex = m.index + 1;
					continue;
				}
				var name = m[1].toLowerCase();
				var value = m[4] || ( m[3] ? m[3].substring( 1 ) : ( m[2] ? m[2].substring( 1 ) : name ) );
				value = value.replace( /[\t\n\r ]+/g, ' ' ).trim();
				attr[name] = this.decodeEntities( value );
			}
			return attr;
		},
		validateAttributes : function( attr ) {
			var hrefRX = new RegExp( '^(?:' + this.getUrlProtocols() + ')[^\s]+$' );
			var newAttr = {};
			for( var name in attr ) if( has.call( attr, name ) ) {
				if( !attributeFlags[name] ) continue;
				var value = attr[name];
				if( name === 'style' ) value = this.validateStyle( value );
				else if( name === 'id' ) value = this.anchorEncode( value );
				else if( ( name === 'href' || name === 'src' ) && !value.match( hrefRX ) ) continue;
				newAttr[name] = value;
				if( name === 'lang' ) newAttr['xml:lang'] = value;
			}
			return newAttr;
		},
		encodeAttributes : function( attr ) {
			var pairs = [];
			for( var name in attr ) if( has.call( attr, name ) ) {
				pairs.push( name + '="' + mw.html.escape( attr[name] + '' ) + '"' );
			}
			return pairs.length ? ' ' + pairs.join( ' ' ) : '';
		},
		validateStyle : function( s ) {
			var s = s.replace( /\/\*[^*]*\*(?:\*|[^*\/][^*]*\*)*\//g, ' ' );
			var stripped = s.replace( /\\([\dA-Fa-f]{1,6})/g, function( m, num ) { return String.fromCharCode( parseInt( num, 16 ) ); } );
			stripped = stripped.replace( /\\/g, '' );
			if( stripped.match( /expression|tps*:\/\/|url\s*\(/ ) ) return '';
			return s;
		},
		decodeEntities : function( s ) {
			var node = document.createElement( 'div' );
			return s.replace( /&(?:#?[xX]?[^\x00-\x2F:;<=>\?@\[\\\]\^_`\{\|\}~\x7F]+;)?/g, function( m ) {
				try{
					node.innerHTML = m;
					if( node.firstChild ) return node.firstChild.nodeValue;
				} catch(e) {}
				return m;
			} );
		},
		getExtLinkRX : function() {
			if( !this.extLinkRX ) {
				this.extLinkRX = new RegExp(
					'<[^>]*>|(\\[?)((?:' + this.getUrlProtocols().join( '|' )
					+ ')[^\\[\\]"\x00-\x20\x7f<>]+)( *[^\\[\\]\n\r]*\\])?'
					+ '|(RFC|PMID)\\s+(\\d+)'
					+ '|ISBN\\s+((?:97[89][ \\-]?)?(?:\\d[ \\-]?){9}[\\dXx]\\b)', 'g'
				);
			}
			return this.extLinkRX;
		},
		getUrlStartRX : function() {
			if( !this.urlStartRX ) {
				this.urlStartRX = new RegExp( '^(?:' + this.getUrlProtocols().join( '|' ) + ')' );
			}
			return this.urlStartRX;
		}
	} );
	this.parse = function( text, title, cb ) {
		return iterate( Parser, 'parse', text, title, cb );
	};

	function PPNode( type, text, offset, count ) {
		this.type = type;
		this.offset = offset;
		this.lineStart = text.charAt( offset - 1 ) === '\n';
		this.parts = [[]];
		//cur and count are only for internal processing.
		//They will be cleaned up later by finish()
		this.cur = this.parts[0];
		this.count = count;
	}
	PPNode.prototype = {
		//Append text or a child to a node
		append : function( node ) {
			if( !node ) return;
			var newstr = typeof node === 'string';
			var oldstr = typeof this.cur[this.cur.length - 1] === 'string';

			//For template nodes, record if and where an equal sign was found
			//We put the '=' on its own index, so we can easily pull the name and value part separately
			//later if necessary. We also make sure the index is >0 so we can do boolean checks on it
			if( newstr && this.type == '{' && !this.cur.split ) {
				var pos = node.indexOf( '=' );
				if( pos > -1 ) {
					if( oldstr ) {
						this.cur[this.cur.length - 1] += node.substring( 0, pos );
					} else if( pos > 0 || this.cur.length == 0 ) {
						this.cur.push( node.substring( 0, pos ) );
					}
					this.cur.split = this.cur.length;
					this.cur.push( '=' );
					this.cur.push( node.substring( pos + 1 ) );
					return;
				}
			}

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

			//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( this.type === 'h' ) {
				if( newstr ) {
					var match = node.match( /(=+)[ \t]*$/ );
					if( match ) {
						this.closing = match[1].length;
					} else if( !node.match( /^[ \t]*$/ ) ) {
						this.closing = false;
					}
				} else if( node.type != 'comment' ) {
					this.closing = false;
				}
			}
		},
		//Break and append a child to a node
		breakAndAppendTo : function( node ) {
			//First add the opening braces
			if( this.type != 'h' ) {
				node.append( ( new Array( this.count + 1 ) ).join( this.type ) );
			}
			//Then the parts, separated by '|'
			for( var i = 0; i < this.parts.length; i++ ) {
				if( i > 0 ) {
					node.append( '|' );
				}
				for( var j = 0; j < this.parts[i].length; j++ ) {
					node.append( this.parts[i][j] );
				}
			}
		},
		//Clean up the extra stuff we put into the node for easier processing
		finish : function( endOffset ) {
			this.len = endOffset - this.offset;
			delete this.cur;
			delete this.count;
			delete this.closing;
			return this;
		}
	};
	function XML( name, content, attr ) {
		return '<' + name + ( attr || '' ) + ( content ? '>' + content + '</' + name + '>' : '/>' );
	}

	/************************************************************************************************
	 * Tidy
	 */
	function Tidy( parser ) {
		this.parser = parser;
		this.stack = [];
		this.blocks = [];
		this.inlines = [];
		this.istacks = [];
		this.headings = [];
		this.open( 'root' );
	}
	Tidy.prototype = {
		getCurrent : function() {
			//returns the currently open element name, 'root' if none
			return this.top.el;
		},
		open : function( el, flags, attr ) {
			var f = tagFlags[el];
			if( this.top ) {
				this.stack.push( this.top );
			}
			var closing = false;
			if( attr && attr.charAt( attr.length - 1 ) == '/' ) {
				closing = true;
				var attr = attr.substring( 0, attr.length - 1 ).trim();
			}
			if( attr ) {
				var attr = ' ' + attr;
			}
			this.top = { el : el, attr : attr, flags : f | flags, content : '' };
			var block = this.top.flags & ( BLOCK_NONE | BLOCK_SKIP | PARAGRAPH | PRE | IMPLICIT );
			if( block && block !== IMPLICIT ) {
				this.blocks.push( block );
			}
			if( el === 'table' ) {
				this.istacks.push( this.inlines );
				this.inlines = [];
			}
			if( closing ) {
				this.addEndTag( el );
			}
		},
		close : function() {
			var next = this.stack.pop();
			if( !this.top.exiled ) {
				if( this.top.flags & CO_EMPTY ) {
					next.content += '<' + this.top.el + ( this.top.attr || '' ) + ' />';
				} else {
					var content = this.top.content.trim();
					if( content || this.top.flags & ALLOW_EMPTY || this.top.attr ) {
						if( this.top.flags & HEADING ) {
							this.headings.push( {
								level : +this.top.el.charAt( 1 ),
								name  : content,
								id    : this.parser.anchorEncode( content )
							} );
							content = '<span class="mw-headline" id="' + this.parser.anchorEncode( content ) + '">' + content + '</span>';
						} else if( this.top.flags & PRE ) {
							content = this.top.content;
						}
						if( this.top.flags & INLINE ) {
							next.content += this.top.content.match( /^\s*/ )[0];
						}
						next.content += '<' + this.top.el + ( this.top.attr || '' ) + '>' + content + '</' + this.top.el + '>';
						if( this.top.flags & INLINE ) {
							next.content += this.top.content.match( /\s*$/ )[0];
						}
					}
				}
				if( this.top.flags & ( BLOCK_NONE | BLOCK_SKIP | PARAGRAPH | PRE ) ) {
					this.blocks.pop();
				}
			} else {
				this.top.exiled = false;
			}
			if( this.top.el === 'table' ) {
				this.inlines = this.istacks.pop();
			}
			this.top = next;
		},

		addText : function( s ) {
			if( !s.trim() ) {
				return this.top.content += s;
			}
			for( ; ; ) {
				var o = tagFlags[this.top.el];
				if( o & CO_EMPTY ) {
					this.close();
					continue;
				}
				if( o & CO_DEFLISTITEM ) {
					this.open( 'dt' );
					continue;
				}
				if( o & CO_LISTITEM ) {
					this.open( 'li' );
					continue;
				}
				if( o & CO_CELL || o & CO_ROW ) {
					this.moveBeforeTable();
					continue;
				}
				if( o & ROOT ) {
					this.open( 'p', IMPLICIT );
					continue;
				}

				if( o & BLOCK && !( o & INLINE ) ) {
					this.propagateInlines();
				}
				this.top.content += s;
				return;
			}
		},
		moveBeforeTable : function() {
			this.stack.push( this.top );
			for( var i = this.stack.length - 1; !( this.stack[i].flags & CO_ROW ); i-- );
			this.top = this.stack[i - 1];
			this.top.exiled = true;
		},
		hasOpen : function( el ) {
			if( this.top.el === el ) {
				return true;
			}
			for( var i = this.stack.length - 1; i >= 0; i-- ) {
				if( this.stack[i].el === el ) {
					return true;
				}
			}
		},
		addStartTag : function( el, attr, flags ) {
			var n = tagFlags[el];
			for( ; ; ) {
				var o = tagFlags[this.top.el];

				//Check if we should close the current node before starting the new one
				if(    ( o & CELL && ( n & CELL || n & ROW ) )
					|| ( o & LISTITEM && n & LISTITEM )
					|| ( o & DEFLISTITEM && n & DEFLISTITEM )
					|| ( this.top.exiled && ( n & ROW || n & CELL ) )
					|| ( o & HEADING && n & HEADING && this.top.el != el )
					|| ( el == 'a' && attr && this.hasOpen( 'a' ) )
					|| ( o & CO_INLINE && !(n & INLINE) && n )
					|| ( o & CO_EMPTY )
					|| ( o & CO_DEFLISTITEM && !(n & DEFLISTITEM) && !(n & BLOCK) && !(n & INLINE) )
					|| ( o & CO_CELL && n & CO_CELL )
					|| ( o & CO_ROW && ( n & DEFLISTITEM || n & LISTITEM ) )
				) {
					this.close();
					continue;
				}

				//Check if we should infer another node before starting the new one
				if( !( o & CO_LISTITEM ) && n & LISTITEM ) {
					this.open( 'ul' );
					continue;
				}
				if( !( o & CO_DEFLISTITEM ) && n & DEFLISTITEM ) {
					this.open( 'dl' );
					continue;
				}
				if( !( o & CO_ROW ) && ( n & ROW || ( !( o & CO_CELL ) && n & CELL ) ) ) {
					this.open( 'table' );
					continue;
				}
				if( o & CO_DEFLISTITEM && !( n & DEFLISTITEM ) ) {
					this.open( 'dd' );
					continue;
				}
				if( o & CO_LISTITEM && !( n & LISTITEM ) ) {
					this.open( 'li', 0, 'list-style: none' + ( n & BLOCK ? '; display : inline' : '' ) );
					continue;
				}
				if( o & CO_ROW && ( n & CELL || n & CO_ROW ) ) {
					this.open( 'tr' );
					continue;
				}
				if( o & ROOT && !( n & BLOCK ) ) {
					this.open( 'p', IMPLICIT );
					continue;
				}
				if( o & PRE && n & PARAGRAPH ) {
					this.open( 'br' );
					return;
				}

				//Special cases
				if( o & HEADING && n & HEADING && this.top.el === el ) {
					this.close();
					return;
				}
				if( o & INLINE && this.top.el === el && !attr && el !== 'big' && el !== 'small' ) {
					this.popInline( el );
					this.close();
					return;
				}
				if( el === 'a' && this.hasOpen( 'a' ) ) {
					this.addEndTag( 'a' );
					return;
				}
				if( o & CO_CELL && n & CO_ROW ) return;
				if( ( o & CO_CELL || o & CO_ROW ) && ( n & INLINE || n & BLOCK ) ) {
					this.moveBeforeTable();
					continue;
				}
				if( o & CO_CELL && !( n & CELL ) ) return;

				//All is well, we can open the new node
				if( o & BLOCK && n & INLINE && !( o & INLINE ) ) this.propagateInlines();
				if( n & INLINE && el != 'a' && !( n & CO_EMPTY ) ) this.pushInline( el, attr );
				this.open( el, flags, attr );
				return;
			}
		},
		addEndTag : function( el ) {
			//TODO: <font..><a..>...</a></font> to <a..><font..>...</font></a> conversion
			var n = tagFlags[el];
			if( n & INLINE && el !== 'a' ) this.popInline( el );
			for(;;) {
				//Check if we got the correct tag for our current tag
				if( el === this.top.el ) {
					this.close();
					return;
				}

				var o = this.top.flags;

				//Check if we should close the current tag and try to match the end tag later
				if( ( this.hasOpen( el ) && ( !(o & CO_CELL) || n & CO_ROW ) && ( !(o & CO_ROW) || n & DEFLISTITEM || n & LISTITEM ) )
					|| ( o & HEADING && n & HEADING )
					|| ( o & CO_EMPTY )
					|| ( o & CO_ROW && !(n & ROW || n & CELL || n & INLINE)  )
				) {
					this.close();
					continue;
				}

				//Special cases
				if( el == 'br' && ( o & CO_BLOCK || o & CO_INLINE ) ) {
					this.open( 'br' );
				}
				return;
			}
		},
		pushInline : function( el, attr ) {
			for( var i = 0; i < this.inlines.length; i++ ) {
				if( this.inlines[i][0] == el ) return;
			}
			this.inlines.push( [el,attr] );
		},
		popInline : function( el ) {
			for( var i = 0; i < this.inlines.length; i++ ) {
				if( this.inlines[i][0] == el ) break;
			}
			if( i < this.inlines.length ) this.inlines.splice( i, 1 );
		},
		propagateInlines : function() {
			for( var i = 0; i < this.inlines.length; i++ ) {
				this.open( this.inlines[i][0], 0, this.inlines[i][1] );
			}
		},
		getCurrentBlock : function() {
			return this.blocks[this.blocks.length - 1];
		},
		nextListItem : function( el ) {
			var list = el === 'li' ? CO_LISTITEM : CO_DEFLISTITEM;
			if( ( el === 'li' && ( this.hasOpen( 'ol' ) || this.hasOpen( 'ul' ) ) )
				|| ( el !== 'li' && this.hasOpen( 'dl' ) ) ) {
				while( !( this.top.flags & list ) ) this.close();
			}
			this.addStartTag( el );
		},
		toString : function() {
			while( this.stack.length ) this.close();
			return this.top.content;
		}
	};

	function magic2RX( alias, cs, triple ) {
		var rx = jQuery.escapeRE( alias );
		if( !cs ) {
			rx = rx.replace( /\w/g, function( ch ) {
				return ch.toLowerCase() === ch.toUpperCase() ? ch : '[' + ch.toLowerCase() + ch.toUpperCase() + ']';
			} );
		}
		if( !triple ) {
			return rx;
		}
		var pos = rx.indexOf( '\\$1' );
		if( pos > -1 ) {
			return '(' + rx.substring( 0, pos ) + ')(.*)(' + rx.substring( pos + 3 ) + ')';
		} else {
			return '(' + rx + ')()()';
		}
	}
	var iterate = this.iterate = function( cls, func, text, title, cb ) {
		var obj = new cls( title );
		var result = obj[func].call( obj, text );
		if( cb ) {
			if( obj.batch.isEmpty ) {
				cb( result );
			} else {
				obj.batch.load( function() {
					iterate( cls, func, text, title, cb );
				}, obj.cache );
			}
		}
		return result;
	};

	/************************************************************************************************
	 * private variables
	 */

	var safeModeMissing = new Object;

	var specialFuncs = {
		'basepagename'          : 'page',
		'basepagenamee'         : 'page',
		'currentday'            : 'utc',
		'currentday2'           : 'utc',
		'currentdayname'        : 'utc',
		'currentdow'            : 'utc',
		'currenthour'           : 'utc',
		'currentmonth'          : 'utc',
		'currentmonthabbrev'    : 'utc',
		'currentmonth1'         : 'utc',
		'currentmonthname'      : 'utc',
		'currentmonthnamegen'   : 'utc',
		'currenttime'           : 'utc',
		'currenttimestamp'      : 'utc',
		'currentweek'           : 'utc',
		'currentyear'           : 'utc',
		'fullpagename'          : 'page',
		'fullpagenamee'         : 'page',
		'fullurl'               : 'page',
		'fullurle'              : 'page',
		'localday'              : 'local',
		'localday2'             : 'local',
		'localdayname'          : 'local',
		'localdow'              : 'local',
		'localhour'             : 'local',
		'localmonth'            : 'local',
		'localmonthabbrev'      : 'local',
		'localmonth1'           : 'local',
		'localmonthname'        : 'local',
		'localmonthnamegen'     : 'local',
		'localtime'             : 'local',
		'localtimestamp'        : 'local',
		'localurl'              : 'page',
		'localurle'             : 'page',
		'localweek'             : 'local',
		'localyear'             : 'local',
		'namespace'             : 'page',
		'namespacee'            : 'page',
		'pagename'              : 'page',
		'pagenamee'             : 'page',
		'subjectpagename'       : 'page',
		'subjectpagenamee'      : 'page',
		'subjectspace'          : 'page',
		'subjectspacee'         : 'page',
		'subpagename'           : 'page',
		'subpagenamee'          : 'page',
		'talkpagename'          : 'page',
		'talkpagenamee'         : 'page',
		'talkspace'             : 'page',
		'talkspacee'            : 'page'
	};

	var weekdayMessages = [
		'sunday',
		'monday',
		'tuesday',
		'wednesday',
		'thursday',
		'friday',
		'saturday'
	];
	var weekdayAbbrevMessages = [
		'sun',
		'mon',
		'tue',
		'wed',
		'thu',
		'fri',
		'sat'
	];
	var monthMessages = [
		'january',
		'february',
		'march',
		'april',
		'may_long',
		'june',
		'july',
		'august',
		'september',
		'october',
		'november',
		'december'
	];
	var monthGenMessages = [
		'january-gen',
		'february-gen',
		'march-gen',
		'april-gen',
		'may-gen',
		'june-gen',
		'july-gen',
		'august-gen',
		'september-gen',
		'october-gen',
		'november-gen',
		'december-gen'
	];
	var monthAbbrevMessages = [
		'jan',
		'feb',
		'mar',
		'apr',
		'may',
		'jun',
		'jul',
		'aug',
		'sep',
		'oct',
		'nov',
		'dec'
	];

	// Tag flags:
	var USER_ALLOWED   = 1 << 0;
	var CO_INLINE      = 1 << 1;
	var CO_BLOCK       = 1 << 2;
	var CO_EMPTY       = 1 << 3;
	var CO_LISTITEM    = 1 << 4;
	var CO_ROW         = 1 << 5;
	var CO_DEFLISTITEM = 1 << 6;
	var CO_CELL        = 1 << 7;
	var INLINE         = 1 << 8;
	var BLOCK          = 1 << 9;
	var ROW            = 1 << 10;
	var LISTITEM       = 1 << 11;
	var DEFLISTITEM    = 1 << 12;
	var HEADING        = 1 << 13;
	var CELL           = 1 << 14;
	var PARAGRAPH      = 1 << 15;
	var PRE            = 1 << 16;
	var ROOT           = 1 << 17;
	var ANCHOR         = 1 << 18;
	var IMPLICIT       = 1 << 19;
	var WIKI           = 1 << 20;
	var BLOCK_NONE     = 1 << 21;
	var BLOCK_SKIP     = 1 << 22;
	var ALLOW_EMPTY    = 1 << 23;
	var PROPAGATE      = 1 << 24;
	var tagFlags = {
		'root'       : CO_BLOCK | BLOCK | ROOT | BLOCK_NONE,
		'a'          : CO_INLINE | INLINE | ANCHOR,
		'abbr'       : USER_ALLOWED | CO_INLINE | INLINE,
		'b'          : USER_ALLOWED | CO_INLINE | INLINE,
		'big'        : USER_ALLOWED | CO_INLINE | INLINE,
		'blockquote' : USER_ALLOWED | CO_BLOCK | BLOCK | BLOCK_SKIP,
		'br'         : USER_ALLOWED | CO_EMPTY | INLINE,
		'caption'    : USER_ALLOWED | CO_INLINE | ROW,
		'center'     : USER_ALLOWED | CO_BLOCK | BLOCK | BLOCK_NONE,
		'cite'       : USER_ALLOWED | CO_INLINE | INLINE,
		'code'       : USER_ALLOWED | CO_INLINE | INLINE,
		'dd'         : USER_ALLOWED | CO_BLOCK | DEFLISTITEM,
		'del'        : USER_ALLOWED | CO_BLOCK | INLINE | BLOCK,
		'div'        : USER_ALLOWED | CO_BLOCK | BLOCK | BLOCK_NONE | ALLOW_EMPTY,
		'dl'         : USER_ALLOWED | CO_DEFLISTITEM | BLOCK | BLOCK_SKIP,
		'dt'         : USER_ALLOWED | CO_INLINE | DEFLISTITEM,
		'em'         : USER_ALLOWED | CO_INLINE | INLINE,
		'font'       : USER_ALLOWED | CO_INLINE | INLINE,
		'h1'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'h2'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'h3'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'h4'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'h5'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'h6'         : USER_ALLOWED | CO_INLINE | BLOCK | HEADING | BLOCK_SKIP,
		'hr'         : USER_ALLOWED | CO_EMPTY | BLOCK,
		'i'          : USER_ALLOWED | CO_INLINE | INLINE,
		'img'        : CO_EMPTY | INLINE,
		'ins'        : USER_ALLOWED | CO_BLOCK | INLINE | BLOCK,
		'li'         : USER_ALLOWED | CO_BLOCK | LISTITEM,
		'ol'         : USER_ALLOWED | CO_LISTITEM | BLOCK | BLOCK_SKIP,
		'p'          : USER_ALLOWED | CO_INLINE | BLOCK | PARAGRAPH,
		'pre'        : USER_ALLOWED | CO_INLINE | BLOCK | PRE,
		'rb'         : USER_ALLOWED | CO_INLINE | INLINE,
		'rp'         : USER_ALLOWED | CO_INLINE | INLINE,
		'rt'         : USER_ALLOWED | CO_INLINE | INLINE,
		'ruby'       : USER_ALLOWED | CO_INLINE | INLINE,
		's'          : USER_ALLOWED | CO_INLINE | INLINE,
		'small'      : USER_ALLOWED | CO_INLINE | INLINE,
		'span'       : USER_ALLOWED | CO_INLINE | INLINE,
		'strike'     : USER_ALLOWED | CO_INLINE | INLINE,
		'strong'     : USER_ALLOWED | CO_INLINE | INLINE,
		'sub'        : USER_ALLOWED | CO_INLINE | INLINE,
		'sup'        : USER_ALLOWED | CO_INLINE | INLINE,
		'table'      : USER_ALLOWED | CO_ROW | BLOCK | BLOCK_SKIP | ALLOW_EMPTY,
		'td'         : USER_ALLOWED | CO_BLOCK | CELL | ALLOW_EMPTY | BLOCK_NONE,
		'th'         : USER_ALLOWED | CO_BLOCK | CELL | ALLOW_EMPTY | BLOCK_NONE,
		'tr'         : USER_ALLOWED | CO_CELL | ROW,
		'tt'         : USER_ALLOWED | CO_INLINE | INLINE,
		'u'          : USER_ALLOWED | CO_INLINE | INLINE,
		'ul'         : USER_ALLOWED | CO_LISTITEM | BLOCK | BLOCK_SKIP,
		'var'        : USER_ALLOWED | CO_INLINE | INLINE
	};

	var attributeFlags = {
		'abbr'        : 1,
		'align'       : 1,
		'alt'         : 1,
		'axis'        : 1,
		'bgcolor'     : 1,
		'border'      : 1,
		'cellpadding' : 1,
		'cellspacing' : 1,
		'char'        : 1,
		'charoff'     : 1,
		'cite'        : 1,
		'class'       : 1,
		'clear'       : 1,
		'color'       : 1,
		'colspan'     : 1,
		'datetime'    : 1,
		'dir'         : 1,
		'face'        : 1,
		'frame'       : 1,
		'headers'     : 1,
		'height'      : 1,
		'href'        : 1,
		'id'          : 1,
		'lang'        : 1,
		'noshade'     : 1,
		'nowrap'      : 1,
		'rel'         : 1,
		'rev'         : 1,
		'rowspan'     : 1,
		'rules'       : 1,
		'scope'       : 1,
		'size'        : 1,
		'span'        : 1,
		'start'       : 1,
		'style'       : 1,
		'summary'     : 1,
		'title'       : 1,
		'type'        : 1,
		'valign'      : 1,
		'value'       : 1,
		'width'       : 1,
		'xml:lang'    : 1
	};

	var MAG_VARIABLE   = 1 << 0;
	var MAG_PFUNC      = 1 << 1;
	var MAG_PFUNC_HASH = 1 << 2;
	var MAG_MODIFIER   = 1 << 3;
	var MAG_IMG_PARAM  = 1 << 4;
	var MAG_UNDERSCORE = 1 << 5;

	var magicWordFlags = {
		'anchorencode'          : MAG_PFUNC,
		'articlepath'           : MAG_VARIABLE,
		'basepagename'          : MAG_VARIABLE | MAG_PFUNC,
		'basepagenamee'         : MAG_VARIABLE | MAG_PFUNC,
		'categorytree'          : MAG_PFUNC_HASH,//
		'contentlanguage'       : MAG_VARIABLE,
		'currentday'            : MAG_VARIABLE,
		'currentday2'           : MAG_VARIABLE,
		'currentdayname'        : MAG_VARIABLE,
		'currentdow'            : MAG_VARIABLE,
		'currenthour'           : MAG_VARIABLE,
		'currentmonth'          : MAG_VARIABLE,
		'currentmonthabbrev'    : MAG_VARIABLE,
		'currentmonth1'         : MAG_VARIABLE,
		'currentmonthname'      : MAG_VARIABLE,
		'currentmonthnamegen'   : MAG_VARIABLE,
		'currenttime'           : MAG_VARIABLE,
		'currenttimestamp'      : MAG_VARIABLE,
		'currentversion'        : MAG_VARIABLE,
		'currentweek'           : MAG_VARIABLE,
		'currentyear'           : MAG_VARIABLE,
		'defaultsort'           : MAG_PFUNC,
		'directionmark'         : MAG_VARIABLE,
		'displaytitle'          : MAG_PFUNC,
		'expr'                  : MAG_PFUNC_HASH,
		'filepath'              : MAG_PFUNC,
		'forcetoc'              : MAG_UNDERSCORE,
		'formatdate'            : MAG_PFUNC_HASH,//
		'formatnum'             : MAG_PFUNC,
		'fullpagename'          : MAG_VARIABLE | MAG_PFUNC,
		'fullpagenamee'         : MAG_VARIABLE | MAG_PFUNC,
		'fullurl'               : MAG_PFUNC,
		'fullurle'              : MAG_PFUNC,
		'gender'                : MAG_PFUNC,
		'grammar'               : MAG_PFUNC,
		'hiddencat'             : MAG_UNDERSCORE,
		'if'                    : MAG_PFUNC_HASH,
		'ifeq'                  : MAG_PFUNC_HASH,
		'iferror'               : MAG_PFUNC_HASH,
		'ifexist'               : MAG_PFUNC_HASH,
		'ifexpr'                : MAG_PFUNC_HASH,
		'img_alt'               : MAG_IMG_PARAM,
		'img_baseline'          : MAG_IMG_PARAM,
		'img_border'            : MAG_IMG_PARAM,
		'img_bottom'            : MAG_IMG_PARAM,
		'img_center'            : MAG_IMG_PARAM,
		'img_framed'            : MAG_IMG_PARAM,
		'img_frameless'         : MAG_IMG_PARAM,
		'img_left'              : MAG_IMG_PARAM,
		'img_link'              : MAG_IMG_PARAM,
		'img_manualthumb'       : MAG_IMG_PARAM,
		'img_middle'            : MAG_IMG_PARAM,
		'img_none'              : MAG_IMG_PARAM,
		'img_page'              : MAG_IMG_PARAM,
		'img_right'             : MAG_IMG_PARAM,
		'img_sub'               : MAG_IMG_PARAM,
		'img_super'             : MAG_IMG_PARAM,
		'img_text_bottom'       : MAG_IMG_PARAM,
		'img_text_top'          : MAG_IMG_PARAM,
		'img_thumbnail'         : MAG_IMG_PARAM,
		'img_top'               : MAG_IMG_PARAM,
		'img_upright'           : MAG_IMG_PARAM,
		'img_width'             : MAG_IMG_PARAM,
		'index'                 : MAG_UNDERSCORE,
		'int'                   : MAG_PFUNC,
		'language'              : MAG_PFUNC, //In practice the func works with a hash, but for some reason the hash is part of the translation in this case !?
		'lc'                    : MAG_PFUNC,
		'lcfirst'               : MAG_PFUNC,
		'localday'              : MAG_VARIABLE,
		'localday2'             : MAG_VARIABLE,
		'localdayname'          : MAG_VARIABLE,
		'localdow'              : MAG_VARIABLE,
		'localhour'             : MAG_VARIABLE,
		'localmonth'            : MAG_VARIABLE,
		'localmonthabbrev'      : MAG_VARIABLE,
		'localmonth1'           : MAG_VARIABLE,
		'localmonthname'        : MAG_VARIABLE,
		'localmonthnamegen'     : MAG_VARIABLE,
		'localtime'             : MAG_VARIABLE,
		'localtimestamp'        : MAG_VARIABLE,
		'localurl'              : MAG_PFUNC,
		'localurle'             : MAG_PFUNC,
		'localweek'             : MAG_VARIABLE,
		'localyear'             : MAG_VARIABLE,
		'msg'                   : MAG_MODIFIER,
		'msgnw'                 : MAG_MODIFIER,
		'namespace'             : MAG_VARIABLE | MAG_PFUNC,
		'namespacee'            : MAG_VARIABLE | MAG_PFUNC,
		'newsectionlink'        : MAG_UNDERSCORE,
		'nocontentconvert'      : MAG_UNDERSCORE,
		'noeditsection'         : MAG_UNDERSCORE,
		'nogallery'             : MAG_UNDERSCORE,
		'noheader'              : MAG_UNDERSCORE,
		'noindex'               : MAG_UNDERSCORE,
		'nonewsectionlink'      : MAG_UNDERSCORE,
		'notitleconvert'        : MAG_UNDERSCORE,
		'notoc'                 : MAG_UNDERSCORE,
		'ns'                    : MAG_PFUNC,
		'nse'                   : MAG_PFUNC,
		'numberingroup'         : MAG_PFUNC,//
		'numberofactiveusers'   : MAG_VARIABLE | MAG_PFUNC,//
		'numberofadmins'        : MAG_VARIABLE | MAG_PFUNC,//
		'numberofarticles'      : MAG_VARIABLE | MAG_PFUNC,//
		'numberofedits'         : MAG_VARIABLE | MAG_PFUNC,//
		'numberoffiles'         : MAG_VARIABLE | MAG_PFUNC,//
		'numberofpages'         : MAG_VARIABLE | MAG_PFUNC,//
		'numberofusers'         : MAG_VARIABLE | MAG_PFUNC,//
		'numberofviews'         : MAG_VARIABLE | MAG_PFUNC,//
		'padleft'               : MAG_PFUNC,
		'padright'              : MAG_PFUNC,
		'pagename'              : MAG_VARIABLE | MAG_PFUNC,
		'pagenamee'             : MAG_VARIABLE | MAG_PFUNC,
		'pagesincategory'       : MAG_PFUNC,//
		'pagesinnamespace'      : MAG_PFUNC,//
		'pagesize'              : MAG_PFUNC,//
		'plural'                : MAG_PFUNC,
		'protectionlevel'       : MAG_PFUNC,//
		'raw'                   : MAG_MODIFIER,
		'rel2abs'               : MAG_PFUNC_HASH,
		'revisionday'           : MAG_VARIABLE,//
		'revisionday2'          : MAG_VARIABLE,//
		'revisionid'            : MAG_VARIABLE,//
		'revisionmonth'         : MAG_VARIABLE,//
		'revisionmonth1'        : MAG_VARIABLE,//
		'revisiontimestamp'     : MAG_VARIABLE,//
		'revisionuser'          : MAG_VARIABLE,//
		'revisionyear'          : MAG_VARIABLE,//
		'safesubst'             : MAG_MODIFIER,
		'scriptpath'            : MAG_VARIABLE,//
		'server'                : MAG_VARIABLE,//
		'servername'            : MAG_VARIABLE,//
		'sitename'              : MAG_VARIABLE,//
		'special'               : MAG_PFUNC_HASH,//
		'staticredirect'        : MAG_UNDERSCORE,
		'stylepath'             : MAG_VARIABLE,//
		'subjectpagename'       : MAG_VARIABLE | MAG_PFUNC,
		'subjectpagenamee'      : MAG_VARIABLE | MAG_PFUNC,
		'subjectspace'          : MAG_VARIABLE | MAG_PFUNC,
		'subjectspacee'         : MAG_VARIABLE | MAG_PFUNC,
		'subpagename'           : MAG_VARIABLE | MAG_PFUNC,
		'subpagenamee'          : MAG_VARIABLE | MAG_PFUNC,
		'subst'                 : MAG_MODIFIER,
		'switch'                : MAG_PFUNC_HASH,
		'tag'                   : MAG_PFUNC_HASH,
		'talkpagename'          : MAG_VARIABLE | MAG_PFUNC,
		'talkpagenamee'         : MAG_VARIABLE | MAG_PFUNC,
		'talkspace'             : MAG_VARIABLE | MAG_PFUNC,
		'talkspacee'            : MAG_VARIABLE | MAG_PFUNC,
		'time'                  : MAG_PFUNC_HASH,
		'timel'                 : MAG_PFUNC_HASH,
		'titleparts'            : MAG_PFUNC_HASH,
		'toc'                   : MAG_UNDERSCORE,
		'uc'                    : MAG_PFUNC,
		'ucfirst'               : MAG_PFUNC,
		'urlencode'             : MAG_PFUNC
	};
}();