Benutzer:P.Copp/scripts/preprocessor.js
aus Wikipedia, der freien Enzyklopädie
< Benutzer:P.Copp | scripts
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
- Opera: Strg+F5
/**************************************************************************************************
* preprocessor.js
* Wikitext preprocessor, based on MediaWiki's parser (Preprocessor_DOM.php r55795)
* http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/parser/Preprocessor_DOM.php
* <nowiki>
*/
importScript( 'Benutzer:P.Copp/scripts/wiki.js' );
/**************************************************************************************************
* Local defines
*/
if( !window.LocalDefines ) LocalDefines = {};
if( !window.Wiki ) Wiki = {};
LocalDefines.extensiontags = [
'categorytree',
'charinsert',
'hiero',
'imagemap',
'inputbox',
'poem',
'ref',
'references',
'source',
'syntaxhighlight',
'timeline'
];
/**************************************************************************************************
* preprocessToObject()
*
* Turns a wikitext string into a document tree
* The returned data structure is a bit more compact than a real XML DOM, so
* some memory is saved, when the extra stuff is not needed.
* Use PPFrameXML to expand the compact form into an XML string
* with the same structure as returned by MediaWiki
*
* The returned object has the following structure:
* domnode = {
* type : ('root'|'link'|'template'|'tplarg'|'h'|'comment'|'ignore'|'ext'),
* offset: int,
* len : int,
* parts : [ [('text'|node)*], ... ],
* index, level : int, //only for heading nodes
* extname: 'name', //only for ext nodes
* }
*
* Dependencies: LocalDefines.extensiontags, Wiki.defaulttags
*/
Wiki.preprocessToObject = function( text, forInclusion ) {
if( text === false ) return text;
var lastindex = 0;
var stack = [];
var top = new Node( 'root', 0 );
var headings = 0;
var skipnewline = false;
var tag = null;
var enableonlyinclude = false;
var search = false;
var match;
//Line 145-156
if( forInclusion
&& text.indexOf( '<onlyinclude>' ) > -1
&& text.indexOf( '</onlyinclude>' ) > -1 )
{
enableonlyinclude = true;
tag = new Node( 'ignore', 0 );
search = /<onlyinclude>|^$/;
}
var ignoredtag = forInclusion ? /includeonly/i : /noinclude|onlyinclude/i;
var ignoredelement = forInclusion ? 'noinclude' : 'includeonly';
//Construct our main regex
var tags = '(' + Wiki.defaulttags.concat( LocalDefines.extensiontags ).join( '|' ) + ')';
var specials = '\\{\\{+|\\[\\[+|\\}\\}+|\\]\\]+|\\||(\n)(=*)|(^=+)';
var regex = RegExp( specials + '|<' + tags + '(?:\\s[^>]*)?\\/?>|<\\/'
+ tags + '\\s*>|<!--|-->|$', 'ig' );
while( match = regex.exec( text ) ) {
var s = match[0];
//If we're in searching mode, skip all tokens until we find a matching one
if( search ) {
if( s.match( search ) ) {
var search = false;
if( tag.type != 'comment' ) {
add( tag, text.substring( lastindex, match.index ) );
lastindex = match.index + s.length;
if( tag.type != 'ignore' ) tag.parts.push( tag.cur = [] );
add( tag, s );
processToken( 'tag', finish( tag, match.index + s.length ) );
}
}
continue;
}
if( s == '<!--' ) { //Comment found
var span = getCommentSpan( match.index );
processToken( 'text', text.substring( lastindex, span[0] ) );
lastindex = span[1];
tag = new Node( 'comment', span[0], text.substring( span[0], span[1] ) );
processToken( 'tag', finish( tag, span[1] ) );
var search = /-->|^$/;
//If we put a trailing newline in the comment, make sure we don't double output it
if( text.charAt( span[1] - 1 ) == '\n' ) skipnewline = true;
continue;
}
//Process all text between the last and the current token
if( match.index > lastindex )
processToken( 'text', text.substring( lastindex, match.index ) );
lastindex = match.index + s.length;
if( !s ) break; //End of text
if( match[1] || match[3] ) { //Line start/end
if( skipnewline || match[3] ) skipnewline = false;
else{
processToken( 'lineend', '', match.index );
processToken( 'text', '\n' );
}
//processToken( 'linestart' );
if( match[2] || match[3] )
processToken( '=', match[2] || match[3], match.index + ( match[1] ? 1 : 0 ) );
continue;
}
if( match[4] ) { //Open <tag /?> found
if( match[4].match( ignoredtag ) ) {
processToken( 'tag', finish( new Node( 'ignore', match.index, s ), lastindex ) );
continue;
}
var lc = match[4].toLowerCase();
if( lc == 'onlyinclude' ) {
//This can only happen, if we're in template mode (forInclusion=true) and
//the token we found is sth. like '<ONLYINCLUDE >'(i.e. unusual case or whitespace)
//Output it literally then, to match MediaWiki's behavior
processToken( 'text', s );
} else {
if( lc == ignoredelement ) tag = new Node( 'ignore', match.index, s );
else {
tag = new Node( 'ext', match.index, s );
tag.extname = lc;
}
if( s.charAt( s.length - 2 ) == '/' ) {
//Immediately closed tag (e.g. <nowiki />)
processToken( 'tag', finish( tag, match.index + s.length ) );
} else {
//Search for the matching closing tag
var search = RegExp( '<\\/' + lc + '\\b|^$', 'i' );
//For ext nodes, we split the opening tag, content and closing tag into
//separate parts. This is to simplify further processing since we already have
//the information after all
if( lc != ignoredelement ) tag.parts.push( tag.cur = [] );
}
}
continue;
} else if( match[5] ) { //Close </tag> found
if( match[5].match( ignoredtag ) ) {
processToken( 'ignore',
finish( new Node( 'ignore', match.index, s ), lastindex ) );
} else if( enableonlyinclude && s == '</onlyinclude>' ) {
//For onlyinclude, the closing tag is the start of the ignored part
var tag = new Node( 'ignore', match.index, s );
var search = /<onlyinclude>|^$/;
} else {
//We don't have a matching opening tag, so output the closing literally
processToken( 'text', s );
}
continue;
} else if( s == '-->' ) { //Comment endings without openings are output normally
processToken( 'text', s );
continue;
}
//Special token found: '|', {+, [+, ]+, }+
var ch = s.charAt( 0 );
processToken( ch, s, match.index );
}
//End of input. Put an extra line end to make sure all headings get closed properly
processToken( 'lineend', text.length );
processToken( 'end', text.length );
return stack[0];
//Handle some token and put it in the stack
function processToken( type, token, offset ) {
switch( type ) {
case 'text' :
case 'ignore' :
case 'tag' : return add( top, token );
case 'lineend': //Check if we can close a heading
if( top.type == 'h' ) {
var next = stack.pop();
if( top.closing ) {
//Some extra info for headings
top.index = ++headings;
top.level = Math.min( top.count, top.closing, 6 );
add( next, finish( top, offset ) );
} else {
//No correct closing, break the heading and continue
addBrokenNode( next, top );
}
top = next;
}
return;
case '=':
//Check if we can open a heading
var len = token.length;
//Line 352-355: Single '=' within a template part isn't treated as heading
if( len == 1 && top.type == '{' && top.parts.length > 1
&& typeof top.cur.splitindex == 'undefined' ) {
add( top, token );
} else {
stack.push( top );
top = new Node( 'h', offset, token, len );
//Line 447-455: More than two '=' means we already have a correct closing
top.closing = Math.floor( ( len - 1 ) / 2 );
}
return;
case '|':
//For brace nodes, start a new part
if( top.type == '[' || top.type == '{' ) top.parts.push( top.cur = [] );
else add( top, token );
return;
case '{' :
case '[' :
stack.push( top );
top = new Node( type, offset, '', token.length );
return;
case '}' :
case ']' :
//Closing brace found, try to close as many nodes as possible
var open = type == '}' ? '{' : '[';
var len = token.length;
while( open == top.type && len >= 2 ) {
while( len >= 2 && top.count >= 2 ) {
//Find the longest possible match
var mc = Math.min( len, top.count, open == '{' ? 3 : 2 );
top.count -= mc;
len -= mc;
//Record which type of node we found
if( open == '{' ) top.type = mc == 2 ? 'template' : 'tplarg';
else top.type = 'link';
if( top.count >= 2 ) {
//if we're still open, create a new parent and embed the node there
var child = top;
top = new Node( open, child.offset, child, child.count );
//Correct the child offset by the number of remaining open braces
child.offset += top.count;
finish( child, offset + token.length - len );
}
}
if( top.count < 2 ) {
//Close the current node
var next = stack.pop();
//There might be one remaining brace open, add it to the parent first
if( top.count == 1 ) add( next, open );
top.offset += top.count;
add( next, finish( top, offset + token.length - len ) );
top = next;
}
}
//Remaining closing braces are added as plain text
if( len ) add( top, ( new Array( len + 1 ) ).join( type ) );
return;
case 'end' :
//We've reached the end, expand any remaining open pieces
stack.push( top );
for( var i = 1; i < stack.length; i++ )
addBrokenNode( stack[0], stack[i] );
finish( stack[0], offset );
}
}
//Helper function to calculate the start and end position of a comment
//We need this, because comments sometimes include the preceding and trailing whitespace
//See lines 275-313
function getCommentSpan( start ) {
var endpos = text.indexOf( '-->', start + 4 );
if( endpos == -1 ) return [start,text.length];
for( var lead = start - 1; text.charAt( lead ) == ' '; lead-- );
if( text.charAt( lead ) != '\n' ) return [start,endpos+3];
for( var trail = endpos + 3; text.charAt( trail ) == ' '; trail++ );
if( text.charAt( trail ) != '\n' ) return [start,endpos+3];
return [lead+1,trail+1];
}
//DOM Node
function Node( type, offset, content, count ) {
this.type = type;
this.offset = offset;
this.parts = [[]];
//cur and count are only for internal processing.
//They will be cleaned up later by finish()
this.cur = this.parts[0];
if( content ) add( this, content );
if( count ) this.count = count;
}
//Append text or a child to a node
function add( node, el ) {
if( !el ) return;
var newstr = typeof el == 'string';
var oldstr = typeof node.cur[node.cur.length - 1] == 'string';
if( newstr && oldstr ) node.cur[node.cur.length - 1] += el;
else node.cur.push( el );
//For template nodes, record if and where an equal sign was found
if( newstr && node.type == '{' && typeof node.cur.splitindex == 'undefined'
&& el.indexOf( '=' ) > -1 ) node.cur.splitindex = node.cur.length - 1;
//For heading nodes, record if we have a correct closing
//A heading must end in one or more equal signs, followed only by
//whitespace or comments
if( node.type == 'h' ) {
if( newstr ) {
var match = el.match( /(=+)[ \t]*$/ );
if( match ) node.closing = match[1].length;
else if( !el.match( /^[ \t]*$/ ) ) node.closing = false;
} else if( el.type != 'comment' ) node.closing = false;
}
}
//Break and append a child to a node
function addBrokenNode( node, el ) {
//First add the opening braces
if( el.type != 'h' ) add( node, ( new Array( el.count + 1 ) ).join( el.type ) );
//Then the parts, separated by '|'
for( var i = 0; i < el.parts.length; i++ ) {
if( i > 0 ) add( node, '|' );
for( var j = 0; j < el.parts[i].length; j++ ) add( node, el.parts[i][j] );
}
}
//Clean up the extra stuff we put into the node for easier processing
function finish( node, endOffset ) {
node.len = endOffset - node.offset;
node.lineStart = text.charAt( node.offset - 1 ) == '\n';
delete node.cur;
delete node.count;
delete node.closing;
return node;
}
};
/**************************************************************************************************
* PPFrame : Basic expansion frame, transforms a document tree back to the original wikitext
*/
function PPFrame() { this.self = PPFrame; }
PPFrame.prototype = {
onEvent : function( evt, node, result, info ) {
},
expand : function( obj ) {
if( typeof obj == 'string' ) {
var result = this.expandString( obj );
this.onEvent( 'text', obj, result );
return result;
}
var type = obj.type.charAt( 0 ).toUpperCase() + obj.type.substring( 1 );
var func = this['expand' + type];
if( !func ) throw 'Unknown node type: ' + obj.type;
this.onEvent( 'enter' + type, obj );
var result = func.call( this, obj );
this.onEvent( 'leave' + type, obj, result );
return result;
},
expandString : function( s ) { return s; },
expandRoot : function( obj ) { return this.expandPart( obj.parts[0] ); },
expandLink : function( obj ) {
return this.expand( '[[' ) + this.expandParts( obj.parts, '|' ) + this.expand( ']]' );
},
expandTemplate : function( obj ) {
return this.expand( '{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}' );
},
expandTplarg : function( obj ) {
return this.expand( '{{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}}' );
},
expandH : function( obj ) { return this.expandPart( obj.parts[0] ); },
expandComment : function( obj ) { return this.expand( obj.parts[0][0] ); },
expandIgnore : function( obj ) { return this.expand( obj.parts[0][0] ); },
expandExt : function( obj ) { return this.expandParts( obj.parts ); },
expandPart : function( part ) {
var result = '';
for( var i = 0; i < part.length; i++ ) result += this.expand( part[i] );
return result;
},
expandParts : function( parts, joiner ) {
var result = '';
for( var i = 0; i < parts.length; i++ ) {
if( joiner && i > 0 ) result += this.expand( joiner );
result += this.expandPart( parts[i] );
}
return result;
},
splitPart : function( part ) {
var i = part.splitindex;
if( typeof i == 'undefined' ) return false;
var pos = part[i].indexOf( '=' );
var name = part.slice( 0, i );
name.push( part[i].substring( 0, pos ) );
var value = [part[i].substring( pos + 1 )].concat( part.slice( i + 1 ) );
return [name,value];
},
extractParams : function( obj ) {
var params = { //numbered and named arguments must be stored separately
numbered : {},
named : {},
obj : obj
};
var num = 1;
for( var i = 1; i < obj.parts.length; i++ ) {
var split = this.splitPart( obj.parts[i] );
if( split ) {
var name = this.expandArgName( obj, split[0], i );
params.named[name] = { value : split[1], part : i };
} else params.numbered[num++] = { part : i };
}
return params;
},
getParam : function( params, name ) {
for( var i = 0; i < 2; i++ ) {
var type = i ? 'named' : 'numbered';
var param = params[type][name];
if( !param ) continue;
if( typeof param.value == 'string' ) return param.value; //cached
//Param exists, but not yet expanded. Expand it and put the result in the cache
param.value = i
? this.expandArgValue( params.obj, param.value, param.part )
: this.expandArg( params.obj, param.part );
return param.value;
}
return false;
},
expandArgName : function( obj, part, num ) {
this.onEvent( 'enterArgName', obj, null, [part,num] );
var result = this.expandPart( part ).trim();
this.onEvent( 'leaveArgName', obj, result, [part,num] );
return result;
},
expandArgValue : function( obj, part, num ) {
this.onEvent( 'enterArgValue', obj, null, [part,num] );
var result = this.expandPart( part ).trim();
this.onEvent( 'leaveArgValue', obj, result, [part,num] );
return result;
},
expandArg : function( obj, num ) {
if( typeof obj.parts[num] == 'undefined' ) return '';
this.onEvent( 'enterArg', obj, null, num );
var result = this.expandPart( obj.parts[num] );
this.onEvent( 'leaveArg', obj, result, num );
return result;
}
};
Wiki.ppFrame = new PPFrame();
/**************************************************************************************************
* PPFrameXML : Transforms a document tree to an XML string
*/
function PPFrameXML() { this.self = PPFrameXML; }
PPFrameXML.prototype = new PPFrame();
PPFrameXML.prototype.expandString = function( s ) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
};
PPFrameXML.prototype.expandRoot = function( obj ) {
return this.XML( 'root', this.expandPart( obj.parts[0] ) );
};
PPFrameXML.prototype.expandTemplate = function( obj ) {
var attr = obj.lineStart ? ' lineStart="1"' : '';
return this.XML( 'template', this.expandTemplateParts( obj.parts ), attr );
};
PPFrameXML.prototype.expandTplarg = function( obj ) {
var attr = obj.lineStart ? ' lineStart="1"' : '';
return this.XML( 'tplarg', this.expandTemplateParts( obj.parts ), attr );
};
PPFrameXML.prototype.expandH = function( obj ) {
return this.XML( 'h', this.expandPart( obj.parts[0] ),
' level="' + obj.level + '" i="' + obj.index + '"' );
};
PPFrameXML.prototype.expandComment = function( obj ) {
return this.XML( 'comment', this.expand( obj.parts[0][0] ) );
};
PPFrameXML.prototype.expandIgnore = function( obj ) {
return this.XML( 'ignore', this.expand( obj.parts[0][0] ) );
};
PPFrameXML.prototype.expandExt = function( obj ) {
var m = obj.parts[0][0].match( /<([^\s\/>]*)(.*)>/ );
var content = this.XML( 'name', this.expand( m[1] ) );
content += this.XML( 'attr', this.expand( m[2].replace( /\/$/, '' ) ) );
if( obj.parts[1] ) content += this.XML( 'inner', this.expand( obj.parts[1][0] ) );
if( obj.parts[2] && obj.parts[2][0] )
content += this.XML( 'close', this.expand( obj.parts[2][0] ) );
return this.XML( 'ext', content );
};
PPFrameXML.prototype.expandTemplateParts = function( parts ) {
var result = this.XML( 'title', this.expandPart( parts[0] ) );
var num = 1;
for( var i = 1; i < parts.length; i++ ) {
var split = this.splitPart( parts[i] );
if( split ) {
var content = this.XML( 'name', this.expandPart( split[0] ) ) + this.expand( '=' );
content += this.XML( 'value', this.expandPart( split[1] ) );
} else {
var content = this.XML( 'name', '', ' index="' + ( num++ ) + '"' );
content += this.XML( 'value', this.expandPart( parts[i] ) );
}
result += this.XML( 'part', content );
}
return result;
};
PPFrameXML.prototype.XML = function( name, content, attr ) {
return '<' + name + ( attr || '' ) + ( content ? '>' + content + '</' + name + '>' : '/>' );
};
Wiki.ppFrameXML = new PPFrameXML();
/**************************************************************************************************
* Experimental stuff
*/
/**************************************************************************************************
* PPTemplateFrame: Transform a document tree into expanded wikitext. Tries to approximate the
* behavior of MediaWiki's template expansion. Not all variables and parser functions have been
* implemented, though, others may work a bit differently from their original.
*/
function PPTemplateFrame() { this.self = PPTemplateFrame; }
PPTemplateFrame.prototype = new PPFrame();
PPTemplateFrame.prototype.setContextPage = function( page ) {
this.contextPage = page;
if( !this.page ) this.page = page;
};
PPTemplateFrame.prototype.createNewChild = function( obj, page ) {
var child = new this.self();
child.setContextPage( this.contextPage );
child.page = page;
child.callback = this.callback;
child.parent = {
frame : this,
params : this.extractParams( obj )
};
return child;
};
//like expand(), but loads template texts etc. asynchronously
//Note: On every ajax call the expansion starts from the beginning, so we get kind of a quadratic performance here
PPTemplateFrame.prototype.expandAsync = function( obj, callback, statusCallback, maxloads ) {
if( !maxloads ) var maxloads = 100; //Upper limit for the number of ajax calls to the server
var frame = this;
this.callback = function() {
maxloads--;
if( !maxloads ) throw 'Too many ajax calls';
var result = false;
try{ var result = frame.expand( obj ); }
catch( e ) {
if( e && e.forceMode ) {
if( statusCallback ) statusCallback( e.msg, e.args );
} else throw e;
}
if( result !== false ) callback( result );
};
this.callback();
};
PPTemplateFrame.prototype.getAjaxItem = function( obj, funcname, args ) {
var cached = obj[funcname + 'Cached'].apply( obj, args );
if( cached !== null ) return cached;
if( this.callback ) {
args.push( this.callback );
obj[funcname].apply( obj, args );
throw { forceMode : true, msg : funcname, args : obj };
}
return obj[funcname].apply( obj, args );
};
PPTemplateFrame.prototype.getTemplateDom = function( page ) {
return this.getAjaxItem( page, 'getTemplateDom', [] );
};
PPTemplateFrame.prototype.expandTemplate = function( obj ) {
//Double brace expansion
var name = this.expandArg( obj, 0 ).trim();
var fname = Wiki.getMagicWord( name );
if( fname && Wiki.magicIsVar( fname ) ) return this.expandVar( fname, obj );
var pos = name.indexOf( ':' );
if( pos > -1 ) {
var fname = Wiki.getMagicWord( name.substring( 0, pos ) );
if( fname == 'subst' ) return this.expandBrokenTemplate( obj );
//TODO: Modifiers {{MSG:...}} etc.
if( fname && Wiki.magicIsFunc( fname ) )
return this.expandFunc( fname, name.substring( pos + 1 ).trim(), obj );
}
if( name.charAt( 0 ) == '/' ) name = this.contextPage.ptitle + name; //TODO: {{../}} etc.
var page = Wiki.getPage( name, 10 );
if( page ) {
this.getAjaxItem( page, 'getText', [true] );
if( page.getVal( 'redirect' ) ) page = Wiki.getPage( page.getVal( 'redirect' ) );
var dom = this.getTemplateDom( page );
if( !dom ) {
this.onEvent( 'brokenTemplate', obj );
return this.expand( '[[:' + page.ptitle + ']]' );
} else return this.expandTemplatePage( page, dom, obj );
} else return this.expandBrokenTemplate( obj );
};
PPTemplateFrame.prototype.expandTplarg = function( obj ) {
//Triple brace expansion
var name = this.expandArg( obj, 0 ).trim();
if( this.parent ) {
var value = this.parent.frame.getParam( this.parent.params, name );
if( value !== false ) {
this.onEvent( 'param', obj, value, name );
return value;
}
}
//No matching param found, try the default
if( obj.parts.length > 1 ) return this.expandArg( obj, 1 );
this.onEvent( 'brokenTplarg', obj );
return this.expand( '{{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}}' );
};
PPTemplateFrame.prototype.expandIgnore =
PPTemplateFrame.prototype.expandComment = function() { return ''; };
PPTemplateFrame.prototype.expandVar = function( name, obj ) {
var result = Wiki.getVarResult( name, this.contextPage );
this.onEvent( 'var', obj, result, name );
return result;
};
PPTemplateFrame.prototype.expandFunc = function( name, arg, obj ) {
var func = this.parserFunctions[name];
if( !func ) return this.expandBrokenTemplate( obj );
this.onEvent( 'enterFunc', obj, null, [name,arg] );
var result = func.call( this, obj, arg );
//see Parser.php line 3026
if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) result = '\n' + result;
this.onEvent( 'leaveFunc', obj, result, [name,arg] );
return result;
};
PPTemplateFrame.prototype.expandTemplatePage = function( page, dom, obj ) {
this.onEvent( 'enterTemplatePage', obj, null, [page,dom] );
var child = this.createNewChild( obj, page );
var result = child.expand( dom );
//see Parser.php line 3026
if( !obj.lineStart && result.match( /^(?:\{\||:|;|#|\*)/ ) ) result = '\n' + result;
this.onEvent( 'leaveTemplatePage', obj, result, [page,dom] );
return result;
};
PPTemplateFrame.prototype.expandBrokenTemplate = function( obj ) {
this.onEvent( 'brokenTemplate', obj );
return this.expand( '{{' ) + this.expandParts( obj.parts, '|' ) + this.expand( '}}' );
};
PPTemplateFrame.prototype.expandExt = function( obj ) {
if( obj.parts[1] && ( obj.extname == 'ref' || obj.extname == 'poem' ) ) {
var open = this.expandPart( obj.parts[0] );
var innerdom = Wiki.preprocessToObject( obj.parts[1].join( '' ), false );
return open + this.expandPart( innerdom.parts[0] ) + this.expandPart( obj.parts[2] );
} else return this.expandParts( obj.parts );
};
PPTemplateFrame.prototype.parserFunctions = {
'if' : function( obj, arg ) {
if( arg ) return this.expandArg( obj, 1 ).trim();
return this.expandArg( obj, 2 ).trim();
},
'ifeq' : function( obj, arg ) {
if( arg == this.expandArg( obj, 1 ).trim() )
return this.expandArg( obj, 2 ).trim();
return this.expandArg( obj, 3 ).trim();
},
'titleparts' : function( obj, arg ) {
var title = Wiki.getPage( arg );
if( !title ) return arg;
var bits = title.ptitle.split( '/', 25 );
var offset = Math.max( 0, ( parseInt( this.expandArg( obj, 2 ), 10 ) || 0 ) - 1 );
var end = parseInt( this.expandArg( obj, 1 ), 10 ) || 0;
end = end > 0 ? offset + end : bits.length + end;
return bits.slice( offset, end ).join( '/' );
},
'switch' : function( obj, arg ) {
var found = false;
var defaultfound = false;
var switchdefault = false;
for( var i = 1; i < obj.parts.length; i++ ) {
var split = this.splitPart( obj.parts[i] );
if( split ) {
var left = this.expandArgName( obj, split[0], i );
if( found || left == arg ) return this.expandArgValue( obj, split[1], i );
else if( defaultfound || left == '#default' ) {
switchdefault = split[1];
var defaultindex = i;
}
} else {
var left = this.expandArg( obj, i ).trim();
if( left == arg ) found = true;
else if( left == '#default' ) defaultfound = true;
}
}
if( !split ) return left;
if( switchdefault !== false )
return this.expandArgValue( obj, switchdefault, defaultindex );
return '';
},
'expr': function( obj, arg ) {
try{ return ExpressionParser.eval( arg ) + ''; }
catch( e ){ return '<strong class="error">'+e+'</strong>'; }
},
'ifexpr': function( obj, arg ) {
try{ var value = ExpressionParser.eval( arg ); }
catch( e ){ return '<strong class="error">'+e+'</strong>'; }
if( value ) return this.expandArg( obj, 1 ).trim();
return this.expandArg( obj, 2 ).trim();
},
'lc': function( obj, arg ) { return arg.toLowerCase(); },
'uc': function( obj, arg ) { return arg.toUpperCase(); },
'lcfirst': function( obj, arg ) { return Wiki.lcfirst( arg ); },
'ucfirst': function( obj, arg ) { return Wiki.ucfirst( arg ); },
'iferror': function( obj, arg ) {
if( arg.match( /<(?:strong|span|p|div)\s[^>]*\bclass="[^">]*\berror\b[^">]*"/ ) )
return this.expandArg( obj, 1 ).trim();
if( obj.parts.length > 2 ) return this.expandArg( obj, 2 ).trim();
return arg;
},
'urlencode': function( obj, arg ) { return Wiki.urlencode( arg ); },
'anchorencode': function( obj, arg ) { return Wiki.anchorencode( arg ); },
'formatnum': function( obj, arg ) {
if( this.expandArg( obj, 1 ).indexOf( 'R' ) > -1 )
return Wiki.parseFormattedNumber( arg );
return Wiki.formatNum( arg );
},
'ifexist': function( obj, arg ) {
var page = Wiki.getPage( arg );
if( page && this.getAjaxItem( page, 'exists', [] ) ) return this.expandArg( obj, 1 ).trim();
return this.expandArg( obj, 2 ).trim();
},
'time': function( obj, arg, local ) {
var date = false;
if( obj.parts.length > 1 ) {
var secs = Date.parse( this.expandArg( obj, 1 ).trim() );
if( secs ) date = new Date( secs );
} else date = local ? Wiki.getLocalDate() : Wiki.getUTCDate();
if( date ) date = Wiki.formatDate( date, arg );
if( date ) return date;
var call = '{{#time' + ( local ? 'l' : '' ) + ':' + arg;
if( obj.parts.length > 1 ) call += '|' + this.expandArg( obj, 1 );
return this.getAjaxItem( Wiki, 'getFunc', [call + '}}'] );
},
'timel': function( obj, arg ) {
return this.parserFunctions['time'].call( this, obj, arg, true );
},
'tag': function( obj, arg ) {
var tagname = arg.toLowerCase();
var result = '<' + tagname;
var inner = this.expandArg( obj, 1 );
if( !Wiki.tagExists( tagname ) )
return '<span class="error">Unknown extension tag "' + tagname + '"</span>';
for( var i = 2; i < obj.parts.length; i++ ) {
var split = this.splitPart( obj.parts[i] );
if( !split ) continue;
result += ' ' + this.expandArgName( obj, split[0], i ) + '="';
result += this.expandArgValue( obj, split[1], i )
.replace( /^\s*["'](.*)["']\s*$/, '$1' ) + '"';
}
return result + '>' + inner + '</' + tagname + '>';
},
'padright': function( obj, arg ) {
var len = obj.parts.length > 1 ? parseInt( this.expandArg( obj, 1 ).trim(), 10 ) : 0;
var pad = obj.parts.length > 2 ? this.expandArg( obj, 2 ).trim() : '0';
return Wiki.padString( arg, len, pad, 'right' );
},
'padleft': function( obj, arg ) {
var len = obj.parts.length > 1 ? parseInt( this.expandArg( obj, 1 ).trim(), 10 ) : 0;
var pad = obj.parts.length > 2 ? this.expandArg( obj, 2 ).trim() : '0';
return Wiki.padString( arg, len, pad, 'left' );
},
'ns': function( obj, arg ) {
var index = parseInt( arg, 10 );
if( !index && arg !== '0' )
index = Wiki.getNS( arg.toLowerCase().replace( /[ _]+/g, '_' ) );
if( index === false ) return '[[:Template:Ns:' + arg + ']]';//FIXME
return wgFormattedNamespaces[index];
},
'nse': function( obj, arg ) {
return Wiki.titleencode( this.parserFunctions['ns'].call( this, obj, arg ) );
},
'localurl': function( obj, arg ) {
var title = Wiki.getPage( arg );
var query = this.expandArg( obj, 1 ).trim();
if( !title ) return '[[:Template:Localurl:' + arg + ']]';//FIXME
if( title.ns == -2 ) title = new Wiki.Page( 6, title.title );
var dbk = Wiki.titleencode( title.ptitle );
if( !query ) return wgArticlePath.replace( /\$1/, dbk );
else return wgScriptPath + '/index.php?title=' + dbk + '&' + ( query == '-' ? '' : query );
},
'localurle': function( obj, arg ) {
return Wiki.escapeXML( this.parserFunctions.localurl.call( this, obj, arg ) );
},
'fullurl': function( obj, arg ) {
//TODO: Fragments
return wgServer + this.parserFunctions.localurl.call( this, obj, arg );
},
'fullurle': function( obj, arg ) {
return Wiki.escapeXML( this.parserFunctions.fullurl.call( this, obj, arg ) );
},
'int': function( obj, arg ) {
if( !arg ) return '[[:Template:Int:' + arg + ']]';//FIXME
var params = [];
for( var i = 1; i < obj.parts.length; i++ )
params.push( this.expandArg( obj, i ).trim() );
var msg = this.getAjaxItem( Wiki, 'getMessage', [wgUserLanguage, arg] );
return Wiki.insertParams( msg, params );
},
/* 'displaytitle': function( obj, arg ) {
return '{{DISPLAYTITLE:'+arg+'}}';
},
'defaultsort': function( obj, arg ) {
return '{{DEFAULTSORT:'+arg+'}}';
},*/
'rel2abs': function( obj, arg ) {
var from = this.expandArg( obj, 1 ).trim();
if( !from ) from = this.contextPage.ptitle;
var to = arg.replace( /[ \/]+$/, '' );
if( !to || to == '.' ) return from;
if( !to.match( /^\.?\.?\/|^\.\.$/ ) ) from = '';
var fullpath = '/' + from + '/' + to + '/';
fullpath = fullpath.replace( /\/(\.\/)+/g, '/' );
fullpath = fullpath.replace( /\/\/+/g, '/' );
fullpath = fullpath.replace( /^\/+|\/+$/g, '' );
var bits = fullpath.split( '/' );
var newbits = [];
for( var i = 0; i < bits.length; i++ ) {
if( bits[i] == '..' ) {
if( !newbits.length )
return '<strong class="error">Error: Invalid depth in path: "' + fullpath
+ '" (tried to access a node above the root node)</strong>';
newbits.pop();
} else newbits.push( bits[i] );
}
return newbits.join( '/' );
},
'plural': function( obj, arg ) {
var num = parseInt( Wiki.parseFormattedNumber( arg ), 10);
if( num == 1 ) return this.expandArg( obj, 1 );
return this.expandArg( obj, 2 ) || this.expandArg( obj, 1 );
},
'namespace' : function( obj, arg ) {
var title = Wiki.getPage( arg );
if( !title ) return '';
return wgFormattedNamespaces[title.ns];
},
'fullpagename' : function( obj, arg ) {
var title = Wiki.getPage( arg );
if( !title ) return '';
return title.ptitle;
},
'talkpagename' : function( obj, arg ) {
var title = Wiki.getPage( arg );
if( !title ) return '';
return ( new Wiki.Page( title.ns | 1, title.title ) ).ptitle;
}
};
/**************************************************************************************************
* helper functions
*/
Wiki.defaulttags = ['nowiki', 'gallery', 'math', 'pre', 'noinclude', 'includeonly', 'onlyinclude'];
Wiki.tagExists = function( name ) {
for( var i = 0; i < Wiki.defaulttags.length; i++ )
if( name == Wiki.defaulttags[i] ) return true;
for( var i = 0; i < LocalDefines.extensiontags.length; i++ )
if( name == LocalDefines.extensiontags[i] ) return true;
return false;
};
Wiki.getVarResult = function( name, title ) {
var t = title.title, p = title.ptitle, ns = title.ns;
switch( name ) {
case 'pagename' : return t;
case 'pagenamee' : return Wiki.titleencode( t );
case 'namespace' : return wgFormattedNamespaces[ns];
case 'namespacee' : return Wiki.titleencode( wgFormattedNamespaces[ns] );
case 'talkspace' : return wgFormattedNamespaces[ns | 1];
case 'talkspacee' : return Wiki.titleencode( wgFormattedNamespaces[ns | 1] );
case 'subjectspace' : return wgFormattedNamespaces[ns & -2];
case 'subjectspacee' : return Wiki.titleencode( wgFormattedNamespaces[ns & -2] );
case 'fullpagename' : return p;
case 'fullpagenamee' : return Wiki.titleencode( p );
case 'subpagename' :
case 'subpagenamee' : //TODO: Namespaces without subpages
var title = t.substring( t.lastIndexOf( '/' ) + 1 );
if( name == 'subpagename' ) return title;
else return Wiki.titleencode( title );
case 'basepagename' :
case 'basepagenamee' : var pos = t.indexOf( '/' );
var title = pos > -1 ? t.substring( 0, pos ) : t;
if( name == 'basepagename' ) return title;
else return Wiki.titleencode( title );
case 'talkpagenamee' :
case 'talkpagename' : var title = new Wiki.Page( ns | 1, t ).ptitle;
if( name == 'talkpagename' ) return title;
else return Wiki.titleencode( title );
case 'subjectpagename' :
case 'subjectpagenamee' : var title = new Wiki.Page( ns & -2, t ).ptitle;
if( name == 'talkpagename' ) return title;
else return Wiki.titleencode( title );
}
if( name.indexOf( 'current' ) === 0 ) {
var date = Wiki.getUTCDate();
var name = name.substring( 7 );
} else if( name.indexOf( 'local' ) === 0 ) {
var date = Wiki.getLocalDate();
var name = name.substring( 5 );
} else return '{{'+name.toUpperCase()+'}}';
switch( name ) {
case 'year' : return Wiki.formatDate( date, 'Y' );
case 'month' : return Wiki.formatDate( date, 'm' );
case 'month1' : return Wiki.formatDate( date, 'n' );
case 'monthname' : return Wiki.formatDate( date, 'F' );
case 'monthnamegen' : return Wiki.formatDate( date, 'xg' );
case 'monthabbrev' : return Wiki.formatDate( date, 'M' );
case 'day' : return Wiki.formatDate( date, 'j' );
case 'day2' : return Wiki.formatDate( date, 'd' );
case 'dow' : return Wiki.formatDate( date, 'N' );
case 'dayname' : return Wiki.formatDate( date, 'l' );
case 'hour' : return Wiki.formatDate( date, 'H' );
case 'time' : return Wiki.formatDate( date, 'H:i' );
case 'timestamp' : return Wiki.formatDate( date, 'YmdHis' );
}
return '{{'+name.toUpperCase()+'}}';
};
/**************************************************************************************************
* ExpressionParser
* ported from http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/ParserFunctions/Expr.php
*/
ExpressionParser = {
eval : function( expr ) {
var operands = [];
var operators = [];
var p = 0;
var end = expr.length;
var expectExpression = true;
var numeric = '0123456789.';
var whitespace = ' \t\n\r';
while( p < end ) {
if( operands.length > 100 || operators.length > 100 ) throw 'Stack exhausted';
var ch = expr.charAt( p );
var ch2= expr.substr( p, 2 );
if( whitespace.indexOf( ch ) > -1 ) {
p++;
continue;
} else if ( numeric.indexOf( ch ) > -1 ) {
if( !expectExpression ) throw 'Unexpected number';
var num = expr.substr( p ).match( /^[0123456789\.]*/ )[0];
operands.push( parseFloat( num ) );
p += num.length;
expectExpression = false;
continue;
} else if( ch.match( /[A-Za-z]/ ) ) {
var word = expr.substr( p ).match( /^[A-Za-z]*/ )[0].toLowerCase();
p += word.length;
switch( word ) {
case 'e' :
if( !expectExpression ) break;
operands.push( Math.E );
expectExpression = false;
continue;
case 'pi' :
if( !expectExpression ) throw 'Unexpected number';
operands.push( Math.PI );
expectExpression = false;
continue;
case 'not' :
case 'sin' :
case 'cos' :
case 'tan' :
case 'asin' :
case 'acos' :
case 'atan' :
case 'exp' :
case 'ln' :
case 'abs' :
case 'floor':
case 'trunc':
case 'ceil' :
if( !expectExpression ) throw 'Unexpected '+word+' operator';
operators.push( word );
continue;
case 'mod' :
case 'and' :
case 'or' :
case 'round':
case 'div' : break;
default : throw 'Unrecognised word "'+word+'"';
}
} else if( ch2 == '<=' || ch2 == '>=' || ch2 == '<>' || ch2 == '!=' ) {
var word = ch2;
p += 2;
} else if( ch == '+' || ch == '-' ) {
p++;
if( expectExpression ) {
operators.push( ch );
continue;
} else var word = ch+ch;
} else if( ch == '*' || ch == '/' || ch == '^' || ch == '=' || ch == '<' || ch == '>' ) {
p++;
var word = ch;
} else if( ch == '(' ) {
if( !expectExpression ) throw 'Unexpected ( operator';
operators.push( ch );
p++;continue;
} else if( ch == ')' ) {
var i = operators.length - 1;
while( i >= 0 && operators[i] != '(' ) {
this.doOperation( operators[i], operands );
operators.pop();i--;
}
if( i < 0 ) throw 'Unexpected closing bracket';
operators.pop();
expectExpression = false;
p++;
continue;
} else throw 'Unrecognised punctuation character "'+ch+'"';
if( expectExpression ) throw 'Unexpected '+word+' operator';
var i = operators.length - 1;
while( i >= 0 && this.precedence[word] <= this.precedence[operators[i]] ) {
this.doOperation( operators[i], operands );
operators.pop();i--;
}
operators.push( word );
expectExpression = true;
}
var i = operators.length - 1;
while( i >= 0 ) {
if( operators[i] == '(' ) throw 'Unclosed bracket';
this.doOperation( operators[i], operands );
i--;
}
return operands[0];
},
doOperation : function( op, stack ) {
if( stack.length < this.arity[op] ) throw 'Missing operand for '+op;
var right = stack.pop();
switch( op ) {
case '-' : stack.push( -right );return;
case '+' : stack.push( right );return;
case '*' : stack.push( stack.pop() * right );return;
case 'div' :
case '/' : if( right == 0 ) throw 'Division by zero';
stack.push( stack.pop() / right );return;
case 'mod' : if( right == 0 ) throw 'Division by zero';
right = right > 0 ? Math.floor( right ) : Math.ceil( right );
var left = stack.pop();
left = left >= 0 ? Math.floor( left ) : Math.ceil( left );
stack.push( left % right );return;
case '++' : stack.push( stack.pop() + right );return;
case '--' : stack.push( stack.pop() - right );return;
case 'and' : stack.push( stack.pop() && right ? 1 : 0 );return;
case 'or' : stack.push( stack.pop() || right ? 1 : 0 );return;
case '=' : stack.push( stack.pop() == right ? 1 : 0 );return;
case 'not' : stack.push( right ? 0 : 1 );return;
case 'round': var digits = Math.floor( right );
stack.push( Math.round(stack.pop() * Math.pow(10,digits))/Math.pow(10,digits) );
return;
case '<' : stack.push( stack.pop() < right ? 1 : 0 );return;
case '>' : stack.push( stack.pop() > right ? 1 : 0 );return;
case '<=' : stack.push( stack.pop() <= right ? 1 : 0 );return;
case '>=' : stack.push( stack.pop() >= right ? 1 : 0 );return;
case '<>' :
case '!=' : stack.push( stack.pop() == right ? 0 : 1 );return;
case 'e' : stack.push( stack.pop() * Math.pow(10,right) );return;
case 'sin' : stack.push( Math.sin( right ) );return;
case 'cos' : stack.push( Math.cos( right ) );return;
case 'tan' : stack.push( Math.tan( right ) );return;
case 'asin' : if( right < -1 || right > 1 ) throw 'Invalid argument for asin: < -1 or > 1';
stack.push( Math.asin( right ) );return;
case 'acos' : if( right < -1 || right > 1 ) throw 'Invalid argument for acos: < -1 or > 1';
stack.push( Math.acos( right ) );return;
case 'atan' : stack.push( Math.atan( right ) );return;
case 'exp' : stack.push( Math.exp( right ) );return;
case 'ln' : if( right <= 0 ) throw 'Invalid argument for ln: <= 0';
stack.push( Math.log( right ) );return;
case 'abs' : stack.push( Math.abs( right ) );return;
case 'floor': stack.push( Math.floor( right ) );return;
case 'ceil' : stack.push( Math.ceil( right ) );return;
case 'trunc': stack.push( right >= 0 ? Math.floor( right ) : Math.ceil( right ) );return;
case '^' : stack.push( Math.pow(stack.pop(), right) );return;
}
},
precedence : {
'-':10,'+':10,'e':10,'sin':9,'cos':9,'tan':9,'asin':9,'acos':9,'atan':9,'exp':9,'ln':9,'abs':9,
'floor':9,'trunc':9,'ceil':9,'not':9,'^':8,'*':7,'/':7,'div':7,'mod':7,'++':6,'--':6,
'round':5,'=':4,'<':4,'>':4,'<=':4,'>=':4,'<>':4,'!=':4,'and':3,'or':2,'pi':0,'(':-1,')':-1
},
arity : {
'-':1,'+':1,'e':2,'sin':1,'cos':1,'tan':1,'asin':1,'acos':1,'atan':1,'exp':1,'ln':1,'abs':1,
'floor':1,'trunc':1,'ceil':1,'not':1,'^':2,'*':2,'/':2,'div':2,'mod':2,'++':2,'--':2,
'round':2,'=':2,'<':2,'>':2,'<=':2,'>=':2,'<>':2,'!=':2,'and':2,'or':2
}
};