diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
commit | d9022f63880ce039446fba8364f68e656b7bf4cb (patch) | |
tree | 16b40fbf17bf7c9ee6f4ead25b16dd192378050a /resources/mediawiki/mediawiki.jqueryMsg.js | |
parent | 27cf83d177256813e2e802241085fce5dd0f3fb9 (diff) |
Update to MediaWiki 1.19.0
Diffstat (limited to 'resources/mediawiki/mediawiki.jqueryMsg.js')
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js new file mode 100644 index 00000000..6c00bd15 --- /dev/null +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -0,0 +1,685 @@ +/** + * 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 = {}; + + /** + * 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} + */ + function getFailableParserFn( options ) { + var parser = new mw.jqueryMsg.parser( options ); + /** + * 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} + */ + return function( args ) { + var key = args[0]; + var argsArray = $.isArray( args[1] ) ? args[1] : $.makeArray( args ).slice( 1 ); + var escapedArgsArray = $.map( argsArray, function( arg ) { + return typeof arg === 'string' ? mw.html.escape( arg ) : arg; + } ); + try { + return parser.parse( key, escapedArgsArray ); + } catch ( e ) { + return $( '<span></span>' ).append( key + ': ' + e.message ); + } + }; + } + + /** + * Class method. + * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements). + * e.g. + * window.gM = mediaWiki.parser.getMessageFunction( options ); + * $( 'p#headline' ).html( gM( 'hello-user', username ) ); + * + * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the + * jQuery plugin version instead. This is only included for backwards compatibility with gM(). + * + * @param {Array} parser options + * @return {Function} function suitable for assigning to window.gM + */ + mw.jqueryMsg.getMessageFunction = function( options ) { + var failableParserFn = getFailableParserFn( options ); + /** + * N.B. replacements are variadic arguments or an array in second parameter. In other words: + * 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 + */ + return function( /* key, replacements */ ) { + return failableParserFn( arguments ).html(); + }; + }; + + /** + * 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. + * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options ); + * var userlink = $( '<a>' ).click( function() { alert( "hello!!") } ); + * $( 'p#headline' ).msg( 'hello-user', userlink ); + * + * @param {Array} parser options + * @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg + */ + mw.jqueryMsg.getPlugin = function( options ) { + var failableParserFn = getFailableParserFn( options ); + /** + * N.B. replacements are variadic arguments or an array in second parameter. In other words: + * somefunction(a, b, c, d) + * is equivalent to + * 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 + */ + return function( /* key, replacements */ ) { + var $target = this.empty(); + $.each( failableParserFn( arguments ).contents(), function( i, node ) { + $target.append( node ); + } ); + return $target; + }; + }; + + var parserDefaults = { + 'magic' : {}, + 'messages' : mw.messages, + 'language' : mw.language + }; + + /** + * The parser itself. + * Describes an object, whose primary duty is to .parse() message keys. + * @param {Array} options + */ + mw.jqueryMsg.parser = function( options ) { + 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: {}, + + /** + * Where the magic happens. + * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery + * If an error is thrown, returns original key, and logs the error + * @param {String} message key + * @param {Array} replacements for $1, $2... $n + * @return {jQuery} + */ + parse: function( key, replacements ) { + return this.emitter.emit( this.getAst( key ), replacements ); + }, + + /** + * Fetch the message string associated with a key, return parsed structure. Memoized. + * Note that we pass '[' + key + ']' back for a missing message here. + * @param {String} key + * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing + */ + getAst: function( key ) { + if ( this.astCache[ key ] === undefined ) { + var wikiText = this.settings.messages.get( key ); + if ( typeof wikiText !== 'string' ) { + wikiText = "\\[" + key + "\\]"; + } + this.astCache[ key ] = this.wikiTextToAst( wikiText ); + } + return this.astCache[ key ]; + }, + + /* + * 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. + * + * @param {String} message string wikitext + * @throws Error + * @return {Mixed} abstract syntax tree + */ + wikiTextToAst: function( input ) { + + // Indicates current position in input as we parse through it. + // Shared among all parsing functions below. + var pos = 0; + + // ========================================================= + // parsing combinators - could be a library on its own + // ========================================================= + + + // Try parsers until one works, if none work return null + function choice( ps ) { + return function() { + for ( var i = 0; i < ps.length; i++ ) { + var result = ps[i](); + if ( result !== null ) { + return result; + } + } + return null; + }; + } + + // 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 = []; + for ( var i = 0; i < ps.length; i++ ) { + var res = ps[i](); + if ( res === null ) { + pos = originalPos; + return null; + } + result.push( res ); + } + return 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 ) { + return function() { + var originalPos = pos; + var result = []; + var parsed = p(); + while ( parsed !== null ) { + result.push( parsed ); + parsed = p(); + } + if ( result.length < n ) { + pos = originalPos; + return null; + } + return result; + }; + } + + // 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 ) { + return function() { + var result = p(); + return result === null ? null : fn( result ); + }; + } + + // Helpers -- just make ps out of simpler JS builtin types + + function makeStringParser( s ) { + var len = s.length; + return function() { + var result = null; + if ( input.substr( pos, len ) === s ) { + result = s; + pos += len; + } + return result; + }; + } + + function makeRegexParser( regex ) { + return function() { + var matches = input.substr( pos ).match( regex ); + if ( matches === null ) { + return null; + } + pos += matches[0].length; + return matches[0]; + }; + } + + + /** + * =================================================================== + * General patterns above this line -- wikitext specific parsers below + * =================================================================== + */ + + // 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 + + + var regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ ); + var regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/); + var regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/); + + var backslash = makeStringParser( "\\" ); + var anyCharacter = makeRegexParser( /^./ ); + + function escapedLiteral() { + var result = sequence( [ + backslash, + anyCharacter + ] ); + return result === null ? null : result[1]; + } + + var escapedOrLiteralWithoutSpace = choice( [ + escapedLiteral, + regularLiteralWithoutSpace + ] ); + + var escapedOrLiteralWithoutBar = choice( [ + escapedLiteral, + regularLiteralWithoutBar + ] ); + + var escapedOrRegularLiteral = choice( [ + escapedLiteral, + regularLiteral + ] ); + + // 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" within template parameters. The pipe character is the parameter delimeter, so by default + // 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(''); + } + + var whitespace = makeRegexParser( /^\s+/ ); + var dollar = makeStringParser( '$' ); + var digits = makeRegexParser( /^\d+/ ); + + function replacement() { + var result = sequence( [ + dollar, + digits + ] ); + if ( result === null ) { + return null; + } + return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; + } + + + var openExtlink = makeStringParser( '[' ); + var closeExtlink = makeStringParser( ']' ); + + // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed + function extlink() { + var result = null; + var parsedResult = sequence( [ + openExtlink, + nonWhitespaceExpression, + whitespace, + expression, + closeExtlink + ] ); + if ( parsedResult !== null ) { + result = [ 'LINK', parsedResult[1], parsedResult[3] ]; + } + return result; + } + + var openLink = makeStringParser( '[[' ); + var closeLink = makeStringParser( ']]' ); + + function link() { + var result = null; + var parsedResult = sequence( [ + openLink, + expression, + closeLink + ] ); + if ( parsedResult !== null ) { + result = [ 'WLINK', parsedResult[1] ]; + } + return result; + } + + var templateName = transform( + // see $wgLegalTitleChars + // not allowing : due to the need to catch "PLURAL:$1" + makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+-]+/ ), + function( result ) { return result.toString(); } + ); + + function templateParam() { + var result = sequence( [ + pipe, + nOrMore( 0, paramExpression ) + ] ); + if ( result === null ) { + return null; + } + var expr = result[1]; + // 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 pipe = makeStringParser( '|' ); + + function templateWithReplacement() { + var result = sequence( [ + templateName, + colon, + replacement + ] ); + return result === null ? null : [ result[0], result[2] ]; + } + + var colon = makeStringParser(':'); + + var templateContents = choice( [ + function() { + var res = sequence( [ + templateWithReplacement, + nOrMore( 0, templateParam ) + ] ); + return res === null ? null : res[0].concat( res[1] ); + }, + function() { + var res = sequence( [ + templateName, + nOrMore( 0, templateParam ) + ] ); + if ( res === null ) { + return null; + } + return [ res[0] ].concat( res[1] ); + } + ] ); + + var openTemplate = makeStringParser('{{'); + var closeTemplate = makeStringParser('}}'); + + function template() { + var result = sequence( [ + openTemplate, + templateContents, + closeTemplate + ] ); + return result === null ? null : result[1]; + } + + var nonWhitespaceExpression = choice( [ + template, + link, + extlink, + replacement, + literalWithoutSpace + ] ); + + var paramExpression = choice( [ + template, + link, + extlink, + replacement, + literalWithoutBar + ] ); + + var expression = choice( [ + template, + link, + extlink, + replacement, + literal + ] ); + + function start() { + var result = nOrMore( 0, expression )(); + if ( result === null ) { + return null; + } + return [ "CONCAT" ].concat( result ); + } + + // 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 + + // finally let's do some actual work... + + var result = start(); + + /* + * For success, the p must have gotten to the end of the input + * and returned a non-null. + * n.b. This is part of language infrastructure, so we do not throw an internationalizable message. + */ + if (result === null || pos !== input.length) { + throw new Error( "Parse error at position " + pos.toString() + " in input: " + input ); + } + return result; + } + + }; + + /** + * htmlEmitter - object which primarily exists to emit HTML from parser ASTs + */ + mw.jqueryMsg.htmlEmitter = function( language, magic ) { + this.language = language; + var _this = this; + + $.each( magic, function( key, val ) { + _this[ 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 + * @param {Mixed} abstract syntax tree (top node or subnode) + * @param {Array} replacements for $1, $2, ... $n + * @return {Mixed} single-string node or array of nodes suitable for jQuery appending + */ + this.emit = function( node, replacements ) { + var ret = null; + var _this = this; + switch( typeof node ) { + case 'string': + case 'number': + ret = node; + break; + case 'object': // node is an array of nodes + var subnodes = $.map( node.slice( 1 ), function( n ) { + return _this.emit( n, replacements ); + } ); + var operation = node[0].toLowerCase(); + if ( typeof _this[operation] === 'function' ) { + ret = _this[ operation ]( subnodes, replacements ); + } else { + throw new Error( 'unknown operation "' + operation + '"' ); + } + break; + case 'undefined': + // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined + // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information? + // The logical thing is probably to return the empty string here when we encounter undefined. + ret = ''; + break; + default: + throw new Error( 'unexpected type in AST: ' + typeof node ); + } + return ret; + }; + + }; + + // For everything in input that follows double-open-curly braces, there should be an equivalent parser + // function. For instance {{PLURAL ... }} will be processed by 'plural'. + // 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 + * However, unwrap any other synthetic spans in our children and pass them upwards + * @param {Array} nodes - mixed, some single nodes, some arrays of nodes + * @return {jQuery} + */ + concat: function( nodes ) { + var span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' ); + $.each( nodes, function( i, node ) { + if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) { + $.each( node.contents(), function( j, childNode ) { + span.append( childNode ); + } ); + } else { + // strings, integers, anything else + span.append( node ); + } + } ); + return span; + }, + + /** + * Return replacement of correct index, or string if unavailable. + * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ]. + * if the specified parameter is not found return the same string + * (e.g. "$99" -> parameter 98 -> not found -> return "$99" ) + * TODO throw error if nodes.length > 1 ? + * @param {Array} of one element, integer, n >= 0 + * @return {String} replacement + */ + replace: function( nodes, replacements ) { + var index = parseInt( nodes[0], 10 ); + return index < replacements.length ? replacements[index] : '$' + ( index + 1 ); + }, + + /** + * Transform wiki-link + * TODO unimplemented + */ + wlink: function( nodes ) { + return "unimplemented"; + }, + + /** + * Transform parsed structure into external link + * 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 + * TODO: throw an error if nodes.length > 2 ? + * @param {Array} of two elements, {jQuery|Function|String} and {String} + * @return {jQuery} + */ + link: function( nodes ) { + var arg = nodes[0]; + var contents = nodes[1]; + var $el; + if ( arg instanceof jQuery ) { + $el = arg; + } else { + $el = $( '<a>' ); + if ( typeof arg === 'function' ) { + $el.click( arg ).attr( 'href', '#' ); + } else { + $el.attr( 'href', arg.toString() ); + } + } + $el.append( contents ); + return $el; + }, + + /** + * 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} ... ] + * @return {String} selected pluralized form according to current language + */ + plural: function( nodes ) { + 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}}. + * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] + * @return {String} selected gender form according to current language + */ + gender: function( nodes ) { + var gender; + if ( nodes[0] && nodes[0].options instanceof mw.Map ){ + gender = nodes[0].options.get( 'gender' ); + } else { + gender = nodes[0]; + } + var forms = nodes.slice(1); + return this.language.gender( gender, forms ); + } + + }; + + // TODO figure out a way to make magic work with common globals like wgSiteName, without requiring init from library users... + // var options = { magic: { 'SITENAME' : mw.config.get( 'wgSiteName' ) } }; + + // 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 (?!) + // Need to fix plugin so it could do attributes as well, then will be okay to remove this. + window.gM = mw.jqueryMsg.getMessageFunction(); + + $.fn.msg = mw.jqueryMsg.getPlugin(); + + // Replace the default message parser with jqueryMsg + var oldParser = mw.Message.prototype.parser; + mw.Message.prototype.parser = function() { + // 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 ); + } + + var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); + return messageFunction( this.key, this.parameters ); + }; + +} )( mediaWiki, jQuery ); |