diff options
Diffstat (limited to 'resources/mediawiki/mediawiki.jqueryMsg.js')
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 1148 |
1 files changed, 0 insertions, 1148 deletions
diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js deleted file mode 100644 index 70b9be93..00000000 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ /dev/null @@ -1,1148 +0,0 @@ -/** -* Experimental advanced wikitext parser-emitter. -* See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs -* -* @author neilk@wikimedia.org -* @author mflaschen@wikimedia.org -*/ -( function ( mw, $ ) { - var oldParser, - slice = Array.prototype.slice, - parserDefaults = { - magic : { - 'SITENAME' : mw.config.get( 'wgSiteName' ) - }, - // This is a whitelist based on, but simpler than, Sanitizer.php. - // Self-closing tags are not currently supported. - allowedHtmlElements : [ - 'b', - 'i' - ], - // Key tag name, value allowed attributes for that tag. - // See Sanitizer::setupAttributeWhitelist - allowedHtmlCommonAttributes : [ - // HTML - 'id', - 'class', - 'style', - 'lang', - 'dir', - 'title', - - // WAI-ARIA - 'role' - ], - - // Attributes allowed for specific elements. - // Key is element name in lower case - // Value is array of allowed attributes for that element - allowedHtmlAttributesByElement : {}, - messages : mw.messages, - 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 mediawiki.js). - // - // 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' - - }; - - /** - * Wrapper around jQuery append that converts all non-objects to TextNode so append will not - * convert what it detects as an htmlString to an element. - * - * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is. - * - * @param {jQuery} $parent Parent node wrapped by jQuery - * @param {Object|string|Array} children What to append, with the same possible types as jQuery - * @return {jQuery} $parent - */ - function appendWithoutParsing( $parent, children ) { - var i, len; - - if ( !$.isArray( children ) ) { - children = [children]; - } - - for ( i = 0, len = children.length; i < len; i++ ) { - if ( typeof children[i] !== 'object' ) { - children[i] = document.createTextNode( children[i] ); - } - } - - return $parent.append( children ); - } - - /** - * Decodes the main HTML entities, those encoded by mw.html.escape. - * - * @param {string} encode Encoded string - * @return {string} String with those entities decoded - */ - function decodePrimaryHtmlEntities( encoded ) { - return encoded - .replace( /'/g, '\'' ) - .replace( /"/g, '"' ) - .replace( /</g, '<' ) - .replace( />/g, '>' ) - .replace( /&/g, '&' ); - } - - /** - * 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], - argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); - try { - return parser.parse( key, argsArray ); - } catch ( e ) { - return $( '<span>' ).text( key + ': ' + e.message ); - } - }; - } - - mw.jqueryMsg = {}; - - /** - * 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 ), - 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} key Message key. - * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). - * @return {string} Rendered HTML. - */ - return function () { - var failableResult = failableParserFn( arguments ); - if ( format === 'text' || format === 'escaped' ) { - return failableResult.text(); - } else { - return failableResult.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} key Message key. - * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). - * @return {jQuery} this - */ - return function () { - var $target = this.empty(); - // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() ) - // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) ) - $.each( failableParserFn( arguments ).contents(), function ( i, node ) { - appendWithoutParsing( $target, node ); - } ); - return $target; - }; - }; - - /** - * 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.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 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} key Message key. - * @param {Array} replacements Variable 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 ) { - 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 + '\\]'; - } - this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText ); - } - 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. - * 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 ) { - var pos, settings = this.settings, concat = Array.prototype.concat, - regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, - doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral, - escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, - whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue, - htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag, - openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, 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. - 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 () { - var i, result; - for ( i = 0; i < ps.length; i++ ) { - 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 i, res, - originalPos = pos, - result = []; - for ( i = 0; i < ps.length; i++ ) { - 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, - result = [], - 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; - }; - } - - /** - * Makes a regex parser, given a RegExp object. - * The regex being passed in should start with a ^ to anchor it to the start - * of the string. - * - * @param {RegExp} regex anchored regex - * @return {Function} function to parse input based on the regex - */ - 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 - - regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ ); - regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); - regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); - regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); - - backslash = makeStringParser( '\\' ); - doubleQuote = makeStringParser( '"' ); - singleQuote = makeStringParser( '\'' ); - anyCharacter = makeRegexParser( /^./ ); - - openHtmlStartTag = makeStringParser( '<' ); - optionalForwardSlash = makeRegexParser( /^\/?/ ); - openHtmlEndTag = makeStringParser( '</' ); - htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ ); - closeHtmlTag = makeRegexParser( /^\s*>/ ); - - function escapedLiteral() { - var result = sequence( [ - backslash, - anyCharacter - ] ); - return result === null ? null : result[1]; - } - escapedOrLiteralWithoutSpace = choice( [ - escapedLiteral, - regularLiteralWithoutSpace - ] ); - escapedOrLiteralWithoutBar = choice( [ - escapedLiteral, - regularLiteralWithoutBar - ] ); - 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(''); - } - - // 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(''); - } - - function curlyBraceTransformExpressionLiteral() { - var result = nOrMore( 1, regularLiteralWithSquareBrackets )(); - return result === null ? null : result.join(''); - } - - asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ ); - htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ ); - htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ ); - - whitespace = makeRegexParser( /^\s+/ ); - dollar = makeStringParser( '$' ); - digits = makeRegexParser( /^\d+/ ); - - function replacement() { - var result = sequence( [ - dollar, - digits - ] ); - if ( result === null ) { - return null; - } - return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; - } - openExtlink = makeStringParser( '[' ); - closeExtlink = makeStringParser( ']' ); - // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed - function extlink() { - var result, parsedResult; - result = null; - parsedResult = sequence( [ - openExtlink, - nonWhitespaceExpression, - whitespace, - nOrMore( 1, expression ), - closeExtlink - ] ); - if ( parsedResult !== null ) { - result = [ 'EXTLINK', parsedResult[1] ]; - // TODO (mattflaschen, 2013-03-22): Clean this up if possible. - // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span. - if ( parsedResult[3].length === 1 ) { - result.push( parsedResult[3][0] ); - } else { - result.push( ['CONCAT'].concat( parsedResult[3] ) ); - } - } - return result; - } - // this is the same as the above extlink, except that the url is being passed on as a parameter - function extLinkParam() { - var result = sequence( [ - openExtlink, - dollar, - digits, - whitespace, - expression, - closeExtlink - ] ); - if ( result === null ) { - return null; - } - return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; - } - openWikilink = makeStringParser( '[[' ); - closeWikilink = 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 wikilink() { - var result, parsedResult, parsedLinkContents; - result = null; - - parsedResult = sequence( [ - openWikilink, - wikilinkContents, - closeWikilink - ] ); - if ( parsedResult !== null ) { - parsedLinkContents = parsedResult[1]; - result = [ 'WIKILINK' ].concat( parsedLinkContents ); - } - return result; - } - - // TODO: Support data- if appropriate - function doubleQuotedHtmlAttributeValue() { - var parsedResult = sequence( [ - doubleQuote, - htmlDoubleQuoteAttributeValue, - doubleQuote - ] ); - return parsedResult === null ? null : parsedResult[1]; - } - - function singleQuotedHtmlAttributeValue() { - var parsedResult = sequence( [ - singleQuote, - htmlSingleQuoteAttributeValue, - singleQuote - ] ); - return parsedResult === null ? null : parsedResult[1]; - } - - function htmlAttribute() { - var parsedResult = sequence( [ - whitespace, - asciiAlphabetLiteral, - htmlAttributeEquals, - choice( [ - doubleQuotedHtmlAttributeValue, - singleQuotedHtmlAttributeValue - ] ) - ] ); - return parsedResult === null ? null : [parsedResult[1], parsedResult[3]]; - } - - /** - * Checks if HTML is allowed - * - * @param {string} startTagName HTML start tag name - * @param {string} endTagName HTML start tag name - * @param {Object} attributes array of consecutive key value pairs, - * with index 2 * n being a name and 2 * n + 1 the associated value - * @return {boolean} true if this is HTML is allowed, false otherwise - */ - function isAllowedHtml( startTagName, endTagName, attributes ) { - var i, len, attributeName; - - startTagName = startTagName.toLowerCase(); - endTagName = endTagName.toLowerCase(); - if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) { - return false; - } - - for ( i = 0, len = attributes.length; i < len; i += 2 ) { - attributeName = attributes[i]; - if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 && - $.inArray( attributeName, settings.allowedHtmlAttributesByElement[startTagName] || [] ) === -1 ) { - return false; - } - } - - return true; - } - - function htmlAttributes() { - var parsedResult = nOrMore( 0, htmlAttribute )(); - // Un-nest attributes array due to structure of jQueryMsg operations (see emit). - return concat.apply( ['HTMLATTRIBUTES'], parsedResult ); - } - - // Subset of allowed HTML markup. - // Most elements and many attributes allowed on the server are not supported yet. - function html() { - var result = null, parsedOpenTagResult, parsedHtmlContents, - parsedCloseTagResult, wrappedAttributes, attributes, - startTagName, endTagName, startOpenTagPos, startCloseTagPos, - endOpenTagPos, endCloseTagPos; - - // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match. - // 1. open through closeHtmlTag - // 2. expression - // 3. openHtmlEnd through close - // This will allow recording the positions to reconstruct if HTML is to be treated as text. - - startOpenTagPos = pos; - parsedOpenTagResult = sequence( [ - openHtmlStartTag, - asciiAlphabetLiteral, - htmlAttributes, - optionalForwardSlash, - closeHtmlTag - ] ); - - if ( parsedOpenTagResult === null ) { - return null; - } - - endOpenTagPos = pos; - startTagName = parsedOpenTagResult[1]; - - parsedHtmlContents = nOrMore( 0, expression )(); - - startCloseTagPos = pos; - parsedCloseTagResult = sequence( [ - openHtmlEndTag, - asciiAlphabetLiteral, - closeHtmlTag - ] ); - - if ( parsedCloseTagResult === null ) { - // Closing tag failed. Return the start tag and contents. - return [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents ); - } - - endCloseTagPos = pos; - endTagName = parsedCloseTagResult[1]; - wrappedAttributes = parsedOpenTagResult[2]; - attributes = wrappedAttributes.slice( 1 ); - if ( isAllowedHtml( startTagName, endTagName, attributes) ) { - result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ].concat( parsedHtmlContents ); - } else { - // HTML is not allowed, so contents will remain how - // it was, while HTML markup at this level will be - // treated as text - // E.g. assuming script tags are not allowed: - // - // <script>[[Foo|bar]]</script> - // - // results in '<script>' and '</script>' - // (not treated as an HTML tag), surrounding a fully - // parsed HTML link. - // - // Concatenate everything from the tag, flattening the contents. - result = [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents, input.substring( startCloseTagPos, endCloseTagPos ) ); - } - - return result; - } - - 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 expr, result; - result = sequence( [ - pipe, - nOrMore( 0, paramExpression ) - ] ); - if ( result === null ) { - return null; - } - 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]; - } - - function templateWithReplacement() { - var result = sequence( [ - templateName, - colon, - replacement - ] ); - return result === null ? null : [ result[0], result[2] ]; - } - function templateWithOutReplacement() { - var result = sequence( [ - templateName, - colon, - paramExpression - ] ); - return result === null ? null : [ result[0], result[2] ]; - } - colon = makeStringParser(':'); - templateContents = choice( [ - function () { - var res = sequence( [ - // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} - // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}} - choice( [ templateWithReplacement, templateWithOutReplacement ] ), - 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] ); - } - ] ); - openTemplate = makeStringParser('{{'); - closeTemplate = makeStringParser('}}'); - nonWhitespaceExpression = choice( [ - template, - wikilink, - extLinkParam, - extlink, - replacement, - literalWithoutSpace - ] ); - paramExpression = choice( [ - template, - wikilink, - extLinkParam, - extlink, - replacement, - literalWithoutBar - ] ); - - expression = choice( [ - template, - wikilink, - extLinkParam, - extlink, - replacement, - html, - literal - ] ); - - // 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 ); - } - // 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... - - // 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 ); - } - return result; - } - - }; - /** - * htmlEmitter - object which primarily exists to emit HTML from parser ASTs - */ - mw.jqueryMsg.htmlEmitter = function ( language, magic ) { - this.language = language; - 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 - * @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, subnodes, operation, - jmsg = this; - switch ( typeof node ) { - case 'string': - case 'number': - ret = node; - break; - // 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 ); - } ); - operation = node[0].toLowerCase(); - if ( typeof jmsg[operation] === 'function' ) { - ret = jmsg[ 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 ) { - appendWithoutParsing( $span, childNode ); - } ); - } else { - // Let jQuery append nodes, arrays of nodes and jQuery objects - // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings) - appendWithoutParsing( $span, node ); - } - } ); - return $span; - }, - - /** - * Return escaped 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 ); - - if ( index < replacements.length ) { - return replacements[index]; - } else { - // index not found, fallback to displaying variable - return '$' + ( index + 1 ); - } - }, - - /** - * Transform wiki-link - * - * 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 - */ - wikilink: function ( nodes ) { - var page, anchor, url; - - page = nodes[0]; - url = mw.util.getUrl( 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 ); - }, - - /** - * Converts array of HTML element key value pairs to object - * - * @param {Array} nodes array of consecutive key value pairs, with index 2 * n being a name and 2 * n + 1 the associated value - * @return {Object} object mapping attribute name to attribute value - */ - htmlattributes: function ( nodes ) { - var i, len, mapping = {}; - for ( i = 0, len = nodes.length; i < len; i += 2 ) { - mapping[nodes[i]] = decodePrimaryHtmlEntities( nodes[i + 1] ); - } - return mapping; - }, - - /** - * Handles an (already-validated) HTML element. - * - * @param {Array} nodes nodes to process when creating element - * @return {jQuery|Array} jQuery node for valid HTML or array for disallowed element - */ - htmlelement: function ( nodes ) { - var tagName, attributes, contents, $element; - - tagName = nodes.shift(); - attributes = nodes.shift(); - contents = nodes; - $element = $( document.createElement( tagName ) ).attr( attributes ); - return appendWithoutParsing( $element, contents ); - }, - - /** - * 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} - */ - extlink: function ( nodes ) { - var $el, - arg = nodes[0], - contents = nodes[1]; - if ( arg instanceof jQuery ) { - $el = arg; - } else { - $el = $( '<a>' ); - if ( typeof arg === 'function' ) { - $el.click( arg ).attr( 'href', '#' ); - } else { - $el.attr( 'href', arg.toString() ); - } - } - return appendWithoutParsing( $el, contents ); - }, - - /** - * This is basically use a combination of replace + external link (link with parameter - * as url), but we don't want to run the regular replace here-on: inserting a - * url as href-attribute of a link will automatically escape it already, so - * we don't want replace to (manually) escape it as well. - * TODO throw error if nodes.length > 1 ? - * @param {Array} of one element, integer, n >= 0 - * @return {String} replacement - */ - extlinkparam: function ( nodes, replacements ) { - var replacement, - index = parseInt( nodes[0], 10 ); - if ( index < replacements.length) { - replacement = replacements[index]; - } else { - replacement = '$' + ( index + 1 ); - } - return this.extlink( [ replacement, nodes[1] ] ); - }, - - /** - * 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 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 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, forms; - - if ( nodes[0] && nodes[0].options instanceof mw.Map ) { - gender = nodes[0].options.get( 'gender' ); - } else { - gender = nodes[0]; - } - - forms = nodes.slice( 1 ); - - return this.language.gender( gender, forms ); - }, - - /** - * Transform parsed structure into grammar conversion. - * Invoked by putting {{grammar:form|word}} in a message - * @param {Array} of nodes [{Grammar case eg: genitive}, {String word}] - * @return {String} selected grammatical form according to current language - */ - grammar: function ( nodes ) { - 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. - // 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 - 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.format === 'plain' || !/\{\{|[\[<>]/.test(this.map.get( this.key ) ) ) { - // Fall back to mw.msg's simple parser - return oldParser.apply( this ); - } - - 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 ); - }; - -}( mediaWiki, jQuery ) ); |