diff options
Diffstat (limited to 'resources/mediawiki/mediawiki.jqueryMsg.js')
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 423 |
1 files changed, 303 insertions, 120 deletions
diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 86af31ff..183b525e 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -5,13 +5,26 @@ * @author neilk@wikimedia.org */ ( function ( mw, $ ) { - var slice = Array.prototype.slice, + var oldParser, + slice = Array.prototype.slice, parserDefaults = { magic : { 'SITENAME' : mw.config.get( 'wgSiteName' ) }, messages : mw.messages, - language : mw.language + language : mw.language, + + // Same meaning as in mediawiki.js. + // + // Only 'text', 'parse', and 'escaped' are supported, and the + // actual escaping for 'escaped' is done by other code (generally + // through jqueryMsg). + // + // However, note that this default only + // applies to direct calls to jqueryMsg. The default for mediawiki.js itself + // is 'text', including when it uses jqueryMsg. + format: 'parse' + }; /** @@ -30,8 +43,8 @@ * @return {jQuery} */ return function ( args ) { - var key = args[0]; - var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); + var key = args[0], + argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); try { return parser.parse( key, argsArray ); } catch ( e ) { @@ -56,19 +69,32 @@ * @return {Function} function suitable for assigning to window.gM */ mw.jqueryMsg.getMessageFunction = function ( options ) { - var failableParserFn = getFailableParserFn( options ); + var failableParserFn = getFailableParserFn( options ), + format; + + if ( options && options.format !== undefined ) { + format = options.format; + } else { + format = parserDefaults.format; + } + /** * 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 + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). + * @return {string} Rendered HTML. */ - return function ( /* key, replacements */ ) { - return failableParserFn( arguments ).html(); + return function () { + var failableResult = failableParserFn( arguments ); + if ( format === 'text' || format === 'escaped' ) { + return failableResult.text(); + } else { + return failableResult.html(); + } }; }; @@ -93,12 +119,14 @@ * 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) + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). * @return {jQuery} this */ - return function ( /* key, replacements */ ) { + return function () { var $target = this.empty(); + // TODO: Simply $target.append( failableParserFn( arguments ).contents() ) + // or Simply $target.append( failableParserFn( arguments ) ) $.each( failableParserFn( arguments ).contents(), function ( i, node ) { $target.append( node ); } ); @@ -113,20 +141,36 @@ */ mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); + this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' ); + 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). + /** + * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, 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). + * + * The two parts of the key are separated by colon. For example: + * + * "message-key:true": ast + * + * if they key is "message-key" and onlyCurlyBraceTransform is true. + * + * This cache is shared by all instances of mw.jqueryMsg.parser. + * + * @static + */ 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 + * @param {String} key Message key. + * @param {Array} replacements Variable replacements for $1, $2... $n * @return {jQuery} */ parse: function ( key, replacements ) { @@ -139,16 +183,19 @@ * @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 ); + var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText; + + if ( this.astCache[ cacheKey ] === undefined ) { + wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { - wikiText = "\\[" + key + "\\]"; + wikiText = '\\[' + key + '\\]'; } - this.astCache[ key ] = this.wikiTextToAst( wikiText ); + this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ cacheKey ]; }, - /* + + /** * 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. @@ -159,18 +206,27 @@ * @return {Mixed} abstract syntax tree */ wikiTextToAst: function ( input ) { + var pos, + regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, + backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, + whitespace, dollar, digits, + openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon, + templateContents, openTemplate, closeTemplate, + nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result; // Indicates current position in input as we parse through it. // Shared among all parsing functions below. - var pos = 0; + 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](); + var i, result; + for ( i = 0; i < ps.length; i++ ) { + result = ps[i](); if ( result !== null ) { return result; } @@ -181,10 +237,11 @@ // 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](); + var i, res, + originalPos = pos, + result = []; + for ( i = 0; i < ps.length; i++ ) { + res = ps[i](); if ( res === null ) { pos = originalPos; return null; @@ -197,9 +254,9 @@ // must succeed a minimum of n times or return null function nOrMore( n, p ) { return function () { - var originalPos = pos; - var result = []; - var parsed = p(); + var originalPos = pos, + result = [], + parsed = p(); while ( parsed !== null ) { result.push( parsed ); parsed = p(); @@ -258,11 +315,12 @@ // 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( /^./ ); + regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); + regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); + regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); + backslash = makeStringParser( '\\' ); + anyCharacter = makeRegexParser( /^./ ); function escapedLiteral() { var result = sequence( [ backslash, @@ -270,36 +328,50 @@ ] ); return result === null ? null : result[1]; } - var escapedOrLiteralWithoutSpace = choice( [ + escapedOrLiteralWithoutSpace = choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); - var escapedOrLiteralWithoutBar = choice( [ + escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); - var escapedOrRegularLiteral = choice( [ + 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(''); + 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(''); + var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); + return result === null ? null : result.join(''); } + + // Used for wikilink page names. Like literalWithoutBar, but + // without allowing escapes. + function unescapedLiteralWithoutBar() { + var result = nOrMore( 1, regularLiteralWithoutBar )(); + return result === null ? null : result.join(''); + } + function literal() { - var result = nOrMore( 1, escapedOrRegularLiteral )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrRegularLiteral )(); + return result === null ? null : result.join(''); } - var whitespace = makeRegexParser( /^\s+/ ); - var dollar = makeStringParser( '$' ); - var digits = makeRegexParser( /^\d+/ ); + + function curlyBraceTransformExpressionLiteral() { + var result = nOrMore( 1, regularLiteralWithSquareBrackets )(); + return result === null ? null : result.join(''); + } + + whitespace = makeRegexParser( /^\s+/ ); + dollar = makeStringParser( '$' ); + digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ @@ -311,12 +383,13 @@ } return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; } - var openExtlink = makeStringParser( '[' ); - var closeExtlink = makeStringParser( ']' ); + openExtlink = makeStringParser( '[' ); + 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( [ + var result, parsedResult; + result = null; + parsedResult = sequence( [ openExtlink, nonWhitespaceExpression, whitespace, @@ -343,39 +416,73 @@ } return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; } - var openLink = makeStringParser( '[[' ); - var closeLink = makeStringParser( ']]' ); + openLink = makeStringParser( '[[' ); + closeLink = makeStringParser( ']]' ); + pipe = makeStringParser( '|' ); + + function template() { + var result = sequence( [ + openTemplate, + templateContents, + closeTemplate + ] ); + return result === null ? null : result[1]; + } + + wikilinkPage = choice( [ + unescapedLiteralWithoutBar, + template + ] ); + + function pipedWikilink() { + var result = sequence( [ + wikilinkPage, + pipe, + expression + ] ); + return result === null ? null : [ result[0], result[2] ]; + } + + wikilinkContents = choice( [ + pipedWikilink, + wikilinkPage // unpiped link + ] ); + function link() { - var result = null; - var parsedResult = sequence( [ + var result, parsedResult, parsedLinkContents; + result = null; + + parsedResult = sequence( [ openLink, - expression, + wikilinkContents, closeLink ] ); if ( parsedResult !== null ) { - result = [ 'WLINK', parsedResult[1] ]; + parsedLinkContents = parsedResult[1]; + result = [ 'WLINK' ].concat( parsedLinkContents ); } return result; } - var templateName = transform( + 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( [ + var expr, result; + 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]; + 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, @@ -392,8 +499,8 @@ ] ); return result === null ? null : [ result[0], result[2] ]; } - var colon = makeStringParser(':'); - var templateContents = choice( [ + colon = makeStringParser(':'); + templateContents = choice( [ function () { var res = sequence( [ // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} @@ -414,17 +521,9 @@ 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( [ + openTemplate = makeStringParser('{{'); + closeTemplate = makeStringParser('}}'); + nonWhitespaceExpression = choice( [ template, link, extLinkParam, @@ -432,7 +531,7 @@ replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ + paramExpression = choice( [ template, link, extLinkParam, @@ -440,7 +539,8 @@ replacement, literalWithoutBar ] ); - var expression = choice( [ + + expression = choice( [ template, link, extLinkParam, @@ -448,25 +548,42 @@ replacement, literal ] ); - function start() { - var result = nOrMore( 0, expression )(); + + // Used when only {{-transformation is wanted, for 'text' + // or 'escaped' formats + curlyBraceTransformExpression = choice( [ + template, + replacement, + curlyBraceTransformExpressionLiteral + ] ); + + + /** + * Starts the parse + * + * @param {Function} rootExpression root parse function + */ + function start( rootExpression ) { + var result = nOrMore( 0, rootExpression )(); if ( result === null ) { return null; } - return [ "CONCAT" ].concat( result ); + 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(); + + // If you add another possible rootExpression, you must update the astCache key scheme. + result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression ); /* * 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 ); + if ( result === null || pos !== input.length ) { + throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input ); } return result; } @@ -491,18 +608,20 @@ * @return {Mixed} single-string node or array of nodes suitable for jQuery appending */ this.emit = function ( node, replacements ) { - var ret = null; - var jmsg = this; + var ret, subnodes, operation, + jmsg = 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 ) { + // typeof returns object for arrays + case 'object': + // node is an array of nodes + subnodes = $.map( node.slice( 1 ), function ( n ) { return jmsg.emit( n, replacements ); } ); - var operation = node[0].toLowerCase(); + operation = node[0].toLowerCase(); if ( typeof jmsg[operation] === 'function' ) { ret = jmsg[ operation ]( subnodes, replacements ); } else { @@ -543,8 +662,9 @@ $span.append( childNode ); } ); } else { - // strings, integers, anything else - $span.append( node ); + // Let jQuery append nodes, arrays of nodes and jQuery objects + // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings) + $span.append( $.type( node ) === 'object' ? node : document.createTextNode( node ) ); } } ); return $span; @@ -555,7 +675,7 @@ * 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 ? + * TODO: Throw error if nodes.length > 1 ? * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ @@ -563,13 +683,7 @@ var index = parseInt( nodes[0], 10 ); if ( index < replacements.length ) { - if ( typeof arg === 'string' ) { - // replacement is a string, escape it - return mw.html.escape( replacements[index] ); - } else { - // replacement is no string, don't touch! - return replacements[index]; - } + return replacements[index]; } else { // index not found, fallback to displaying variable return '$' + ( index + 1 ); @@ -578,10 +692,41 @@ /** * Transform wiki-link - * TODO unimplemented + * + * TODO: + * It only handles basic cases, either no pipe, or a pipe with an explicit + * anchor. + * + * It does not attempt to handle features like the pipe trick. + * However, the pipe trick should usually not be present in wikitext retrieved + * from the server, since the replacement is done at save time. + * It may, though, if the wikitext appears in extension-controlled content. + * + * @param nodes */ wlink: function ( nodes ) { - return 'unimplemented'; + var page, anchor, url; + + page = nodes[0]; + url = mw.util.wikiGetlink( page ); + + // [[Some Page]] or [[Namespace:Some Page]] + if ( nodes.length === 1 ) { + anchor = page; + } + + /* + * [[Some Page|anchor text]] or + * [[Namespace:Some Page|anchor] + */ + else { + anchor = nodes[1]; + } + + return $( '<a />' ).attr( { + title: page, + href: url + } ).text( anchor ); }, /** @@ -594,9 +739,9 @@ * @return {jQuery} */ link: function ( nodes ) { - var arg = nodes[0]; - var contents = nodes[1]; - var $el; + var $el, + arg = nodes[0], + contents = nodes[1]; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -639,25 +784,32 @@ * @return {String} selected pluralized form according to current language */ plural: function ( nodes ) { - var count = parseFloat( this.language.convertNumber( nodes[0], true ) ); - var forms = nodes.slice(1); + var forms, count; + count = parseFloat( this.language.convertNumber( nodes[0], true ) ); + 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} ] + * Transform parsed structure according to gender. + * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}. + * The first node is either a string, which can be "male" or "female", + * or a User object (not a username). + * + * @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 ){ + var gender, forms; + + if ( nodes[0] && nodes[0].options instanceof mw.Map ) { gender = nodes[0].options.get( 'gender' ); } else { gender = nodes[0]; } - var forms = nodes.slice(1); + + forms = nodes.slice( 1 ); + return this.language.gender( gender, forms ); }, @@ -668,9 +820,33 @@ * @return {String} selected grammatical form according to current language */ grammar: function ( nodes ) { - var form = nodes[0]; - var word = nodes[1]; + var form = nodes[0], + word = nodes[1]; return word && form && this.language.convertGrammar( word, form ); + }, + + /** + * Tranform parsed structure into a int: (interface language) message include + * Invoked by putting {{int:othermessage}} into a message + * @param {Array} of nodes + * @return {string} Other message + */ + int: function ( nodes ) { + return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() ); + }, + + /** + * Takes an unformatted number (arab, no group separators and . as decimal separator) + * and outputs it in the localized digit script and formatted with decimal + * separator, according to the current language + * @param {Array} of nodes + * @return {Number|String} formatted number + */ + formatnum: function ( nodes ) { + var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false, + number = nodes[0]; + + return this.language.convertNumber( number, isInteger ); } }; // Deprecated! don't rely on gM existing. @@ -681,17 +857,24 @@ $.fn.msg = mw.jqueryMsg.getPlugin(); // Replace the default message parser with jqueryMsg - var oldParser = mw.Message.prototype.parser; + oldParser = mw.Message.prototype.parser; mw.Message.prototype.parser = function () { + var messageFunction; + // 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 ) { + if ( this.format === 'plain' || !/\{\{|\[/.test(this.map.get( this.key ) ) ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); + + messageFunction = mw.jqueryMsg.getMessageFunction( { + 'messages': this.map, + // For format 'escaped', escaping part is handled by mediawiki.js + 'format': this.format + } ); return messageFunction( this.key, this.parameters ); }; |