- * Experimental advanced wikitext parser-emitter.
- * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
- *
- * @author neilk@wikimedia.org
- */
-
-( function( mw, $, undefined ) {
-
- mw.jqueryMsg = {};
+* Experimental advanced wikitext parser-emitter.
+* See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
+*
+* @author neilk@wikimedia.org
+*/
+( function ( mw, $ ) {
+ var slice = Array.prototype.slice,
+ parserDefaults = {
+ magic : {
+ 'SITENAME' : mw.config.get( 'wgSiteName' )
+ },
+ messages : mw.messages,
+ language : mw.language
+ };
/**
* Given parser options, return a function that parses a key and replacements, returning jQuery object
* @param {Object} parser options
* @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery}
*/
/**
* Given parser options, return a function that parses a key and replacements, returning jQuery object
* @param {Object} parser options
* @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery}
*/
* Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
* If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
* the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
* Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
* If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
* the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
* @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements.
* @return {jQuery}
*/
* @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements.
* @return {jQuery}
*/
* window.gM = mediaWiki.parser.getMessageFunction( options );
* $( 'p#headline' ).html( gM( 'hello-user', username ) );
*
* window.gM = mediaWiki.parser.getMessageFunction( options );
* $( 'p#headline' ).html( gM( 'hello-user', username ) );
*
- * somefunction(a, b, c, d)
- * is equivalent to
+ * somefunction(a, b, c, d)
+ * is equivalent to
* somefunction(a, [b, c, d])
*
* @param {String} message key
* @param {Array} optional replacements (can also specify variadically)
* @return {String} rendered HTML as string
*/
* somefunction(a, [b, c, d])
*
* @param {String} message key
* @param {Array} optional replacements (can also specify variadically)
* @return {String} rendered HTML as string
*/
- * Class method.
- * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
- * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
- * e.g.
+ * Class method.
+ * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
+ * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
+ * e.g.
* $( 'p#headline' ).msg( 'hello-user', userlink );
*
* @param {Array} parser options
* @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg
*/
* $( 'p#headline' ).msg( 'hello-user', userlink );
*
* @param {Array} parser options
* @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg
*/
- * somefunction(a, b, c, d)
- * is equivalent to
+ * somefunction(a, b, c, d)
+ * is equivalent to
* somefunction(a, [b, c, d])
* somefunction(a, [b, c, d])
* We append to 'this', which in a jQuery plugin context will be the selected elements.
* @param {String} message key
* @param {Array} optional replacements (can also specify variadically)
* @return {jQuery} this
*/
* We append to 'this', which in a jQuery plugin context will be the selected elements.
* @param {String} message key
* @param {Array} optional replacements (can also specify variadically)
* @return {jQuery} this
*/
this.settings = $.extend( {}, parserDefaults, options );
this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
};
mw.jqueryMsg.parser.prototype = {
this.settings = $.extend( {}, parserDefaults, options );
this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
};
mw.jqueryMsg.parser.prototype = {
// cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
// (This is why we would like to move this functionality server-side).
astCache: {},
// cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
// (This is why we would like to move this functionality server-side).
astCache: {},
* @param {String} key
* @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
* @param {String} key
* @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
var wikiText = this.settings.messages.get( key );
if ( typeof wikiText !== 'string' ) {
wikiText = "\\[" + key + "\\]";
}
this.astCache[ key ] = this.wikiTextToAst( wikiText );
}
var wikiText = this.settings.messages.get( key );
if ( typeof wikiText !== 'string' ) {
wikiText = "\\[" + key + "\\]";
}
this.astCache[ key ] = this.wikiTextToAst( wikiText );
}
/*
* Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
*
* CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
* n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
/*
* Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
*
* CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
* n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
// =========================================================
// parsing combinators - could be a library on its own
// =========================================================
// =========================================================
// parsing combinators - could be a library on its own
// =========================================================
// try several ps in a row, all must succeed or return null
// this is the only eager one
function sequence( ps ) {
var originalPos = pos;
var result = [];
// try several ps in a row, all must succeed or return null
// this is the only eager one
function sequence( ps ) {
var originalPos = pos;
var result = [];
// run the same parser over and over until it fails.
// must succeed a minimum of n times or return null
function nOrMore( n, p ) {
// run the same parser over and over until it fails.
// must succeed a minimum of n times or return null
function nOrMore( n, p ) {
// There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
// But using this as a combinator seems to cause problems when combined with nOrMore().
// May be some scoping issue
function transform( p, fn ) {
// There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
// But using this as a combinator seems to cause problems when combined with nOrMore().
// May be some scoping issue
function transform( p, fn ) {
// Parsing functions follow. All parsing functions work like this:
// They don't accept any arguments.
// Instead, they just operate non destructively on the string 'input'
// As they can consume parts of the string, they advance the shared variable pos,
// and return tokens (or whatever else they want to return).
// Parsing functions follow. All parsing functions work like this:
// They don't accept any arguments.
// Instead, they just operate non destructively on the string 'input'
// As they can consume parts of the string, they advance the shared variable pos,
// and return tokens (or whatever else they want to return).
// some things are defined as closures and other things as ordinary functions
// converting everything to a closure makes it a lot harder to debug... errors pop up
// but some debuggers can't tell you exactly where they come from. Also the mutually
// recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
// This may be because, to save code, memoization was removed
// some things are defined as closures and other things as ordinary functions
// converting everything to a closure makes it a lot harder to debug... errors pop up
// but some debuggers can't tell you exactly where they come from. Also the mutually
// recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
// This may be because, to save code, memoization was removed
-
-
- var regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ );
- var regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/);
- var regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/);
-
+ var regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
+ var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
+ var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
// Used to define "literals" without spaces, in space-delimited situations
function literalWithoutSpace() {
var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
return result === null ? null : result.join('');
}
// Used to define "literals" without spaces, in space-delimited situations
function literalWithoutSpace() {
var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
return result === null ? null : result.join('');
}
// it is not a literal in the parameter
function literalWithoutBar() {
var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
return result === null ? null : result.join('');
}
// it is not a literal in the parameter
function literalWithoutBar() {
var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
return result === null ? null : result.join('');
}
function literal() {
var result = nOrMore( 1, escapedOrRegularLiteral )();
return result === null ? null : result.join('');
}
function literal() {
var result = nOrMore( 1, escapedOrRegularLiteral )();
return result === null ? null : result.join('');
}
// this is the same as the above extlink, except that the url is being passed on as a parameter
function extLinkParam() {
var result = sequence( [
// this is the same as the above extlink, except that the url is being passed on as a parameter
function extLinkParam() {
var result = sequence( [
}
return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
}
}
return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
}
- makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+-]+/ ),
- function( result ) { return result.toString(); }
+ makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
+ function ( result ) { return result.toString(); }
// use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
}
// use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
}
var res = sequence( [
// templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
// or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
var res = sequence( [
// templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
// or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
// everything above this point is supposed to be stateless/static, but
// I am deferring the work of turning it into prototypes & objects. It's quite fast enough
// everything above this point is supposed to be stateless/static, but
// I am deferring the work of turning it into prototypes & objects. It's quite fast enough
* and returned a non-null.
* n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
*/
* and returned a non-null.
* n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
*/
- var _this = this;
-
- $.each( magic, function( key, val ) {
- _this[ key.toLowerCase() ] = function() { return val; };
+ var jmsg = this;
+ $.each( magic, function ( key, val ) {
+ jmsg[ key.toLowerCase() ] = function () {
+ return val;
+ };
/**
* (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
* Walk entire node structure, applying replacements and template functions when appropriate
/**
* (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
* Walk entire node structure, applying replacements and template functions when appropriate
* @param {Array} replacements for $1, $2, ... $n
* @return {Mixed} single-string node or array of nodes suitable for jQuery appending
*/
* @param {Array} replacements for $1, $2, ... $n
* @return {Mixed} single-string node or array of nodes suitable for jQuery appending
*/
- var subnodes = $.map( node.slice( 1 ), function( n ) {
- return _this.emit( n, replacements );
+ var subnodes = $.map( node.slice( 1 ), function ( n ) {
+ return jmsg.emit( n, replacements );
- if ( typeof _this[operation] === 'function' ) {
- ret = _this[ operation ]( subnodes, replacements );
+ if ( typeof jmsg[operation] === 'function' ) {
+ ret = jmsg[ operation ]( subnodes, replacements );
// If you have 'magic words' then configure the parser to have them upon creation.
//
// An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
// Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
mw.jqueryMsg.htmlEmitter.prototype = {
// If you have 'magic words' then configure the parser to have them upon creation.
//
// An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
// Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
mw.jqueryMsg.htmlEmitter.prototype = {
/**
* Parsing has been applied depth-first we can assume that all nodes here are single nodes
* Must return a single node to parents -- a jQuery with synthetic span
/**
* Parsing has been applied depth-first we can assume that all nodes here are single nodes
* Must return a single node to parents -- a jQuery with synthetic span
- concat: function( nodes ) {
- var span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
- $.each( nodes, function( i, node ) {
+ concat: function ( nodes ) {
+ var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
+ $.each( nodes, function ( i, node ) {
- $.each( node.contents(), function( j, childNode ) {
- span.append( childNode );
+ $.each( node.contents(), function ( j, childNode ) {
+ $span.append( childNode );
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
if ( index < replacements.length ) {
if ( typeof arg === 'string' ) {
// replacement is a string, escape it
if ( index < replacements.length ) {
if ( typeof arg === 'string' ) {
// replacement is a string, escape it
* If the href is a jQuery object, treat it as "enclosing" the link text.
* ... function, treat it as the click handler
* ... string, treat it as a URI
* If the href is a jQuery object, treat it as "enclosing" the link text.
* ... function, treat it as the click handler
* ... string, treat it as a URI
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
var replacement,
index = parseInt( nodes[0], 10 );
if ( index < replacements.length) {
var replacement,
index = parseInt( nodes[0], 10 );
if ( index < replacements.length) {
* Transform parsed structure into pluralization
* n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
* So convert it back with the current language's convertNumber.
* Transform parsed structure into pluralization
* n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
* So convert it back with the current language's convertNumber.
- * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ]
+ * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ]
var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 );
var forms = nodes.slice(1);
return forms.length ? this.language.convertPlural( count, forms ) : '';
var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 );
var forms = nodes.slice(1);
return forms.length ? this.language.convertPlural( count, forms ) : '';
/**
* Transform parsed structure into gender
* Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}.
/**
* Transform parsed structure into gender
* Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}.
- * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ]
+ * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ]
* @param {Array} of nodes [{Grammar case eg: genitive}, {String word}]
* @return {String} selected grammatical form according to current language
*/
* @param {Array} of nodes [{Grammar case eg: genitive}, {String word}]
* @return {String} selected grammatical form according to current language
*/
-
- // deprecated! don't rely on gM existing.
- // the window.gM ought not to be required - or if required, not required here. But moving it to extensions breaks it (?!)
+ // Deprecated! don't rely on gM existing.
+ // The window.gM ought not to be required - or if required, not required here.
+ // But moving it to extensions breaks it (?!)
// TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
// Caching is somewhat problematic, because we do need different message functions for different maps, so
// we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
// TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
// Caching is somewhat problematic, because we do need different message functions for different maps, so
// we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
// Do not use mw.jqueryMsg unless required
if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
// Fall back to mw.msg's simple parser
return oldParser.apply( this );
}
// Do not use mw.jqueryMsg unless required
if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
// Fall back to mw.msg's simple parser
return oldParser.apply( this );
}
var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
return messageFunction( this.key, this.parameters );
};
var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
return messageFunction( this.key, this.parameters );
};