diff options
author | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-01-28 09:50:25 -0500 |
---|---|---|
committer | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-01-28 09:50:25 -0500 |
commit | 5744df39e15f85c6cc8a9faf8924d77e76d2b216 (patch) | |
tree | a8c8dd40a94d1fa0d5377566aa5548ae55a163da /resources/mediawiki | |
parent | 4bb2aeca1d198391ca856aa16c40b8559c68daec (diff) | |
parent | 224b22a051051f6c2e494c3a2fb4adb42898e2d1 (diff) |
Merge branch 'archwiki'
Conflicts:
extensions/FluxBBAuthPlugin.php
extensions/SyntaxHighlight_GeSHi/README
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.i18n.php
extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php
extensions/SyntaxHighlight_GeSHi/geshi/docs/CHANGES
extensions/SyntaxHighlight_GeSHi/geshi/docs/THANKS
extensions/SyntaxHighlight_GeSHi/geshi/docs/TODO
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/AbstractPrivateMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Class.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Class_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Constant.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Constructor.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Destructor.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Function.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Global.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/I.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Index.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Interface.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Interface_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/L.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Lminus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Lplus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Method.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Page.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Page_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateClass.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateClass_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/PrivateVariable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/StaticMethod.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/StaticVariable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/T.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Tminus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Tplus.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/Variable.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/blank.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/class_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/file.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/function_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/next_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/next_button_disabled.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/package.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/package_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/previous_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/previous_button_disabled.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/private_class_logo.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/tutorial.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/tutorial_folder.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/api/media/images/up_button.png
extensions/SyntaxHighlight_GeSHi/geshi/docs/geshi-doc.html
extensions/SyntaxHighlight_GeSHi/geshi/docs/geshi-doc.txt
extensions/SyntaxHighlight_GeSHi/geshi/geshi.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/4cs.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502acme.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502kickass.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/6502tasm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/68000devpac.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/abap.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/actionscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/actionscript3.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ada.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/algol68.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/apache.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/applescript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/apt_sources.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/asm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/asp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autoconf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autohotkey.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/autoit.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/avisynth.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/awk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bascomavr.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bash.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/basic4gl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bibtex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/blitzbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/bnf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/boo.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c_loadrunner.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/c_mac.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/caddcl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cadlisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cfdg.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cfm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/chaiscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cil.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/clojure.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cmake.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cobol.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/coffeescript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cpp-qt.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/csharp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/css.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/cuesheet.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/d.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dcs.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/delphi.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/diff.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/div.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dos.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/dot.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/e.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ecmascript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/eiffel.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/email.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/epc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/erlang.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/euphoria.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/f1.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/falcon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fo.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fortran.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/freebasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/fsharp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gambas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gdb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/genero.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/genie.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gettext.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/glsl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gnuplot.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/go.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/groovy.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/gwbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/haskell.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/hicest.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/hq9plus.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/html4strict.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/html5.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/icon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/idl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ini.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/inno.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/intercal.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/io.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/j.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/java.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/java5.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/javascript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/jquery.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/kixtart.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/klonec.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/klonecpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/latex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/llvm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/locobasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/logtalk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lolcode.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lotusformulas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lotusscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lsl2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/lua.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/m68k.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/magiksf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/make.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mapbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/matlab.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mirc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mmix.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/modula2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/modula3.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mpasm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mxml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/mysql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/newlisp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/nsis.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oberon2.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/objc.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/objeck.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ocaml-brief.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ocaml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oobas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oracle11.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oracle8.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oxygene.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/oz.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pascal.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pcre.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/per.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/perl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/perl6.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/php-brief.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/php.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pic16.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pike.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pixelbender.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pli.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/plsql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/postgresql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/povray.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/powerbuilder.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/powershell.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/proftpd.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/progress.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/prolog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/properties.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/providex.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/purebasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/pycon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/python.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/q.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/qbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rails.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rebol.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/reg.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/robots.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rpmspec.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/rsplus.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/ruby.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sas.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scala.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scheme.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/scilab.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sdlbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/smalltalk.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/smarty.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/sql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/systemverilog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/tcl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/teraterm.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/text.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/thinbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/tsql.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/typoscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/unicon.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/uscript.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vala.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vb.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vbnet.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/verilog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vhdl.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/vim.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/visualfoxpro.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/visualprolog.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/whitespace.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/whois.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/winbatch.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xbasic.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xorg_conf.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/xpp.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/yaml.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/z80.php
extensions/SyntaxHighlight_GeSHi/geshi/geshi/zxbasic.php
Diffstat (limited to 'resources/mediawiki')
23 files changed, 2783 insertions, 1301 deletions
diff --git a/resources/mediawiki/images/arrow-collapsed-ltr.png b/resources/mediawiki/images/arrow-collapsed-ltr.png Binary files differnew file mode 100644 index 00000000..b17e578b --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-ltr.png diff --git a/resources/mediawiki/images/arrow-collapsed-rtl.png b/resources/mediawiki/images/arrow-collapsed-rtl.png Binary files differnew file mode 100644 index 00000000..a834548e --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-rtl.png diff --git a/resources/mediawiki/images/arrow-expanded.png b/resources/mediawiki/images/arrow-expanded.png Binary files differnew file mode 100644 index 00000000..2bec798e --- /dev/null +++ b/resources/mediawiki/images/arrow-expanded.png diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index 33cca585..5038c515 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -1,189 +1,368 @@ -/** - * mediaWiki.Title - * +/*! * @author Neil Kandalgaonkar, 2010 - * @author Timo Tijhof, 2011 + * @author Timo Tijhof, 2011-2013 * @since 1.18 - * - * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink */ ( function ( mw, $ ) { - /* Local space */ - /** - * Title - * @constructor + * @class mw.Title + * + * Parse titles into an object struture. Note that when using the constructor + * directly, passing invalid titles will result in an exception. Use #newFromText to use the + * logic directly and get null for invalid titles which is easier to work with. * - * @param title {String} Title of the page. If no second argument given, - * this will be searched for a namespace. - * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is. - * @return {Title} this + * @constructor + * @param {string} title Title of the page. If no second argument given, + * this will be searched for a namespace + * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title + * @throws {Error} When the title is invalid */ function Title( title, namespace ) { - this.ns = 0; // integer namespace id - this.name = null; // name in canonical 'database' form - this.ext = null; // extension - - if ( arguments.length === 2 ) { - setNameAndExtension( this, title ); - this.ns = fixNsId( namespace ); - } else if ( arguments.length === 1 ) { - setAll( this, title ); + var parsed = parse( title, namespace ); + if ( !parsed ) { + throw new Error( 'Unable to parse title' ); } + + this.namespace = parsed.namespace; + this.title = parsed.title; + this.ext = parsed.ext; + this.fragment = parsed.fragment; + return this; } -var - /** - * Public methods (defined later) - */ - fn, + /* Private members */ - /** - * Strip some illegal chars: control chars, colon, less than, greater than, - * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity - * intact, like unicode bidi chars, but it's a good start.. - * @param s {String} - * @return {String} - */ - clean = function ( s ) { - if ( s !== undefined ) { - return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' ); - } - }, + var /** - * Convert db-key to readable text. - * @param s {String} - * @return {String} + * @private + * @static + * @property NS_MAIN */ - text = function ( s ) { - if ( s !== null && s !== undefined ) { - return s.replace( /_/g, ' ' ); - } else { - return ''; - } - }, + NS_MAIN = 0, /** - * Sanitize name. + * @private + * @static + * @property NS_TALK */ - fixName = function ( s ) { - return clean( $.trim( s ) ); - }, + NS_TALK = 1, /** - * Sanitize name. + * @private + * @static + * @property NS_SPECIAL */ - fixExt = function ( s ) { - return clean( s ); - }, + NS_SPECIAL = -1, /** - * Sanitize namespace id. - * @param id {Number} Namespace id. - * @return {Number|Boolean} The id as-is or boolean false if invalid. + * Get the namespace id from a namespace name (either from the localized, canonical or alias + * name). + * + * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or + * even 'Bild'. + * + * @private + * @static + * @method getNsIdByName + * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored) + * @return {number|boolean} Namespace id or boolean false */ - fixNsId = function ( id ) { - // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] ) - var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()]; + getNsIdByName = function ( ns ) { + var id; - // Check only undefined (may be false-y, such as '' (main namespace) ). - if ( ns === undefined ) { + // Don't cast non-strings to strings, because null or undefined should not result in + // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki) + // Also, toLowerCase throws exception on null/undefined, because it is a String method. + if ( typeof ns !== 'string' ) { + return false; + } + ns = ns.toLowerCase(); + id = mw.config.get( 'wgNamespaceIds' )[ns]; + if ( id === undefined ) { return false; - } else { - return Number( id ); } + return id; }, + rUnderscoreTrim = /^_+|_+$/g, + + rSplit = /^(.+?)_*:_*(.*)$/, + + // See Title.php#getTitleInvalidRegex + rInvalid = new RegExp( + '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' + + // URL percent encoding sequences interfere with the ability + // to round-trip titles -- you can't link to them consistently. + '|%[0-9A-Fa-f]{2}' + + // XML/HTML character references produce similar issues. + '|&[A-Za-z0-9\u0080-\uFFFF]+;' + + '|&#[0-9]+;' + + '|&#x[0-9A-Fa-f]+;' + ), + /** - * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias). + * Internal helper for #constructor and #newFromtext. + * + * Based on Title.php#secureAndSplit * - * @example On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. - * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). - * @return {Number|Boolean} Namespace id or boolean false if unrecognized. + * @private + * @static + * @method parse + * @param {string} title + * @param {number} [defaultNamespace=NS_MAIN] + * @return {Object|boolean} */ - getNsIdByName = function ( ns ) { - // Don't cast non-strings to strings, because null or undefined - // should not result in returning the id of a potential namespace - // called "Null:" (e.g. on nullwiki.example.org) - // Also, toLowerCase throws exception on null/undefined, because - // it is a String.prototype method. - if ( typeof ns !== 'string' ) { + parse = function ( title, defaultNamespace ) { + var namespace, m, id, i, fragment, ext; + + namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace; + + title = title + // Normalise whitespace to underscores and remove duplicates + .replace( /[ _\s]+/g, '_' ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + + if ( title === '' ) { return false; } - ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize - var id = mw.config.get( 'wgNamespaceIds' )[ns]; - if ( id === undefined ) { - mw.log( 'mw.Title: Unrecognized namespace: ' + ns ); + + // Process initial colon + if ( title.charAt( 0 ) === ':' ) { + // Initial colon means main namespace instead of specified default + namespace = NS_MAIN; + title = title + // Strip colon + .substr( 1 ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + } + + // Process namespace prefix (if any) + m = title.match( rSplit ); + if ( m ) { + id = getNsIdByName( m[1] ); + if ( id !== false ) { + // Ordinary namespace + namespace = id; + title = m[2]; + + // For Talk:X pages, make sure X has no "namespace" prefix + if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) { + // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x) + if ( getNsIdByName( m[1] ) !== false ) { + return false; + } + } + } + } + + // Process fragment + i = title.indexOf( '#' ); + if ( i === -1 ) { + fragment = null; + } else { + fragment = title + // Get segment starting after the hash + .substr( i + 1 ) + // Convert to text + // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo") + .replace( /_/g, ' ' ); + + title = title + // Strip hash + .substr( 0, i ) + // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux") + .replace( rUnderscoreTrim, '' ); + } + + + // Reject illegal characters + if ( title.match( rInvalid ) ) { + return false; + } + + // Disallow titles that browsers or servers might resolve as directory navigation + if ( + title.indexOf( '.' ) !== -1 && ( + title === '.' || title === '..' || + title.indexOf( './' ) === 0 || + title.indexOf( '../' ) === 0 || + title.indexOf( '/./' ) !== -1 || + title.indexOf( '/../' ) !== -1 || + title.substr( -2 ) === '/.' || + title.substr( -3 ) === '/..' + ) + ) { + return false; + } + + // Disallow magic tilde sequence + if ( title.indexOf( '~~~' ) !== -1 ) { return false; } - return fixNsId( id ); + + // Disallow titles exceeding the 255 byte size limit (size of underlying database field) + // Except for special pages, e.g. [[Special:Block/Long name]] + // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should + // be less than 512 bytes. + if ( namespace !== NS_SPECIAL && $.byteLength( title ) > 255 ) { + return false; + } + + // Can't make a link to a namespace alone. + if ( title === '' && namespace !== NS_MAIN ) { + return false; + } + + // Any remaining initial :s are illegal. + if ( title.charAt( 0 ) === ':' ) { + return false; + } + + // For backwards-compatibility with old mw.Title, we separate the extension from the + // rest of the title. + i = title.lastIndexOf( '.' ); + if ( i === -1 || title.length <= i + 1 ) { + // Extensions are the non-empty segment after the last dot + ext = null; + } else { + ext = title.substr( i + 1 ); + title = title.substr( 0, i ); + } + + return { + namespace: namespace, + title: title, + ext: ext, + fragment: fragment + }; }, /** - * Helper to extract namespace, name and extension from a string. + * Convert db-key to readable text. * - * @param title {mw.Title} - * @param raw {String} - * @return {mw.Title} + * @private + * @static + * @method text + * @param {string} s + * @return {string} */ - setAll = function ( title, s ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), - ns_match = getNsIdByName( matches[1] ); - - // Namespace must be valid, and title must be a non-empty string. - if ( ns_match && typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ns = ns_match; - title.name = fixName( matches[2] ); - if ( typeof matches[3] === 'string' && matches[3] !== '' ) { - title.ext = fixExt( matches[3] ); - } + text = function ( s ) { + if ( s !== null && s !== undefined ) { + return s.replace( /_/g, ' ' ); } else { - // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace. - title.ns = 0; - setNameAndExtension( title, s ); + return ''; } - return title; }, + // Polyfill for ES5 Object.create + createObject = Object.create || ( function () { + return function ( o ) { + function Title() {} + if ( o !== Object( o ) ) { + throw new Error( 'Cannot inherit from a non-object' ); + } + Title.prototype = o; + return new Title(); + }; + }() ); + + + /* Static members */ + /** - * Helper to extract name and extension from a string. + * Constructor for Title objects with a null return instead of an exception for invalid titles. * - * @param title {mw.Title} - * @param raw {String} - * @return {mw.Title} + * @static + * @method + * @param {string} title + * @param {number} [namespace=NS_MAIN] Default namespace + * @return {mw.Title|null} A valid Title object or null if the title is invalid */ - setNameAndExtension = function ( title, raw ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ ); - - // Title must be a non-empty string. - if ( typeof matches[1] === 'string' && matches[1] !== '' ) { - title.name = fixName( matches[1] ); - if ( typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ext = fixExt( matches[2] ); - } - } else { - throw new Error( 'mw.Title: Could not parse title "' + raw + '"' ); + Title.newFromText = function ( title, namespace ) { + var t, parsed = parse( title, namespace ); + if ( !parsed ) { + return null; } - return title; + + t = createObject( Title.prototype ); + t.namespace = parsed.namespace; + t.title = parsed.title; + t.ext = parsed.ext; + t.fragment = parsed.fragment; + + return t; }; + /** + * Get the file title from an image element + * + * var title = mw.Title.newFromImg( $( 'img:first' ) ); + * + * @static + * @param {HTMLElement|jQuery} img The image to use as a base + * @return {mw.Title|null} The file title or null if unsuccessful + */ + Title.newFromImg = function ( img ) { + var matches, i, regex, src, decodedSrc, + + // thumb.php-generated thumbnails + thumbPhpRegex = /thumb\.php/, + + regexes = [ + // Thumbnails + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Thumbnails in non-hashed upload directories + /\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Full size images + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/, + + // Full-size images in non-hashed upload directories + /\/([^\s\/]+)$/ + ], + + recount = regexes.length; + + src = img.jquery ? img[0].src : img.src; + + matches = src.match( thumbPhpRegex ); + + if ( matches ) { + return mw.Title.newFromText( 'File:' + mw.util.getParamValue( 'f', src ) ); + } + + decodedSrc = decodeURIComponent( src ); + + for ( i = 0; i < recount; i++ ) { + regex = regexes[i]; + matches = decodedSrc.match( regex ); + + if ( matches && matches[1] ) { + return mw.Title.newFromText( 'File:' + matches[1] ); + } + } - /* Static space */ + return null; + }; /** * Whether this title exists on the wiki. - * @param title {mixed} prefixed db-key name (string) or instance of Title - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * + * @static + * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title + * @return {boolean|null} Boolean if the information is available, otherwise null */ Title.exists = function ( title ) { - var type = $.type( title ), obj = Title.exist.pages, match; + var match, + type = $.type( title ), + obj = Title.exist.pages; + if ( type === 'string' ) { match = obj[title]; } else if ( type === 'object' && title instanceof Title ) { @@ -191,27 +370,34 @@ var } else { throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' ); } + if ( typeof match === 'boolean' ) { return match; } + return null; }; - /** - * @var Title.exist {Object} - */ Title.exist = { /** - * @var Title.exist.pages {Object} Keyed by PrefixedDb title. * Boolean true value indicates page does exist. + * + * @static + * @property {Object} exist.pages Keyed by PrefixedDb title. */ pages: {}, + /** - * @example Declare existing titles: Title.exist.set(['User:John_Doe', ...]); - * @example Declare titles nonexistent: Title.exist.set(['File:Foo_bar.jpg', ...], false); - * @param titles {String|Array} Title(s) in strict prefixedDb title form. - * @param state {Boolean} (optional) State of the given titles. Defaults to true. - * @return {Boolean} + * Example to declare existing titles: + * Title.exist.set(['User:John_Doe', ...]); + * Eample to declare titles nonexistent: + * Title.exist.set(['File:Foo_bar.jpg', ...], false); + * + * @static + * @property exist.set + * @param {string|Array} titles Title(s) in strict prefixedDb title form + * @param {boolean} [state=true] State of the given titles + * @return {boolean} */ set: function ( titles, state ) { titles = $.isArray( titles ) ? titles : [titles]; @@ -224,119 +410,176 @@ var } }; - /* Public methods */ + /* Public members */ - fn = { + Title.prototype = { constructor: Title, /** - * Get the namespace number. - * @return {Number} + * Get the namespace number + * + * Example: 6 for "File:Example_image.svg". + * + * @return {number} */ - getNamespaceId: function (){ - return this.ns; + getNamespaceId: function () { + return this.namespace; }, /** - * Get the namespace prefix (in the content-language). - * In NS_MAIN this is '', otherwise namespace name plus ':' - * @return {String} + * Get the namespace prefix (in the content language) + * + * Example: "File:" for "File:Example_image.svg". + * In #NS_MAIN this is '', otherwise namespace name plus ':' + * + * @return {string} */ - getNamespacePrefix: function (){ - return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); + getNamespacePrefix: function () { + return this.namespace === NS_MAIN ? + '' : + ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' ); }, /** - * The name, like "Foo_bar" - * @return {String} + * Get the page name without extension or namespace prefix + * + * Example: "Example_image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMain. + * + * @return {string} */ getName: function () { - if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { - return this.name; + if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.title; } else { - return $.ucFirst( this.name ); + return $.ucFirst( this.title ); } }, /** - * The name, like "Foo bar" - * @return {String} + * Get the page name (transformed by #text) + * + * Example: "Example image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMainText. + * + * @return {string} */ getNameText: function () { return text( this.getName() ); }, /** - * Get full name in prefixed DB form, like File:Foo_bar.jpg, - * most useful for API calls, anything that must identify the "title". + * Get the extension of the page name (if any) + * + * @return {string|null} Name extension or null if there is none */ - getPrefixedDb: function () { - return this.getNamespacePrefix() + this.getMain(); + getExtension: function () { + return this.ext; }, /** - * Get full name in text form, like "File:Foo bar.jpg". - * @return {String} + * Shortcut for appendable string to form the main page name. + * + * Returns a string like ".json", or "" if no extension. + * + * @return {string} */ - getPrefixedText: function () { - return text( this.getPrefixedDb() ); + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; }, /** - * The main title (without namespace), like "Foo_bar.jpg" - * @return {String} + * Get the main page name (transformed by #text) + * + * Example: "Example_image.svg" for "File:Example_image.svg". + * + * @return {string} */ getMain: function () { return this.getName() + this.getDotExtension(); }, /** - * The "text" form, like "Foo bar.jpg" - * @return {String} + * Get the main page name (transformed by #text) + * + * Example: "Example image.svg" for "File:Example_image.svg". + * + * @return {string} */ getMainText: function () { return text( this.getMain() ); }, /** - * Get the extension (returns null if there was none) - * @return {String|null} extension + * Get the full page name + * + * Eaxample: "File:Example_image.svg". + * Most useful for API calls, anything that must identify the "title". + * + * @return {string} */ - getExtension: function () { - return this.ext; + getPrefixedDb: function () { + return this.getNamespacePrefix() + this.getMain(); }, /** - * Convenience method: return string like ".jpg", or "" if no extension - * @return {String} + * Get the full page name (transformed by #text) + * + * Example: "File:Example image.svg" for "File:Example_image.svg". + * + * @return {string} */ - getDotExtension: function () { - return this.ext === null ? '' : '.' + this.ext; + getPrefixedText: function () { + return text( this.getPrefixedDb() ); }, /** - * Return the URL to this title - * @return {String} + * Get the fragment (if any). + * + * Note that this method (by design) does not include the hash character and + * the value is not url encoded. + * + * @return {string|null} + */ + getFragment: function () { + return this.fragment; + }, + + /** + * Get the URL to this title + * + * @see mw.util#getUrl + * @return {string} */ getUrl: function () { - return mw.util.wikiGetlink( this.toString() ); + return mw.util.getUrl( this.toString() ); }, /** * Whether this title exists on the wiki. - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * + * @see #static-method-exists + * @return {boolean|null} Boolean if the information is available, otherwise null */ exists: function () { return Title.exists( this ); } }; - // Alias - fn.toString = fn.getPrefixedDb; - fn.toText = fn.getPrefixedText; + /** + * @alias #getPrefixedDb + * @method + */ + Title.prototype.toString = Title.prototype.getPrefixedDb; + - // Assign - Title.prototype = fn; + /** + * @alias #getPrefixedText + * @method + */ + Title.prototype.toText = Title.prototype.getPrefixedText; // Expose mw.Title = Title; diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index bd12b214..a2d4d6cb 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -61,11 +61,11 @@ /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of * having to add something to the URI as we go, but only if it's present, and to include a character before or after if so. - * @param {String} to prepend, if value not empty - * @param {String} value to include, if not empty - * @param {String} to append, if value not empty - * @param {Boolean} raw -- if true, do not URI encode - * @return {String} + * @param {string|undefined} pre To prepend. + * @param {string} val To include. + * @param {string} post To append. + * @param {boolean} raw If true, val will not be encoded. + * @return {string} Result. */ function cat( pre, val, post, raw ) { if ( val === undefined || val === null || val === '' ) { @@ -76,8 +76,8 @@ // Regular expressions to parse many common URIs. var parser = { - strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ + strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ }, // The order here matches the order of captured matches in the above parser regexes. @@ -103,14 +103,14 @@ /** * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse. * @constructor - * @param {Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone). + * @param {Object|string} uri URI string, or an Object with appropriate properties (especially another URI object to clone). * Object must have non-blank 'protocol', 'host', and 'path' properties. - * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created - * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core). - * @param {Object|Boolean} Object with options, or (backwards compatibility) a boolean for strictMode - * - strictMode {Boolean} Trigger strict mode parsing of the url. Default: false - * - overrideKeys {Boolean} Wether to let duplicate query parameters override eachother (true) or automagically - * convert to an array (false, default). + * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created + * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core). + * @param {Object|boolean} Object with options, or (backwards compatibility) a boolean for strictMode + * - {boolean} strictMode Trigger strict mode parsing of the url. Default: false + * - {boolean} overrideKeys Wether to let duplicate query parameters override eachother (true) or automagically + * convert to an array (false, default). */ function Uri( uri, options ) { options = typeof options === 'object' ? options : { strictMode: !!options }; @@ -158,7 +158,7 @@ } if ( this.path && this.path.charAt( 0 ) !== '/' ) { // A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot - // figure out whether the last path compoennt of defaultUri.path is a directory or a file. + // figure out whether the last path component of defaultUri.path is a directory or a file. throw new Error( 'Bad constructor arguments' ); } if ( !( this.protocol && this.host && this.path ) ) { @@ -169,8 +169,8 @@ /** * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986 * Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a + - * @param {String} string - * @return {String} encoded for URI + * @param {string} s String to encode. + * @return {string} Encoded string for URI. */ Uri.encode = function ( s ) { return encodeURIComponent( s ) @@ -180,9 +180,9 @@ }; /** - * Standard decodeURIComponent, with '+' to space - * @param {String} string encoded for URI - * @return {String} decoded string + * Standard decodeURIComponent, with '+' to space. + * @param {string} s String encoded for URI. + * @return {string} Decoded string. */ Uri.decode = function ( s ) { return decodeURIComponent( s.replace( /\+/g, '%20' ) ); @@ -192,16 +192,16 @@ /** * Parse a string and set our properties accordingly. - * @param {String} URI + * @param {string} str URI * @param {Object} options - * @return {Boolean} success + * @return {boolean} Success. */ parse: function ( str, options ) { var q, uri = this, matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); $.each( properties, function ( i, property ) { - uri[ property ] = matches[ i+1 ]; + uri[ property ] = matches[ i + 1 ]; } ); // uri.query starts out as the query string; we will parse it into key-val pairs then make @@ -210,7 +210,7 @@ q = {}; // using replace to iterate over a string if ( uri.query ) { - uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { + uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) { var k, v; if ( $1 ) { k = Uri.decode( $1 ); @@ -240,7 +240,7 @@ /** * Returns user and password portion of a URI. - * @return {String} + * @return {string} */ getUserInfo: function () { return cat( '', this.user, cat( ':', this.password, '' ) ); @@ -248,7 +248,7 @@ /** * Gets host and port portion of a URI. - * @return {String} + * @return {string} */ getHostPort: function () { return this.host + cat( ':', this.port, '' ); @@ -257,7 +257,7 @@ /** * Returns the userInfo and host and port portion of the URI. * In most real-world URLs, this is simply the hostname, but it is more general. - * @return {String} + * @return {string} */ getAuthority: function () { return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); @@ -266,7 +266,7 @@ /** * Returns the query arguments of the URL, encoded into a string * Does not preserve the order of arguments passed into the URI. Does handle escaping. - * @return {String} + * @return {string} */ getQueryString: function () { var args = []; @@ -274,7 +274,13 @@ var k = Uri.encode( key ), vals = $.isArray( val ) ? val : [ val ]; $.each( vals, function ( i, v ) { - args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); + if ( v === null ) { + args.push( k ); + } else if ( k === 'title' ) { + args.push( k + '=' + mw.util.wikiUrlencode( v ) ); + } else { + args.push( k + '=' + Uri.encode( v ) ); + } } ); } ); return args.join( '&' ); @@ -282,7 +288,7 @@ /** * Returns everything after the authority section of the URI - * @return {String} + * @return {string} */ getRelativePath: function () { return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); @@ -290,7 +296,7 @@ /** * Gets the entire URI string. May not be precisely the same as input due to order of query arguments. - * @return {String} the URI string + * @return {string} The URI string. */ toString: function () { return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css index 149e1bff..513cb847 100644 --- a/resources/mediawiki/mediawiki.debug.css +++ b/resources/mediawiki/mediawiki.debug.css @@ -1,6 +1,5 @@ .mw-debug { width: 100%; - text-align: left; background-color: #eee; border-top: 1px solid #aaa; } diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index 1ad1a623..986917a1 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -96,7 +96,7 @@ buildHtml: function () { var $container, $bits, panes, id, gitInfo; - $container = $( '<div id="mw-debug-toolbar" class="mw-debug"></div>' ); + $container = $( '<div id="mw-debug-toolbar" class="mw-debug" lang="en" dir="ltr"></div>' ); $bits = $( '<div class="mw-debug-bits"></div>' ); @@ -187,9 +187,7 @@ .text( 'Time: ' + this.data.time.toFixed( 5 ) ); bitDiv( 'memory' ) - .text( 'Memory: ' + this.data.memory ) - .append( $( '<span title="Peak usage"></span>' ).text( ' (' + this.data.memoryPeak + ')' ) ); - + .text( 'Memory: ' + this.data.memory + ' (Peak: ' + this.data.memoryPeak + ')' ); $bits.appendTo( $container ); @@ -231,7 +229,7 @@ $( '<colgroup>' ).css( 'width', 350 ).appendTo( $table ); - entryTypeText = function( entryType ) { + entryTypeText = function ( entryType ) { switch ( entryType ) { case 'log': return 'Log'; diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js index 634d02b1..1afe51ef 100644 --- a/resources/mediawiki/mediawiki.feedback.js +++ b/resources/mediawiki/mediawiki.feedback.js @@ -1,5 +1,5 @@ /** - * mediawiki.Feedback + * mediawiki.feedback * * @author Ryan Kaldari, 2010 * @author Neil Kandalgaonkar, 2010-11 @@ -68,17 +68,28 @@ mw.Feedback.prototype = { setup: function () { - var fb = this; + var $feedbackPageLink, + $bugNoteLink, + $bugsListLink, + fb = this; - var $feedbackPageLink = $( '<a>' ) - .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } ) - .css( { 'white-space': 'nowrap' } ); + $feedbackPageLink = $( '<a>' ) + .attr( { + href: fb.title.getUrl(), + target: '_blank' + } ) + .css( { + whiteSpace: 'nowrap' + } ); - var $bugNoteLink = $( '<a>' ).attr( { 'href': '#' } ).click( function () { + $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () { fb.displayBugs(); } ); - var $bugsListLink = $( '<a>' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } ); + $bugsListLink = $( '<a>' ).attr( { + href: fb.bugsListLink, + target: '_blank' + } ); // TODO: Use a stylesheet instead of these inline styles this.$dialog = @@ -108,7 +119,7 @@ ), $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' ).append( mw.msg( 'feedback-adding' ), - $( '<br/>' ), + $( '<br>' ), $( '<span class="feedback-spinner"></span>' ) ), $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg( @@ -148,9 +159,9 @@ }, displayBugs: function () { - var fb = this; + var fb = this, + bugsButtons = {}; this.display( 'bugs' ); - var bugsButtons = {}; bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { window.open( fb.bugsLink, '_blank' ); }; @@ -163,9 +174,9 @@ }, displayThanks: function () { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'thanks' ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -181,14 +192,14 @@ * message: {String} */ displayForm: function ( contents ) { - var fb = this; + var fb = this, + formButtons = {}; this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : ''; this.messageInput.value = ( contents && contents.message ) ? contents.message : ''; this.display( 'form' ); // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized - var formButtons = {}; formButtons[ mw.msg( 'feedback-submit' ) ] = function () { fb.submit(); }; @@ -199,10 +210,10 @@ }, displayError: function ( message ) { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'error' ); this.$dialog.find( '.feedback-error-msg' ).msg( message ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -231,7 +242,7 @@ } } - function err( code, info ) { + function err() { // ajax request failed fb.displayError( 'feedback-error3' ); } diff --git a/resources/mediawiki/mediawiki.hidpi.js b/resources/mediawiki/mediawiki.hidpi.js new file mode 100644 index 00000000..ecee450c --- /dev/null +++ b/resources/mediawiki/mediawiki.hidpi.js @@ -0,0 +1,5 @@ +jQuery( function ( $ ) { + // Apply hidpi images on DOM-ready + // Some may have already partly preloaded at low resolution. + $( 'body' ).hidpi(); +} ); diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index a4753b99..de068598 100644 --- a/resources/mediawiki/mediawiki.htmlform.js +++ b/resources/mediawiki/mediawiki.htmlform.js @@ -1,64 +1,128 @@ /** - * Utility functions for jazzing up HTMLForm elements + * Utility functions for jazzing up HTMLForm elements. */ -( function ( $ ) { +( function ( mw, $ ) { -/** - * jQuery plugin to fade or snap to visible state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goIn = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).show(); - } - return $(this).stop( true, true ).fadeIn(); -}; + /** + * jQuery plugin to fade or snap to visible state. + * + * @param {boolean} instantToggle [optional] + * @return {jQuery} + */ + $.fn.goIn = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).show(); + } + return $(this).stop( true, true ).fadeIn(); + }; -/** - * jQuery plugin to fade or snap to hiding state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goOut = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).hide(); - } - return $(this).stop( true, true ).fadeOut(); -}; + /** + * jQuery plugin to fade or snap to hiding state. + * + * @param {boolean} instantToggle [optional] + * @return jQuery + */ + $.fn.goOut = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).hide(); + } + return $(this).stop( true, true ).fadeOut(); + }; -/** - * Bind a function to the jQuery object via live(), and also immediately trigger - * the function on the objects with an 'instant' parameter set to true - * @param callback function taking one parameter, which is Bool true when the event - * is called immediately, and the EventArgs object when triggered from an event - */ -$.fn.liveAndTestAtStart = function ( callback ){ - $(this) - .live( 'change', callback ) - .each( function ( index, element ){ - callback.call( this, true ); - } ); -}; + /** + * Bind a function to the jQuery object via live(), and also immediately trigger + * the function on the objects with an 'instant' parameter set to true. + * @param {Function} callback Takes one parameter, which is {true} when the + * event is called immediately, and {jQuery.Event} when triggered from an event. + */ + $.fn.liveAndTestAtStart = function ( callback ){ + $(this) + .live( 'change', callback ) + .each( function () { + callback.call( this, true ); + } ); + }; -// Document ready: -$( function () { + $( function () { - // Animate the SelectOrOther fields, to only show the text field when - // 'other' is selected. - $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { - var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); - $other = $other.add( $other.siblings( 'br' ) ); - if ( $(this).val() === 'other' ) { - $other.goIn( instant ); - } else { - $other.goOut( instant ); - } - }); + // Animate the SelectOrOther fields, to only show the text field when + // 'other' is selected. + $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { + var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); + $other = $other.add( $other.siblings( 'br' ) ); + if ( $(this).val() === 'other' ) { + $other.goIn( instant ); + } else { + $other.goOut( instant ); + } + }); -}); + } ); + function addMulti( $oldContainer, $container ) { + var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ), + oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ), + $select = $( '<select>' ), + dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' ); + oldClass = $.trim( oldClass ); + $select.attr( { + name: name, + multiple: 'multiple', + 'data-placeholder': dataPlaceholder.plain(), + 'class': 'htmlform-chzn-select mw-input ' + oldClass + } ); + $oldContainer.find( 'input' ).each( function () { + var $oldInput = $(this), + checked = $oldInput.prop( 'checked' ), + $option = $( '<option>' ); + $option.prop( 'value', $oldInput.prop( 'value' ) ); + if ( checked ) { + $option.prop( 'selected', true ); + } + $option.text( $oldInput.prop( 'value' ) ); + $select.append( $option ); + } ); + $container.append( $select ); + } -}( jQuery ) ); + function convertCheckboxesToMulti( $oldContainer, type ) { + var $fieldLabel = $( '<td>' ), + $td = $( '<td>' ), + $fieldLabelText = $( '<label>' ), + $container; + if ( type === 'tr' ) { + addMulti( $oldContainer, $td ); + $container = $( '<tr>' ); + $container.append( $td ); + } else if ( type === 'div' ) { + $fieldLabel = $( '<div>' ); + $container = $( '<div>' ); + addMulti( $oldContainer, $container ); + } + $fieldLabel.attr( 'class', 'mw-label' ); + $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() ); + $fieldLabel.append( $fieldLabelText ); + $container.prepend( $fieldLabel ); + $oldContainer.replaceWith( $container ); + return $container; + } + + if ( $( '.mw-chosen' ).length ) { + mw.loader.using( 'jquery.chosen', function () { + $( '.mw-chosen' ).each( function () { + var type = this.nodeName.toLowerCase(), + $converted = convertCheckboxesToMulti( $( this ), type ); + $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } ); + } ); + } ); + } + + $( function () { + var $matrixTooltips = $( '.mw-htmlform-matrix .mw-htmlform-tooltip' ); + if ( $matrixTooltips.length ) { + mw.loader.using( 'jquery.tipsy', function () { + $matrixTooltips.tipsy( { gravity: 's' } ); + } ); + } + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.icon.css b/resources/mediawiki/mediawiki.icon.css new file mode 100644 index 00000000..f61b7257 --- /dev/null +++ b/resources/mediawiki/mediawiki.icon.css @@ -0,0 +1,15 @@ +/* General-purpose icons via CSS. Classes here should be named "mw-icon-*". */ + +/* For the collapsed and expanded arrows, we also provide selectors to make it + * easy to use them with jquery.makeCollapsible. */ +.mw-icon-arrow-collapsed, +.mw-collapsible-arrow.mw-collapsible-toggle-collapsed { + /* @embed */ + background: url(images/arrow-collapsed-ltr.png) no-repeat left bottom; +} + +.mw-icon-arrow-expanded, +.mw-collapsible-arrow.mw-collapsible-toggle-expanded { + /* @embed */ + background: url(images/arrow-expanded.png) no-repeat left bottom; +} diff --git a/resources/mediawiki/mediawiki.inspect.js b/resources/mediawiki/mediawiki.inspect.js new file mode 100644 index 00000000..2f2ca335 --- /dev/null +++ b/resources/mediawiki/mediawiki.inspect.js @@ -0,0 +1,204 @@ +/*! + * Tools for inspecting page composition and performance. + * + * @author Ori Livneh + * @since 1.22 + */ +/*jshint devel:true */ +( function ( mw, $ ) { + + function sortByProperty( array, prop, descending ) { + var order = descending ? -1 : 1; + return array.sort( function ( a, b ) { + return a[prop] > b[prop] ? order : a[prop] < b[prop] ? -order : 0; + } ); + } + + /** + * @class mw.inspect + * @singleton + */ + var inspect = { + + /** + * Calculate the byte size of a ResourceLoader module. + * + * @param {string} moduleName The name of the module + * @return {number|null} Module size in bytes or null + */ + getModuleSize: function ( moduleName ) { + var module = mw.loader.moduleRegistry[ moduleName ], + payload = 0; + + if ( mw.loader.getState( moduleName ) !== 'ready' ) { + return null; + } + + if ( !module.style && !module.script ) { + return null; + } + + // Tally CSS + if ( module.style && $.isArray( module.style.css ) ) { + $.each( module.style.css, function ( i, stylesheet ) { + payload += $.byteLength( stylesheet ); + } ); + } + + // Tally JavaScript + if ( $.isFunction( module.script ) ) { + payload += $.byteLength( module.script.toString() ); + } + + return payload; + }, + + /** + * Given CSS source, count both the total number of selectors it + * contains and the number which match some element in the current + * document. + * + * @param {string} css CSS source + * @return Selector counts + * @return {number} return.selectors Total number of selectors + * @return {number} return.matched Number of matched selectors + */ + auditSelectors: function ( css ) { + var selectors = { total: 0, matched: 0 }, + style = document.createElement( 'style' ), + sheet, rules; + + style.textContent = css; + document.body.appendChild( style ); + // Standards-compliant browsers use .sheet.cssRules, IE8 uses .styleSheet.rules… + sheet = style.sheet || style.styleSheet; + rules = sheet.cssRules || sheet.rules; + $.each( rules, function ( index, rule ) { + selectors.total++; + if ( document.querySelector( rule.selectorText ) !== null ) { + selectors.matched++; + } + } ); + document.body.removeChild( style ); + return selectors; + }, + + /** + * Get a list of all loaded ResourceLoader modules. + * + * @return {Array} List of module names + */ + getLoadedModules: function () { + return $.grep( mw.loader.getModuleNames(), function ( module ) { + return mw.loader.getState( module ) === 'ready'; + } ); + }, + + /** + * Print tabular data to the console, using console.table, console.log, + * or mw.log (in declining order of preference). + * + * @param {Array} data Tabular data represented as an array of objects + * with common properties. + */ + dumpTable: function ( data ) { + try { + // Bartosz made me put this here. + if ( window.opera ) { throw window.opera; } + // Use Function.prototype#call to force an exception on Firefox, + // which doesn't define console#table but doesn't complain if you + // try to invoke it. + console.table.call( console, data ); + return; + } catch (e) {} + try { + console.log( $.toJSON( data, null, 2 ) ); + return; + } catch (e) {} + mw.log( data ); + }, + + /** + * Generate and print one more reports. When invoked with no arguments, + * print all reports. + * + * @param {string...} [reports] Report names to run, or unset to print + * all available reports. + */ + runReports: function () { + var reports = arguments.length > 0 ? + Array.prototype.slice.call( arguments ) : + $.map( inspect.reports, function ( v, k ) { return k; } ); + + $.each( reports, function ( index, name ) { + inspect.dumpTable( inspect.reports[name]() ); + } ); + }, + + /** + * @class mw.inspect.reports + * @singleton + */ + reports: { + /** + * Generate a breakdown of all loaded modules and their size in + * kilobytes. Modules are ordered from largest to smallest. + */ + size: function () { + // Map each module to a descriptor object. + var modules = $.map( inspect.getLoadedModules(), function ( module ) { + return { + name: module, + size: inspect.getModuleSize( module ) + }; + } ); + + // Sort module descriptors by size, largest first. + sortByProperty( modules, 'size', true ); + + // Convert size to human-readable string. + $.each( modules, function ( i, module ) { + module.size = module.size > 1024 ? + ( module.size / 1024 ).toFixed( 2 ) + ' KB' : + ( module.size !== null ? module.size + ' B' : null ); + } ); + + return modules; + }, + + /** + * For each module with styles, count the number of selectors, and + * count how many match against some element currently in the DOM. + */ + css: function () { + var modules = []; + + $.each( inspect.getLoadedModules(), function ( index, name ) { + var css, stats, module = mw.loader.moduleRegistry[name]; + + try { + css = module.style.css.join(); + } catch (e) { return; } // skip + + stats = inspect.auditSelectors( css ); + modules.push( { + module: name, + allSelectors: stats.total, + matchedSelectors: stats.matched, + percentMatched: stats.total !== 0 ? + ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null + } ); + } ); + sortByProperty( modules, 'allSelectors', true ); + return modules; + }, + } + }; + + if ( mw.config.get( 'debug' ) ) { + mw.log( 'mw.inspect: reports are not available in debug mode.' ); + } + + mw.inspect = inspect; + +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 86af31ff..70b9be93 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -3,18 +3,98 @@ * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs * * @author neilk@wikimedia.org +* @author mflaschen@wikimedia.org */ ( function ( mw, $ ) { - var slice = Array.prototype.slice, + 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 + 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} @@ -30,12 +110,12 @@ * @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 ) { - return $( '<span>' ).append( key + ': ' + e.message ); + return $( '<span>' ).text( key + ': ' + e.message ); } }; } @@ -56,19 +136,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,14 +186,16 @@ * 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 appendWithoutParsing( $target, failableParserFn( arguments ).contents() ) + // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) ) $.each( failableParserFn( arguments ).contents(), function ( i, node ) { - $target.append( node ); + appendWithoutParsing( $target, node ); } ); return $target; }; @@ -113,20 +208,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 +250,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 +273,29 @@ * @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. - 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 +306,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 +323,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(); @@ -232,6 +358,15 @@ 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 ); @@ -258,11 +393,23 @@ // 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( '\\' ); + 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, @@ -270,36 +417,54 @@ ] ); 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(''); + } + + asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ ); + htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ ); + htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ ); + + whitespace = makeRegexParser( /^\s+/ ); + dollar = makeStringParser( '$' ); + digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ @@ -311,20 +476,28 @@ } 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 + 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 = null; - var parsedResult = sequence( [ + var result, parsedResult; + result = null; + parsedResult = sequence( [ openExtlink, nonWhitespaceExpression, whitespace, - expression, + nOrMore( 1, expression ), closeExtlink ] ); if ( parsedResult !== null ) { - result = [ 'LINK', parsedResult[1], parsedResult[3] ]; + 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; } @@ -341,41 +514,212 @@ if ( result === null ) { return null; } - return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; + return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; } - var openLink = makeStringParser( '[[' ); - var closeLink = makeStringParser( ']]' ); - function link() { - var result = null; - var parsedResult = sequence( [ - openLink, - expression, - closeLink + 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 ) { - result = [ 'WLINK', parsedResult[1] ]; + parsedLinkContents = parsedResult[1]; + result = [ 'WIKILINK' ].concat( parsedLinkContents ); } return result; } - var templateName = transform( + + // 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 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 +736,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,59 +758,70 @@ 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, + wikilink, extLinkParam, extlink, replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ + paramExpression = choice( [ template, - link, + wikilink, extLinkParam, extlink, replacement, literalWithoutBar ] ); - var expression = choice( [ + + expression = choice( [ template, - link, + wikilink, extLinkParam, extlink, replacement, + html, 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 +846,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 { @@ -540,11 +897,12 @@ $.each( nodes, function ( i, node ) { if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) { $.each( node.contents(), function ( j, childNode ) { - $span.append( childNode ); + appendWithoutParsing( $span, 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) + appendWithoutParsing( $span, node ); } } ); return $span; @@ -555,7 +913,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 +921,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 +930,71 @@ /** * 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'; + 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 ); }, /** @@ -593,10 +1006,10 @@ * @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; + extlink: function ( nodes ) { + var $el, + arg = nodes[0], + contents = nodes[1]; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -607,12 +1020,11 @@ $el.attr( 'href', arg.toString() ); } } - $el.append( contents ); - return $el; + return appendWithoutParsing( $el, contents ); }, /** - * This is basically use a combination of replace + link (link with parameter + * 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. @@ -620,7 +1032,7 @@ * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ - linkparam: function ( nodes, replacements ) { + extlinkparam: function ( nodes, replacements ) { var replacement, index = parseInt( nodes[0], 10 ); if ( index < replacements.length) { @@ -628,7 +1040,7 @@ } else { replacement = '$' + ( index + 1 ); } - return this.link( [ replacement, nodes[1] ] ); + return this.extlink( [ replacement, nodes[1] ] ); }, /** @@ -639,25 +1051,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 +1087,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 +1124,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 ); }; diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg index e059ed1d..7879d6fa 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.peg +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -37,6 +37,7 @@ templateParam templateName = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() } +/* TODO: Update to reflect separate piped and unpiped handling */ link = "[[" w:expression "]]" { return [ 'WLINK', w ]; } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 19112aed..80223e5d 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,26 +1,81 @@ -/* - * Core MediaWiki JavaScript Library +/** + * Base library for MediaWiki. + * + * @class mw + * @alternateClassName mediaWiki + * @singleton */ -/*global mw:true */ + var mw = ( function ( $, undefined ) { - "use strict"; + 'use strict'; /* Private Members */ var hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice; + /** + * Log a message to window.console, if possible. Useful to force logging of some + * errors that are otherwise hard to detect (I.e., this logs also in production mode). + * Gets console references in each invocation, so that delayed debugging tools work + * fine. No need for optimization here, which would only result in losing logs. + * + * @private + * @param {string} msg text for the log entry. + * @param {Error} [e] + */ + function log( msg, e ) { + var console = window.console; + if ( console && console.log ) { + console.log( msg ); + // If we have an exception object, log it through .error() to trigger + // proper stacktraces in browsers that support it. There are no (known) + // browsers that don't support .error(), that do support .log() and + // have useful exception handling through .log(). + if ( e && console.error ) { + console.error( String( e ), e ); + } + } + } + /* Object constructors */ /** - * Map - * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. * - * @param global boolean Whether to store the values in the global window + * @example + * + * var addies, wanted, results; + * + * // Create your address book + * addies = new mw.Map(); + * + * // This data could be coming from an external source (eg. API/AJAX) + * addies.set( { + * 'John Doe' : '10 Wall Street, New York, USA', + * 'Jane Jackson' : '21 Oxford St, London, UK', + * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL' + * } ); + * + * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson']; + * + * // You can detect missing keys first + * if ( !addies.exists( wanted ) ) { + * // One or more are missing (in this case: "George Johnson") + * mw.log( 'One or more names were not found in your address book' ); + * } + * + * // Or just let it give you what it can + * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' ); + * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK" + * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US" + * + * @class mw.Map + * + * @constructor + * @param {boolean} [global=false] Whether to store the values in the global window * object or a exclusively in the object property 'values'. - * @return Map */ function Map( global ) { this.values = global === true ? window : {}; @@ -33,32 +88,32 @@ var mw = ( function ( $, undefined ) { * * If called with no arguments, all values will be returned. * - * @param selection mixed String key or array of keys to get values for. - * @param fallback mixed Value to use in case key(s) do not exist (optional). + * @param {string|Array} selection String key or array of keys to get values for. + * @param {Mixed} [fallback] Value to use in case key(s) do not exist. * @return mixed If selection was a string returns the value or null, * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as * objects are always passed by reference in JavaScript!). - * @return Values as a string or object, null if invalid/inexistant. + * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. */ get: function ( selection, fallback ) { var results, i; + // If we only do this in the `return` block, it'll fail for the + // call to get() from the mutli-selection block. + fallback = arguments.length > 1 ? fallback : null; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; - for ( i = 0; i < selection.length; i += 1 ) { + for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; - } - return null; + if ( !hasOwn.call( this.values, selection ) ) { + return fallback; } return this.values[selection]; } @@ -74,8 +129,8 @@ var mw = ( function ( $, undefined ) { /** * Sets one or multiple key/value pairs. * - * @param selection {mixed} String key or array of keys to set values for. - * @param value {mixed} Value to set (optional, only in use when key is a string) + * @param {string|Object} selection String key to set value for, or object mapping keys to values. + * @param {Mixed} [value] Value to set (optional, only in use when key is a string) * @return {Boolean} This returns true on success, false on failure. */ set: function ( selection, value ) { @@ -87,7 +142,7 @@ var mw = ( function ( $, undefined ) { } return true; } - if ( typeof selection === 'string' && value !== undefined ) { + if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } @@ -97,37 +152,40 @@ var mw = ( function ( $, undefined ) { /** * Checks if one or multiple keys exist. * - * @param selection {mixed} String key or array of keys to check - * @return {Boolean} Existence of key(s) + * @param {Mixed} selection String key or array of keys to check + * @return {boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { - for ( s = 0; s < selection.length; s += 1 ) { - if ( this.values[selection[s]] === undefined ) { + for ( s = 0; s < selection.length; s++ ) { + if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } - return this.values[selection] !== undefined; + return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** - * Message + * Object constructor for messages. + * + * Similar to the Message class in MediaWiki PHP. + * + * Format defaults to 'text'. * - * Object constructor for messages, - * similar to the Message class in MediaWiki PHP. + * @class mw.Message * - * @param map Map Instance of mw.Map - * @param key String - * @param parameters Array - * @return Message + * @constructor + * @param {mw.Map} map Message storage + * @param {string} key + * @param {Array} [parameters] */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -137,8 +195,11 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** * Simple message parser, does $N replacement and nothing else. + * * This may be overridden to provide a more complex message parser. * + * The primary override is in mediawiki.jqueryMsg. + * * This function will not be called for nonexistent messages. */ parser: function () { @@ -152,8 +213,8 @@ var mw = ( function ( $, undefined ) { /** * Appends (does not replace) parameters for replacement to the .parameters property. * - * @param parameters Array - * @return Message + * @param {Array} parameters + * @chainable */ params: function ( parameters ) { var i; @@ -166,25 +227,21 @@ var mw = ( function ( $, undefined ) { /** * Converts message object to it's string form based on the state of format. * - * @return string Message as a string in the current form or <key> if key does not exist. + * @return {string} Message as a string in the current form or `<key>` if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use <key> as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + if ( this.format === 'escaped' || this.format === 'parse' ) { + // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } - if ( this.format === 'plain' ) { - // @todo FIXME: Although not applicable to core Message, - // Plugins like jQueryMsg should be able to distinguish - // between 'plain' (only variable replacement and plural/gender) - // and actually parsing wikitext to HTML. + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,15 +250,16 @@ var mw = ( function ( $, undefined ) { text = mw.html.escape( text ); } - if ( this.format === 'parse' ) { - text = this.parser(); - } - return text; }, /** - * Changes format to parse and converts message to string + * Changes format to 'parse' and converts message to string + * + * If jqueryMsg is loaded, this parses the message text from wikitext + * (where supported) to HTML + * + * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ @@ -211,7 +269,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * Changes format to 'plain' and converts message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. * * @return {string} String form of plain message */ @@ -221,7 +282,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * Changes format to 'text' and converts message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain. + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Changes the format to 'escaped' and converts message to string + * + * This is equivalent to using the 'text' format (see text method), then + * HTML-escaping the output. * * @return {string} String form of html escaped message */ @@ -233,7 +310,8 @@ var mw = ( function ( $, undefined ) { /** * Checks if message exists * - * @return {string} String form of parsed message + * @see mw.Map#exists + * @return {boolean} */ exists: function () { return this.map.exists( this.key ); @@ -244,82 +322,98 @@ var mw = ( function ( $, undefined ) { /* Public Members */ /** - * Dummy function which in debug mode can be replaced with a function that - * emulates console.log in console-less environments. + * Dummy placeholder for {@link mw.log} + * @method */ - log: function () { }, + log: ( function () { + var log = function () {}; + log.warn = function () {}; + log.deprecate = function ( obj, key, val ) { + obj[key] = val; + }; + return log; + }() ), - /** - * @var constructor Make the Map constructor publicly available. - */ + // Make the Map constructor publicly available. Map: Map, - /** - * @var constructor Make the Message constructor publicly available. - */ + // Make the Message constructor publicly available. Message: Message, /** - * List of configuration values + * Map of configuration values + * + * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config) + * on MediaWiki.org. * - * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If $wgLegacyJavaScriptGlobals is true, this Map will have its values - * in the global window object. + * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the + * global window object. + * + * @property {mw.Map} config */ + // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`. config: null, /** - * @var object - * * Empty object that plugins can be installed in. + * @property */ libs: {}, - /* Extension points */ - + /** + * Access container for deprecated functionality that can be moved from + * from their legacy location and attached to this object (e.g. a global + * function that is deprecated and as stop-gap can be exposed through here). + * + * This was reserved for future use but never ended up being used. + * + * @deprecated since 1.22: Let deprecated identifiers keep their original name + * and use mw.log#deprecate to create an access container for tracking. + * @property + */ legacy: {}, /** * Localization system + * @property {mw.Map} */ messages: new Map(), /* Public Methods */ /** - * Gets a message object, similar to wfMessage() + * Get a message object. + * + * Similar to wfMessage() in MediaWiki PHP. * - * @param key string Key of message to get - * @param parameter_1 mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {mw.Message} */ - message: function ( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = slice.call( arguments ); - parameters.shift(); - } else { - parameters = []; - } + message: function ( key ) { + // Variadic arguments + var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** - * Gets a message string, similar to wfMessage() + * Get a message string using 'text' format. * - * @param key string Key of message to get - * @param parameters mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return String. + * Similar to wfMsg() in MediaWiki PHP. + * + * @see mw.Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {string} */ - msg: function ( /* key, parameter_1, parameter_2, .. */ ) { + msg: function () { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader + * @class mw.loader + * @singleton */ loader: ( function () { @@ -338,29 +432,32 @@ var mw = ( function ( $, undefined ) { * mw.loader.implement. * * Format: - * { - * 'moduleName': { - * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} - * 'group': 'somegroup', (or) null, - * 'source': 'local', 'someforeignwiki', (or) null - * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, - * } - * } + * { + * 'moduleName': { + * 'version': ############## (unix timestamp), + * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} + * 'group': 'somegroup', (or) null, + * 'source': 'local', 'someforeignwiki', (or) null + * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing' + * 'script': ..., + * 'style': ..., + * 'messages': { 'key': 'value' }, + * } + * } + * + * @property + * @private */ var registry = {}, - /** - * Mapping of sources, keyed by source-id, values are objects. - * Format: - * { - * 'sourceId': { - * 'loadScript': 'http://foo.bar/w/load.php' - * } - * } - */ + // + // Mapping of sources, keyed by source-id, values are objects. + // Format: + // { + // 'sourceId': { + // 'loadScript': 'http://foo.bar/w/load.php' + // } + // } + // sources = {}, // List of modules which will be loaded as when ready batch = [], @@ -369,7 +466,11 @@ var mw = ( function ( $, undefined ) { // List of callback functions waiting for modules to be ready to be called jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! - $marker = null; + $marker = null, + // Buffer for addEmbeddedCSS. + cssBuffer = '', + // Callbacks for addEmbeddedCSS. + cssCallbacks = $.Callbacks(); /* Private methods */ @@ -392,12 +493,13 @@ var mw = ( function ( $, undefined ) { /** * Create a new style tag and add it to the DOM. * - * @param text String: CSS text - * @param nextnode mixed: [optional] An Element or jQuery object for an element where - * the style tag should be inserted before. Otherwise appended to the <head>. - * @return HTMLStyleElement + * @private + * @param {string} text CSS text + * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be + * inserted before. Otherwise it will be appended to `<head>`. + * @return {HTMLElement} Reference to the created `<style>` element. */ - function addStyleTag( text, nextnode ) { + function newStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); // Insert into document before setting cssText (bug 33305) if ( nextnode ) { @@ -429,76 +531,112 @@ var mw = ( function ( $, undefined ) { } /** - * Checks if certain cssText is safe to append to - * a stylesheet. + * Checks whether it is safe to add this css to a stylesheet. * - * Right now it only makes sure that cssText containing @import - * rules will end up in a new stylesheet (as those only work when - * placed at the start of a stylesheet; bug 35562). - * This could later be extended to take care of other bugs, such as - * the IE cssRules limit - not the same as the IE styleSheets limit). + * @private + * @param {string} cssText + * @return {boolean} False if a new one must be created. */ - function canExpandStylesheetWith( $style, cssText ) { + function canExpandStylesheetWith( cssText ) { + // Makes sure that cssText containing `@import` + // rules will end up in a new stylesheet (as those only work when + // placed at the start of a stylesheet; bug 35562). return cssText.indexOf( '@import' ) === -1; } - function addEmbeddedCSS( cssText ) { + /** + * Add a bit of CSS text to the current browser page. + * + * The CSS will be appended to an existing ResourceLoader-created `<style>` tag + * or create a new one based on whether the given `cssText` is safe for extension. + * + * @param {string} [cssText=cssBuffer] If called without cssText, + * the internal buffer will be inserted instead. + * @param {Function} [callback] + */ + function addEmbeddedCSS( cssText, callback ) { var $style, styleEl; - $style = getMarker().prev(); - // Re-use <style> tags if possible, this to try to stay - // under the IE stylesheet limit (bug 31676). - // Also verify that the the element before Marker actually is one - // that came from ResourceLoader, and not a style tag that some - // other script inserted before our marker, or, more importantly, - // it may not be a style tag at all (could be <meta> or <script>). - if ( - $style.data( 'ResourceLoaderDynamicStyleTag' ) === true && - canExpandStylesheetWith( $style, cssText ) - ) { - // There's already a dynamic <style> tag present and - // canExpandStylesheetWith() gave a green light to append more to it. - styleEl = $style.get( 0 ); - if ( styleEl.styleSheet ) { - try { - styleEl.styleSheet.cssText += cssText; // IE - } catch ( e ) { - log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e ); - } - } else { - styleEl.appendChild( document.createTextNode( String( cssText ) ) ); + + if ( callback ) { + cssCallbacks.add( callback ); + } + + // Yield once before inserting the <style> tag. There are likely + // more calls coming up which we can combine this way. + // Appending a stylesheet and waiting for the browser to repaint + // is fairly expensive, this reduces it (bug 45810) + if ( cssText ) { + // Be careful not to extend the buffer with css that needs a new stylesheet + if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) { + // Linebreak for somewhat distinguishable sections + // (the rl-cachekey comment separating each) + cssBuffer += '\n' + cssText; + // TODO: Use requestAnimationFrame in the future which will + // perform even better by not injecting styles while the browser + // is paiting. + setTimeout( function () { + // Can't pass addEmbeddedCSS to setTimeout directly because Firefox + // (below version 13) has the non-standard behaviour of passing a + // numerical "lateness" value as first argument to this callback + // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/ + addEmbeddedCSS(); + } ); + return; } + + // This is a delayed call and we got a buffer still + } else if ( cssBuffer ) { + cssText = cssBuffer; + cssBuffer = ''; } else { - $( addStyleTag( cssText, getMarker() ) ) - .data( 'ResourceLoaderDynamicStyleTag', true ); + // This is a delayed call, but buffer is already cleared by + // another delayed call. + return; } - } - function compare( a, b ) { - var i; - if ( a.length !== b.length ) { - return false; - } - for ( i = 0; i < b.length; i += 1 ) { - if ( $.isArray( a[i] ) ) { - if ( !compare( a[i], b[i] ) ) { - return false; + // By default, always create a new <style>. Appending text + // to a <style> tag means the contents have to be re-parsed (bug 45810). + // Except, of course, in IE below 9, in there we default to + // re-using and appending to a <style> tag due to the + // IE stylesheet limit (bug 31676). + if ( 'documentMode' in document && document.documentMode <= 9 ) { + + $style = getMarker().prev(); + // Verify that the the element before Marker actually is a + // <style> tag and one that came from ResourceLoader + // (not some other style tag or even a `<meta>` or `<script>`). + if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { + // There's already a dynamic <style> tag present and + // canExpandStylesheetWith() gave a green light to append more to it. + styleEl = $style.get( 0 ); + if ( styleEl.styleSheet ) { + try { + styleEl.styleSheet.cssText += cssText; // IE + } catch ( e ) { + log( 'addEmbeddedCSS fail', e ); + } + } else { + styleEl.appendChild( document.createTextNode( String( cssText ) ) ); } - } - if ( a[i] !== b[i] ) { - return false; + cssCallbacks.fire().empty(); + return; } } - return true; + + $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); + + cssCallbacks.fire().empty(); } /** * Generates an ISO8601 "basic" string from a UNIX timestamp + * @private */ function formatVersionNumber( timestamp ) { - var pad = function ( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - }, - d = new Date(); + var d = new Date(); + function pad( a, b, c ) { + return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); + } d.setTime( timestamp * 1000 ); return [ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', @@ -509,15 +647,16 @@ var mw = ( function ( $, undefined ) { /** * Resolves dependencies and detects circular references. * - * @param module String Name of the top-level module whose dependencies shall be + * @private + * @param {string} module Name of the top-level module whose dependencies shall be * resolved and sorted. - * @param resolved Array Returns a topological sort of the given module and its + * @param {Array} resolved Returns a topological sort of the given module and its * dependencies, such that later modules depend on earlier modules. The array * contains the module names. If the array contains already some module names, * this function appends its result to the pre-existing array. - * @param unresolved Object [optional] Hash used to track the current dependency + * @param {Object} [unresolved] Hash used to track the current dependency * chain; used to report loops in the dependency graph. - * @throws Error if any unregistered module or a dependency loop is encountered + * @throws {Error} If any unregistered module or a dependency loop is encountered */ function sortDependencies( module, resolved, unresolved ) { var n, deps, len; @@ -566,9 +705,10 @@ var mw = ( function ( $, undefined ) { * Gets a list of module names that a module depends on in their proper dependency * order. * - * @param module string module name or array of string module names - * @return list of dependencies, including 'module'. - * @throws Error if circular reference is detected + * @private + * @param {string} module Module name or array of string module names + * @return {Array} list of dependencies, including 'module'. + * @throws {Error} If circular reference is detected */ function resolve( module ) { var m, resolved; @@ -597,10 +737,11 @@ var mw = ( function ( $, undefined ) { * One can also filter for 'unregistered', which will return the * modules names that don't have a registry entry. * - * @param states string or array of strings of module states to filter by - * @param modules array list of module names to filter (optional, by default the entire + * @private + * @param {string|string[]} states Module states to filter by + * @param {Array} [modules] List of module names to filter (optional, by default the entire * registry is used) - * @return array list of filtered module names + * @return {Array} List of filtered module names */ function filter( states, modules ) { var list, module, s, m; @@ -642,44 +783,22 @@ var mw = ( function ( $, undefined ) { * Determine whether all dependencies are in state 'ready', which means we may * execute the module or job now. * - * @param dependencies Array dependencies (module names) to be checked. - * - * @return Boolean true if all dependencies are in state 'ready', false otherwise + * @private + * @param {Array} dependencies Dependencies (module names) to be checked. + * @return {boolean} True if all dependencies are in state 'ready', false otherwise */ function allReady( dependencies ) { return filter( 'ready', dependencies ).length === dependencies.length; } /** - * Log a message to window.console, if possible. Useful to force logging of some - * errors that are otherwise hard to detect (I.e., this logs also in production mode). - * Gets console references in each invocation, so that delayed debugging tools work - * fine. No need for optimization here, which would only result in losing logs. - * - * @param msg String text for the log entry. - * @param e Error [optional] to also log. - */ - function log( msg, e ) { - var console = window.console; - if ( console && console.log ) { - console.log( msg ); - // If we have an exception object, log it through .error() to trigger - // proper stacktraces in browsers that support it. There are no (known) - // browsers that don't support .error(), that do support .log() and - // have useful exception handling through .log(). - if ( e && console.error ) { - console.error( e ); - } - } - } - - /** * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs * and modules that depend upon this module. if the given module failed, propagate the 'error' * state up the dependency tree; otherwise, execute all jobs/modules that now have all their * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any. * - * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'. + * @private + * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. */ function handlePending( module ) { var j, job, hasErrors, m, stateChange; @@ -712,22 +831,18 @@ var mw = ( function ( $, undefined ) { j -= 1; try { if ( hasErrors ) { - throw new Error ("Module " + module + " failed."); + if ( $.isFunction( job.error ) ) { + job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] ); + } } else { if ( $.isFunction( job.ready ) ) { job.ready(); } } } catch ( e ) { - if ( $.isFunction( job.error ) ) { - try { - job.error( e, [module] ); - } catch ( ex ) { - // A user-defined operation raised an exception. Swallow to protect - // our state machine! - log( 'Exception thrown by job.error()', ex ); - } - } + // A user-defined callback raised an exception. + // Swallow it to protect our state machine! + log( 'Exception thrown by job.error', e ); } } } @@ -747,27 +862,32 @@ var mw = ( function ( $, undefined ) { * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, * depending on whether document-ready has occurred yet and whether we are in async mode. * - * @param src String: URL to script, will be used as the src attribute in the script tag - * @param callback Function: Optional callback which will be run when the script is done + * @private + * @param {string} src URL to script, will be used as the src attribute in the script tag + * @param {Function} [callback] Callback which will be run when the script is done */ function addScript( src, callback, async ) { /*jshint evil:true */ - var script, head, - done = false; + var script, head, done; // Using isReady directly instead of storing it locally from // a $.fn.ready callback (bug 31895). if ( $.isReady || async ) { - // jQuery's getScript method is NOT better than doing this the old-fashioned way - // because jQuery will eval the script's code, and errors will not have sane - // line numbers. + // Can't use jQuery.getScript because that only uses <script> for cross-domain, + // it uses XHR and eval for same-domain scripts, which we don't want because it + // messes up line numbers. + // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js) + + // IE-safe way of getting the <head>. document.head isn't supported + // in old IE, and doesn't work when in the <head>. + done = false; + head = document.getElementsByTagName( 'head' )[0] || document.body; + script = document.createElement( 'script' ); - script.setAttribute( 'src', src ); - script.setAttribute( 'type', 'text/javascript' ); + script.async = true; + script.src = src; if ( $.isFunction( callback ) ) { - // Attach handlers for all browsers (based on jQuery.ajax) script.onload = script.onreadystatechange = function () { - if ( !done && ( @@ -775,24 +895,20 @@ var mw = ( function ( $, undefined ) { || /loaded|complete/.test( script.readyState ) ) ) { - done = true; - callback(); + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; - // Handle memory leak in IE. This seems to fail in - // IE7 sometimes (Permission Denied error when - // accessing script.parentNode) so wrap it in - // a try catch. - try { - script.onload = script.onreadystatechange = null; - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = undefined; - } catch ( e ) { } + // Detach the element from the document + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the element from javascript + script = undefined; + + callback(); } }; } @@ -800,20 +916,17 @@ var mw = ( function ( $, undefined ) { if ( window.opera ) { // Appending to the <head> blocks rendering completely in Opera, // so append to the <body> after document ready. This means the - // scripts only start loading after the document has been rendered, + // scripts only start loading after the document has been rendered, // but so be it. Opera users don't deserve faster web pages if their - // browser makes it impossible - $( function () { document.body.appendChild( script ); } ); + // browser makes it impossible. + $( function () { + document.body.appendChild( script ); + } ); } else { - // IE-safe way of getting the <head> . document.documentElement.head doesn't - // work in scripts that run in the <head> - head = document.getElementsByTagName( 'head' )[0]; - ( document.body || head ).appendChild( script ); + head.appendChild( script ); } } else { - document.write( mw.html.element( - 'script', { 'type': 'text/javascript', 'src': src }, '' - ) ); + document.write( mw.html.element( 'script', { 'src': src }, '' ) ); if ( $.isFunction( callback ) ) { // Document.write is synchronous, so this is called when it's done // FIXME: that's a lie. doc.write isn't actually synchronous @@ -825,10 +938,12 @@ var mw = ( function ( $, undefined ) { /** * Executes a loaded module, making it ready to use * - * @param module string module name to execute + * @private + * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, script, markModuleReady, nestedAddScript; + var key, value, media, i, urls, cssHandle, checkCssHandles, + cssHandlesRegistered = false; if ( registry[module] === undefined ) { throw new Error( 'Module has not been registered yet: ' + module ); @@ -837,12 +952,13 @@ var mw = ( function ( $, undefined ) { } else if ( registry[module].state === 'loading' ) { throw new Error( 'Module has not completed loading yet: ' + module ); } else if ( registry[module].state === 'ready' ) { - throw new Error( 'Module has already been loaded: ' + module ); + throw new Error( 'Module has already been executed: ' + module ); } /** * Define loop-function here for efficiency * and to avoid re-using badly scoped variables. + * @ignore */ function addLink( media, url ) { var el = document.createElement( 'link' ); @@ -854,6 +970,87 @@ var mw = ( function ( $, undefined ) { el.href = url; } + function runScript() { + var script, markModuleReady, nestedAddScript; + try { + script = registry[module].script; + markModuleReady = function () { + registry[module].state = 'ready'; + handlePending( module ); + }; + nestedAddScript = function ( arr, callback, async, i ) { + // Recursively call addScript() in its own callback + // for each element of arr. + if ( i >= arr.length ) { + // We're at the end of the array + callback(); + return; + } + + addScript( arr[i], function () { + nestedAddScript( arr, callback, async, i + 1 ); + }, async ); + }; + + if ( $.isArray( script ) ) { + nestedAddScript( script, markModuleReady, registry[module].async, 0 ); + } else if ( $.isFunction( script ) ) { + registry[module].state = 'ready'; + script( $ ); + handlePending( module ); + } + } catch ( e ) { + // This needs to NOT use mw.log because these errors are common in production mode + // and not in debug mode, such as when a symbol that should be global isn't exported + log( 'Exception thrown by ' + module, e ); + registry[module].state = 'error'; + handlePending( module ); + } + } + + // This used to be inside runScript, but since that is now fired asychronously + // (after CSS is loaded) we need to set it here right away. It is crucial that + // when execute() is called this is set synchronously, otherwise modules will get + // executed multiple times as the registry will state that it isn't loading yet. + registry[module].state = 'loading'; + + // Add localizations to message system + if ( $.isPlainObject( registry[module].messages ) ) { + mw.messages.set( registry[module].messages ); + } + + if ( $.isReady || registry[module].async ) { + // Make sure we don't run the scripts until all (potentially asynchronous) + // stylesheet insertions have completed. + ( function () { + var pending = 0; + checkCssHandles = function () { + // cssHandlesRegistered ensures we don't take off too soon, e.g. when + // one of the cssHandles is fired while we're still creating more handles. + if ( cssHandlesRegistered && pending === 0 && runScript ) { + runScript(); + runScript = undefined; // Revoke + } + }; + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if (check) { + pending--; + check(); + check = undefined; // Revoke + } + }; + }; + }() ); + } else { + // We are in blocking mode, and so we can't afford to wait for CSS + cssHandle = function () {}; + // Run immediately + checkCssHandles = runScript; + } + // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } // * back-compat: { <media>: [url, ..] } @@ -872,7 +1069,7 @@ var mw = ( function ( $, undefined ) { // Strings are pre-wrapped in "@media". The media-type was just "" // (because it had to be set to something). // This is one of the reasons why this format is no longer used. - addEmbeddedCSS( value ); + addEmbeddedCSS( value, cssHandle() ); } else { // back-compat: { <media>: [url, ..] } media = key; @@ -889,7 +1086,7 @@ var mw = ( function ( $, undefined ) { addLink( media, value[i] ); } else if ( key === 'css' ) { // { "css": [css, ..] } - addEmbeddedCSS( value[i] ); + addEmbeddedCSS( value[i], cssHandle() ); } } // Not an array, but a regular object @@ -906,61 +1103,24 @@ var mw = ( function ( $, undefined ) { } } - // Add localizations to message system - if ( $.isPlainObject( registry[module].messages ) ) { - mw.messages.set( registry[module].messages ); - } - - // Execute script - try { - script = registry[module].script; - markModuleReady = function () { - registry[module].state = 'ready'; - handlePending( module ); - }; - nestedAddScript = function ( arr, callback, async, i ) { - // Recursively call addScript() in its own callback - // for each element of arr. - if ( i >= arr.length ) { - // We're at the end of the array - callback(); - return; - } - - addScript( arr[i], function () { - nestedAddScript( arr, callback, async, i + 1 ); - }, async ); - }; - - if ( $.isArray( script ) ) { - registry[module].state = 'loading'; - nestedAddScript( script, markModuleReady, registry[module].async, 0 ); - } else if ( $.isFunction( script ) ) { - registry[module].state = 'ready'; - script( $ ); - handlePending( module ); - } - } catch ( e ) { - // This needs to NOT use mw.log because these errors are common in production mode - // and not in debug mode, such as when a symbol that should be global isn't exported - log( 'Exception thrown by ' + module + ': ' + e.message, e ); - registry[module].state = 'error'; - handlePending( module ); - } + // Kick off. + cssHandlesRegistered = true; + checkCssHandles(); } /** * Adds a dependencies to the queue with optional callbacks to be run * when the dependencies are ready or fail * - * @param dependencies string module name or array of string module names - * @param ready function callback to execute when all dependencies are ready - * @param error function callback to execute when any dependency fails - * @param async (optional) If true, load modules asynchronously even if - * document ready has not yet occurred + * @private + * @param {string|string[]} dependencies Module name or array of string module names + * @param {Function} [ready] Callback to execute when all dependencies are ready + * @param {Function} [error] Callback to execute when any dependency fails + * @param {boolean} [async] If true, load modules asynchronously even if + * document ready has not yet occurred. */ function request( dependencies, ready, error, async ) { - var regItemDeps, regItemDepLen, n; + var n; // Allow calling by single module name if ( typeof dependencies === 'string' ) { @@ -1012,6 +1172,7 @@ var mw = ( function ( $, undefined ) { /** * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } * to a query string of the form foo.bar,baz|bar.baz,quux + * @private */ function buildModulesString( moduleMap ) { var arr = [], p, prefix; @@ -1025,14 +1186,15 @@ var mw = ( function ( $, undefined ) { /** * Asynchronously append a script tag to the end of the body * that invokes load.php - * @param moduleMap {Object}: Module map, see buildModulesString() - * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request - * @param sourceLoadScript {String}: URL of load.php - * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred + * @private + * @param {Object} moduleMap Module map, see #buildModulesString + * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request + * @param {string} sourceLoadScript URL of load.php + * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred */ function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { var request = $.extend( - { 'modules': buildModulesString( moduleMap ) }, + { modules: buildModulesString( moduleMap ) }, currReqBase ); request = sortQuery( request ); @@ -1043,10 +1205,24 @@ var mw = ( function ( $, undefined ) { /* Public Methods */ return { - addStyleTag: addStyleTag, + /** + * The module registry is exposed as an aid for debugging and inspecting page + * state; it is not a public interface for modifying the registry. + * + * @see #registry + * @property + * @private + */ + moduleRegistry: registry, /** - * Requests dependencies from server, loading and executing when things when ready. + * @inheritdoc #newStyleTag + * @method + */ + addStyleTag: newStyleTag, + + /** + * Batch-request queued dependencies from the server. */ work: function () { var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, @@ -1127,9 +1303,9 @@ var mw = ( function ( $, undefined ) { } } - currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase ); // For user modules append a user name to the request. - if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) { + if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) { currReqBase.user = mw.config.get( 'wgUserName' ); } currReqBaseLength = $.param( currReqBase ).length; @@ -1183,10 +1359,10 @@ var mw = ( function ( $, undefined ) { /** * Register a source. * - * @param id {String}: Short lowercase a-Z string representing a source, only used internally. - * @param props {Object}: Object containing only the loadScript property which is a url to - * the load.php location of the source. - * @return {Boolean} + * @param {string} id Short lowercase a-Z string representing a source, only used internally. + * @param {Object} props Object containing only the loadScript property which is a url to + * the load.php location of the source. + * @return {boolean} */ addSource: function ( id, props ) { var source; @@ -1208,15 +1384,15 @@ var mw = ( function ( $, undefined ) { }, /** - * Registers a module, letting the system know about it and its + * Register a module, letting the system know about it and its * properties. Startup modules contain calls to this function. * - * @param module {String}: Module name - * @param version {Number}: Module version number as a timestamp (falls backs to 0) - * @param dependencies {String|Array|Function}: One string or array of strings of module + * @param {string} module Module name + * @param {number} version Module version number as a timestamp (falls backs to 0) + * @param {string|Array|Function} dependencies One string or array of strings of module * names on which this module depends, or a function that returns that array. - * @param group {String}: Group which the module is in (optional, defaults to null) - * @param source {String}: Name of the source. Defaults to local. + * @param {string} [group=null] Group which the module is in + * @param {string} [source='local'] Name of the source */ register: function ( module, version, dependencies, group, source ) { var m; @@ -1242,15 +1418,15 @@ var mw = ( function ( $, undefined ) { } // List the module as registered registry[module] = { - 'version': version !== undefined ? parseInt( version, 10 ) : 0, - 'dependencies': [], - 'group': typeof group === 'string' ? group : null, - 'source': typeof source === 'string' ? source: 'local', - 'state': 'registered' + version: version !== undefined ? parseInt( version, 10 ) : 0, + dependencies: [], + group: typeof group === 'string' ? group : null, + source: typeof source === 'string' ? source: 'local', + state: 'registered' }; if ( typeof dependencies === 'string' ) { // Allow dependencies to be given as a single module name - registry[module].dependencies = [dependencies]; + registry[module].dependencies = [ dependencies ]; } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { // Allow dependencies to be given as an array of module names // or a function which returns an array @@ -1259,26 +1435,27 @@ var mw = ( function ( $, undefined ) { }, /** - * Implements a module, giving the system a course of action to take - * upon loading. Results of a request for one or more modules contain - * calls to this function. + * Implement a module given the components that make up the module. + * + * When #load or #using requests one or more modules, the server + * response contain calls to this function. * * All arguments are required. * - * @param {String} module Name of module + * @param {string} module Name of module * @param {Function|Array} script Function with module code or Array of URLs to - * be used as the src attribute of a new <script> tag. + * be used as the src attribute of a new `<script>` tag. * @param {Object} style Should follow one of the following patterns: - * { "css": [css, ..] } - * { "url": { <media>: [url, ..] } } - * And for backwards compatibility (needs to be supported forever due to caching): - * { <media>: css } - * { <media>: [url, ..] } + * { "css": [css, ..] } + * { "url": { <media>: [url, ..] } } + * And for backwards compatibility (needs to be supported forever due to caching): + * { <media>: css } + * { <media>: [url, ..] } * - * The reason css strings are not concatenated anymore is bug 31676. We now check - * whether it's safe to extend the stylesheet (see canExpandStylesheetWith). + * The reason css strings are not concatenated anymore is bug 31676. We now check + * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith). * - * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set + * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}. */ implement: function ( module, script, style, msgs ) { // Validate input @@ -1316,12 +1493,12 @@ var mw = ( function ( $, undefined ) { }, /** - * Executes a function as soon as one or more required modules are ready + * Execute a function as soon as one or more required modules are ready. * - * @param dependencies {String|Array} Module name or array of modules names the callback + * @param {string|Array} dependencies Module name or array of modules names the callback * dependends on to be ready before executing - * @param ready {Function} callback to execute when all dependencies are ready (optional) - * @param error {Function} callback to execute when if dependencies have a errors (optional) + * @param {Function} [ready] callback to execute when all dependencies are ready + * @param {Function} [error] callback to execute when if dependencies have a errors */ using: function ( dependencies, ready, error ) { var tod = typeof dependencies; @@ -1331,7 +1508,7 @@ var mw = ( function ( $, undefined ) { } // Allow calling with a single dependency as a string if ( tod === 'string' ) { - dependencies = [dependencies]; + dependencies = [ dependencies ]; } // Resolve entire dependency map dependencies = resolve( dependencies ); @@ -1353,20 +1530,20 @@ var mw = ( function ( $, undefined ) { }, /** - * Loads an external script or one or more modules for future use + * Load an external script or one or more modules. * - * @param modules {mixed} Either the name of a module, array of modules, + * @param {string|Array} modules Either the name of a module, array of modules, * or a URL of an external script or style - * @param type {String} mime-type to use if calling with a URL of an + * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an * external script or style; acceptable values are "text/css" and * "text/javascript"; if no type is provided, text/javascript is assumed. - * @param async {Boolean} (optional) If true, load modules asynchronously - * even if document ready has not yet occurred. If false (default), - * block before document ready and load async after. If not set, true will - * be assumed if loading a URL, and false will be assumed otherwise. + * @param {boolean} [async] If true, load modules asynchronously + * even if document ready has not yet occurred. If false, block before + * document ready and load async after. If not set, true will be + * assumed if loading a URL, and false will be assumed otherwise. */ load: function ( modules, type, async ) { - var filtered, m, module; + var filtered, m, module, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1381,11 +1558,13 @@ var mw = ( function ( $, undefined ) { async = true; } if ( type === 'text/css' ) { - $( 'head' ).append( $( '<link>', { - rel: 'stylesheet', - type: 'text/css', - href: modules - } ) ); + // IE7-8 throws security warnings when inserting a <link> tag + // with a protocol-relative URL set though attributes (instead of + // properties) - when on HTTPS. See also bug #. + l = document.createElement( 'link' ); + l.rel = 'stylesheet'; + l.href = modules; + $( 'head' ).append( l ); return; } if ( type === 'text/javascript' || type === undefined ) { @@ -1396,7 +1575,7 @@ var mw = ( function ( $, undefined ) { throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type ); } // Called with single module - modules = [modules]; + modules = [ modules ]; } // Filter out undefined modules, otherwise resolve() will throw @@ -1427,14 +1606,14 @@ var mw = ( function ( $, undefined ) { return; } // Since some modules are not yet ready, queue up a request. - request( filtered, null, null, async ); + request( filtered, undefined, undefined, async ); }, /** - * Changes the state of a module + * Change the state of one or more modules. * - * @param module {String|Object} module name or object of module name/state pairs - * @param state {String} state name + * @param {string|Object} module module name or object of module name/state pairs + * @param {string} state state name */ state: function ( module, state ) { var m; @@ -1448,7 +1627,7 @@ var mw = ( function ( $, undefined ) { if ( registry[module] === undefined ) { mw.loader.register( module ); } - if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1 + if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1 && registry[module].state !== state ) { // Make sure pending modules depending on this one get executed if their // dependencies are now fulfilled! @@ -1460,9 +1639,9 @@ var mw = ( function ( $, undefined ) { }, /** - * Gets the version of a module + * Get the version of a module. * - * @param module string name of module to get version for + * @param {string} module Name of module to get version for */ getVersion: function ( module ) { if ( registry[module] !== undefined && registry[module].version !== undefined ) { @@ -1472,16 +1651,17 @@ var mw = ( function ( $, undefined ) { }, /** - * @deprecated since 1.18 use mw.loader.getVersion() instead + * @inheritdoc #getVersion + * @deprecated since 1.18 use #getVersion instead */ version: function () { return mw.loader.getVersion.apply( mw.loader, arguments ); }, /** - * Gets the state of a module + * Get the state of a module. * - * @param module string name of module to get state for + * @param {string} module name of module to get state for */ getState: function ( module ) { if ( registry[module] !== undefined && registry[module].state !== undefined ) { @@ -1502,19 +1682,52 @@ var mw = ( function ( $, undefined ) { }, /** - * For backwards-compatibility with Squid-cached pages. Loads mw.user + * Load the `mediawiki.user` module. + * + * For backwards-compatibility with cached pages from before 2013 where: + * + * - the `mediawiki.user` module didn't exist yet + * - `mw.user` was still part of mediawiki.js + * - `mw.loader.go` still existed and called after `mw.loader.load()` */ go: function () { mw.loader.load( 'mediawiki.user' ); + }, + + /** + * @inheritdoc mw.inspect#runReports + * @method + */ + inspect: function () { + var args = slice.call( arguments ); + mw.loader.using( 'mediawiki.inspect', function () { + mw.inspect.runReports.apply( mw.inspect, args ); + } ); } + }; }() ), - /** HTML construction helper functions */ + /** + * HTML construction helper functions + * + * @example + * + * var Html, output; + * + * Html = mw.html; + * output = Html.element( 'div', {}, new Html.Raw( + * Html.element( 'img', { src: '<' } ) + * ) ); + * mw.log( output ); // <div><img src="<"/></div> + * + * @class mw.html + * @singleton + */ html: ( function () { function escapeCallback( s ) { switch ( s ) { - case "'": + case '\'': return '''; case '"': return '"'; @@ -1530,46 +1743,24 @@ var mw = ( function ( $, undefined ) { return { /** * Escape a string for HTML. Converts special characters to HTML entities. - * @param s The string to escape + * @param {string} s The string to escape */ escape: function ( s ) { return s.replace( /['"<>&]/g, escapeCallback ); }, /** - * Wrapper object for raw HTML passed to mw.html.element(). - * @constructor - */ - Raw: function ( value ) { - this.value = value; - }, - - /** - * Wrapper object for CDATA element contents passed to mw.html.element() - * @constructor - */ - Cdata: function ( value ) { - this.value = value; - }, - - /** * Create an HTML element string, with safe escaping. * - * @param name The tag name. - * @param attrs An object with members mapping element names to values - * @param contents The contents of the element. May be either: + * @param {string} name The tag name. + * @param {Object} attrs An object with members mapping element names to values + * @param {Mixed} contents The contents of the element. May be either: * - string: The string is escaped. * - null or undefined: The short closing form is used, e.g. <br/>. * - this.Raw: The value attribute is included without escaping. * - this.Cdata: The value attribute is included, and an exception is * thrown if it contains an illegal ETAGO delimiter. * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 - * - * Example: - * var h = mw.html; - * return h.element( 'div', {}, - * new h.Raw( h.element( 'img', {src: '<'} ) ) ); - * Returns <div><img src="<"/></div> */ element: function ( name, attrs, contents ) { var v, attrName, s = '<' + name; @@ -1618,6 +1809,22 @@ var mw = ( function ( $, undefined ) { } s += '</' + name + '>'; return s; + }, + + /** + * Wrapper object for raw HTML passed to mw.html.element(). + * @class mw.html.Raw + */ + Raw: function ( value ) { + this.value = value; + }, + + /** + * Wrapper object for CDATA element contents passed to mw.html.element() + * @class mw.html.Cdata + */ + Cdata: function ( value ) { + this.value = value; } }; }() ), @@ -1626,7 +1833,87 @@ var mw = ( function ( $, undefined ) { user: { options: new Map(), tokens: new Map() - } + }, + + /** + * Registry and firing of events. + * + * MediaWiki has various interface components that are extended, enhanced + * or manipulated in some other way by extensions, gadgets and even + * in core itself. + * + * This framework helps streamlining the timing of when these other + * code paths fire their plugins (instead of using document-ready, + * which can and should be limited to firing only once). + * + * Features like navigating to other wiki pages, previewing an edit + * and editing itself – without a refresh – can then retrigger these + * hooks accordingly to ensure everything still works as expected. + * + * Example usage: + * + * mw.hook( 'wikipage.content' ).add( fn ).remove( fn ); + * mw.hook( 'wikipage.content' ).fire( $content ); + * + * Handlers can be added and fired for arbitrary event names at any time. The same + * event can be fired multiple times. The last run of an event is memorized + * (similar to `$(document).ready` and `$.Deferred().done`). + * This means if an event is fired, and a handler added afterwards, the added + * function will be fired right away with the last given event data. + * + * Like Deferreds and Promises, the mw.hook object is both detachable and chainable. + * Thus allowing flexible use and optimal maintainability and authority control. + * You can pass around the `add` and/or `fire` method to another piece of code + * without it having to know the event name (or `mw.hook` for that matter). + * + * var h = mw.hook( 'bar.ready' ); + * new mw.Foo( .. ).fetch( { callback: h.fire } ); + * + * Note: Events are documented with an underscore instead of a dot in the event + * name due to jsduck not supporting dots in that position. + * + * @class mw.hook + */ + hook: ( function () { + var lists = {}; + + /** + * Create an instance of mw.hook. + * + * @method hook + * @member mw + * @param {string} name Name of hook. + * @return {mw.hook} + */ + return function ( name ) { + var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) ); + + return { + /** + * Register a hook handler + * @param {Function...} handler Function to bind. + * @chainable + */ + add: list.add, + + /** + * Unregister a hook handler + * @param {Function...} handler Function to unbind. + * @chainable + */ + remove: list.remove, + + /** + * Run a hook. + * @param {Mixed...} data + * @chainable + */ + fire: function () { + return list.fireWith( null, slice.call( arguments ) ); + } + }; + }; + }() ) }; }( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js index 4ea1a881..75e4c961 100644 --- a/resources/mediawiki/mediawiki.log.js +++ b/resources/mediawiki/mediawiki.log.js @@ -1,4 +1,4 @@ -/** +/*! * Logger for MediaWiki javascript. * Implements the stub left by the main 'mediawiki' module. * @@ -9,15 +9,20 @@ ( function ( mw, $ ) { /** + * @class mw.log + * @singleton + */ + + /** * Logs a message to the console. * * In the case the browser does not have a console API, a console is created on-the-fly by appending - * a <div id="mw-log-console"> element to the bottom of the body and then appending this and future + * a `<div id="mw-log-console">` element to the bottom of the body and then appending this and future * messages to that, instead of the console. * - * @param {String} First in list of variadic messages to output to console. + * @param {string...} msg Messages to output to console. */ - mw.log = function ( /* logmsg, logmsg, */ ) { + mw.log = function () { // Turn arguments into an array var args = Array.prototype.slice.call( arguments ), // Allow log messages to use a configured prefix to identify the source window (ie. frame) @@ -41,7 +46,7 @@ ':' + ( d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds() ) + '.' + ( d.getMilliseconds() < 10 ? '00' + d.getMilliseconds() : ( d.getMilliseconds() < 100 ? '0' + d.getMilliseconds() : d.getMilliseconds() ) ), $log = $( '#mw-log-console' ); - + if ( !$log.length ) { $log = $( '<div id="mw-log-console"></div>' ).css( { overflow: 'auto', @@ -54,7 +59,7 @@ hovzer.update(); } $log.append( - $( '<div></div>' ) + $( '<div>' ) .css( { borderBottom: 'solid 1px #DDDDDD', fontSize: 'small', @@ -68,4 +73,54 @@ } ); }; + /** + * Write a message the console's warning channel. + * Also logs a stacktrace for easier debugging. + * Each action is silently ignored if the browser doesn't support it. + * + * @param {string...} msg Messages to output to console + */ + mw.log.warn = function () { + var console = window.console; + if ( console && console.warn ) { + console.warn.apply( console, arguments ); + if ( console.trace ) { + console.trace(); + } + } + }; + + /** + * Create a property in a host object that, when accessed, will produce + * a deprecation warning in the console with backtrace. + * + * @param {Object} obj Host object of deprecated property + * @param {string} key Name of property to create in `obj` + * @param {Mixed} val The value this property should return when accessed + * @param {string} [msg] Optional text to include in the deprecation message. + */ + mw.log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { + obj[key] = val; + } : function ( obj, key, val, msg ) { + msg = 'MWDeprecationWarning: Use of "' + key + '" property is deprecated.' + + ( msg ? ( ' ' + msg ) : '' ); + try { + Object.defineProperty( obj, key, { + configurable: true, + enumerable: true, + get: function () { + mw.log.warn( msg ); + return val; + }, + set: function ( newVal ) { + mw.log.warn( msg ); + val = newVal; + } + } ); + } catch ( err ) { + // IE8 can throw on Object.defineProperty + obj[key] = val; + } + }; + }( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.notification.css b/resources/mediawiki/mediawiki.notification.css index 9a7b651d..3aa358ac 100644 --- a/resources/mediawiki/mediawiki.notification.css +++ b/resources/mediawiki/mediawiki.notification.css @@ -2,15 +2,25 @@ * Stylesheet for mediawiki.notification module */ -#mw-notification-area { +.mw-notification-area { position: absolute; - top: 1em; - right: 1em; + top: 0; + right: 0; + padding: 1em 1em 0 0; width: 20em; line-height: 1.35; z-index: 10000; } +.mw-notification-area-floating { + position: fixed; +} + +* html .mw-notification-area-floating { + /* Make it at least 'absolute' in IE6 since 'fixed' is not supported */ + position: absolute; +} + .mw-notification { padding: 0.25em 1em; margin-bottom: 0.5em; diff --git a/resources/mediawiki/mediawiki.notification.js b/resources/mediawiki/mediawiki.notification.js index 58a3ab6a..4ede8096 100644 --- a/resources/mediawiki/mediawiki.notification.js +++ b/resources/mediawiki/mediawiki.notification.js @@ -1,24 +1,24 @@ -/** - * Implements mediaWiki.notification library - */ ( function ( mw, $ ) { 'use strict'; - var isPageReady = false, - isInitialized = false, - preReadyNotifQueue = [], - /** - * @var {jQuery} - * The #mw-notification-area div that all notifications are contained inside. - */ - $area = null; + var notification, + // The #mw-notification-area div that all notifications are contained inside. + $area, + isPageReady = false, + preReadyNotifQueue = []; /** * Creates a Notification object for 1 message. - * Does not insert anything into the document (see .start()). + * Does not insert anything into the document (see #start). + * + * The "_" in the name is to avoid a bug (http://github.com/senchalabs/jsduck/issues/304) + * It is not part of the actual class name. + * + * @class mw.Notification_ + * @alternateClassName mw.Notification + * @private * * @constructor - * @see mw.notification.notify */ function Notification( message, options ) { var $notification, $notificationTitle, $notificationContent; @@ -88,7 +88,9 @@ // Other notification elements matching the same tag $tagMatches, outerHeight, - placeholderHeight; + placeholderHeight, + autohideCount, + notif; if ( this.isOpen ) { return; @@ -164,10 +166,11 @@ } } ); + notif = this; + // Create a clear placeholder we can use to make the notifications around the notification that is being // replaced expand or contract gracefully to fit the height of the new notification. - var self = this; - self.$replacementPlaceholder = $( '<div>' ) + notif.$replacementPlaceholder = $( '<div>' ) // Set the height to the space the previous notification or placeholder took .css( 'height', outerHeight ) // Make sure that this placeholder is at the very end of this tagged notification group @@ -181,7 +184,7 @@ // Reset the notification position after we've finished the space animation // However do not do it if the placeholder was removed because another tagged // notification went and closed this one. - if ( self.$replacementPlaceholder ) { + if ( notif.$replacementPlaceholder ) { $notification.css( 'position', '' ); } // Finally, remove the placeholder from the DOM @@ -206,7 +209,7 @@ // By default a notification is paused. // If this notification is within the first {autoHideLimit} notifications then // start the auto-hide timer as soon as it's created. - var autohideCount = $area.find( '.mw-notification-autohide' ).length; + autohideCount = $area.find( '.mw-notification-autohide' ).length; if ( autohideCount <= notification.autoHideLimit ) { this.resume(); } @@ -253,6 +256,7 @@ * * @param {Object} options An object containing options for the closing of the notification. * These are typically only used internally. + * * - speed: Use a close speed different than the default 'slow'. * - placeholder: Set to false to disable the placeholder transition. */ @@ -326,7 +330,7 @@ /** * Helper function, take a list of notification divs and call - * a function on the Notification instance attached to them + * a function on the Notification instance attached to them. * * @param {jQuery} $notifications A jQuery object containing notification divs * @param {string} fn The name of the function to call on the Notification instance @@ -341,40 +345,58 @@ } /** - * Initialisation - * (don't call before document ready) + * Initialisation. + * Must only be called once, and not before the document is ready. + * @ignore */ function init() { - if ( !isInitialized ) { - isInitialized = true; - $area = $( '<div id="mw-notification-area"></div>' ) - // Pause auto-hide timers when the mouse is in the notification area. - .on( { - mouseenter: notification.pause, - mouseleave: notification.resume - } ) - // When clicking on a notification close it. - .on( 'click', '.mw-notification', function () { - var notif = $( this ).data( 'mw.notification' ); - if ( notif ) { - notif.close(); - } - } ) - // Stop click events from <a> tags from propogating to prevent clicking. - // on links from hiding a notification. - .on( 'click', 'a', function ( e ) { - e.stopPropagation(); - } ); - - // Prepend the notification area to the content area and save it's object. - mw.util.$content.prepend( $area ); + var offset, $window = $( window ); + + $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' ) + // Pause auto-hide timers when the mouse is in the notification area. + .on( { + mouseenter: notification.pause, + mouseleave: notification.resume + } ) + // When clicking on a notification close it. + .on( 'click', '.mw-notification', function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + notif.close(); + } + } ) + // Stop click events from <a> tags from propogating to prevent clicking. + // on links from hiding a notification. + .on( 'click', 'a', function ( e ) { + e.stopPropagation(); + } ); + + // Prepend the notification area to the content area and save it's object. + mw.util.$content.prepend( $area ); + offset = $area.offset(); + + function updateAreaMode() { + var isFloating = $window.scrollTop() > offset.top; + $area + .toggleClass( 'mw-notification-area-floating', isFloating ) + .toggleClass( 'mw-notification-area-layout', !isFloating ); } + + $window.on( 'scroll', updateAreaMode ); + + // Initial mode + updateAreaMode(); } - var notification = { + /** + * @class mw.notification + * @singleton + */ + notification = { /** * Pause auto-hide timers for all notifications. * Notifications will not auto-hide until resume is called. + * @see mw.Notification#pause */ pause: function () { callEachNotification( @@ -385,13 +407,13 @@ /** * Resume any paused auto-hide timers from the beginning. - * Only the first {autoHideLimit} timers will be resumed. + * Only the first #autoHideLimit timers will be resumed. */ resume: function () { callEachNotification( - // Only call resume on the first {autoHideLimit} notifications. - // Exclude noautohide notifications to avoid bugs where {autoHideLimit} - // { autoHide: false } notifications are at the start preventing any + // Only call resume on the first #autoHideLimit notifications. + // Exclude noautohide notifications to avoid bugs where #autoHideLimit + // `{ autoHide: false }` notifications are at the start preventing any // auto-hide notifications from being autohidden. $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ), 'resume' @@ -401,10 +423,10 @@ /** * Display a notification message to the user. * - * @param {mixed} message The DOM-element, jQuery object, mw.Message instance, - * or plaintext string to be used as the message. + * @param {HTMLElement|jQuery|mw.Message|string} message * @param {Object} options The options to use for the notification. - * See mw.notification.defaults for details. + * See #defaults for details. + * @return {Object} Object with a close function to close the notification */ notify: function ( message, options ) { var notif; @@ -417,25 +439,27 @@ } else { preReadyNotifQueue.push( notif ); } + return { close: $.proxy( notif.close, notif ) }; }, /** - * @var {Object} - * The defaults for mw.notification.notify's options parameter - * autoHide: - * A boolean indicating whether the notifification should automatically - * be hidden after shown. Or if it should persist. + * @property {Object} + * The defaults for #notify options parameter. + * + * - autoHide: + * A boolean indicating whether the notifification should automatically + * be hidden after shown. Or if it should persist. * - * tag: - * An optional string. When a notification is tagged only one message - * with that tag will be displayed. Trying to display a new notification - * with the same tag as one already being displayed will cause the other - * notification to be closed and this new notification to open up inside - * the same place as the previous notification. + * - tag: + * An optional string. When a notification is tagged only one message + * with that tag will be displayed. Trying to display a new notification + * with the same tag as one already being displayed will cause the other + * notification to be closed and this new notification to open up inside + * the same place as the previous notification. * - * title: - * An optional title for the notification. Will be displayed above the - * content. Usually in bold. + * - title: + * An optional title for the notification. Will be displayed above the + * content. Usually in bold. */ defaults: { autoHide: true, @@ -444,20 +468,20 @@ }, /** - * @var {number} + * @property {number} * Number of seconds to wait before auto-hiding notifications. */ autoHideSeconds: 5, /** - * @var {number} + * @property {number} * Maximum number of notifications to count down auto-hide timers for. - * Only the first {autoHideLimit} notifications being displayed will + * Only the first #autoHideLimit notifications being displayed will * auto-hide. Any notifications further down in the list will only start * counting down to auto-hide after the first few messages have closed. * * This basically represents the number of notifications the user should - * be able to process in {autoHideSeconds} time. + * be able to process in #autoHideSeconds time. */ autoHideLimit: 3 }; diff --git a/resources/mediawiki/mediawiki.notify.js b/resources/mediawiki/mediawiki.notify.js index 3bf2a896..743d6517 100644 --- a/resources/mediawiki/mediawiki.notify.js +++ b/resources/mediawiki/mediawiki.notify.js @@ -1,20 +1,28 @@ /** - * Implements mediaWiki.notify function + * @class mw.plugin.notify */ -( function ( mw ) { +( function ( mw, $ ) { 'use strict'; /** - * @see mw.notification.notify + * @see mw.notification#notify + * @param message + * @param options + * @return {jQuery.Promise} */ mw.notify = function ( message, options ) { + var d = $.Deferred(); // Don't bother loading the whole notification system if we never use it. mw.loader.using( 'mediawiki.notification', function () { - // Don't bother calling mw.loader.using a second time after we've already loaded mw.notification. - mw.notify = mw.notification.notify; // Call notify with the notification the user requested of us. - mw.notify( message, options ); - } ); + d.resolve( mw.notification.notify( message, options ) ); + }, d.reject ); + return d.promise(); }; -}( mediaWiki ) );
\ No newline at end of file + /** + * @class mw + * @mixins mw.plugin.notify + */ + +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.searchSuggest.css b/resources/mediawiki/mediawiki.searchSuggest.css new file mode 100644 index 00000000..0fb862b9 --- /dev/null +++ b/resources/mediawiki/mediawiki.searchSuggest.css @@ -0,0 +1,16 @@ +/* Make sure the links are not underlined or colored, ever. */ +/* There is already a :focus / :hover indication on the <div>. */ +.suggestions a.mw-searchSuggest-link, +.suggestions a.mw-searchSuggest-link:hover, +.suggestions a.mw-searchSuggest-link:active, +.suggestions a.mw-searchSuggest-link:focus { + text-decoration: none; + color: black; +} + +.suggestions-result-current a.mw-searchSuggest-link, +.suggestions-result-current a.mw-searchSuggest-link:hover, +.suggestions-result-current a.mw-searchSuggest-link:active, +.suggestions-result-current a.mw-searchSuggest-link:focus { + color: white; +} diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js index 99a55576..7f078626 100644 --- a/resources/mediawiki/mediawiki.searchSuggest.js +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -2,8 +2,8 @@ * Add search suggestions to the search form. */ ( function ( mw, $ ) { - $( document ).ready( function ( $ ) { - var map, searchboxesSelectors, + $( function () { + var map, resultRenderCache, searchboxesSelectors, // Region where the suggestions box will appear directly below // (using the same width). Can be a container element or the input // itself, depending on what suits best in the environment. @@ -41,12 +41,95 @@ return; } + // Compute form data for search suggestions functionality. + function computeResultRenderCache( context ) { + var $form, formAction, baseHref, linkParams; + + // Compute common parameters for links' hrefs + $form = context.config.$region.closest( 'form' ); + + formAction = $form.attr( 'action' ); + baseHref = formAction + ( formAction.match(/\?/) ? '&' : '?' ); + + linkParams = {}; + $.each( $form.serializeArray(), function ( idx, obj ) { + linkParams[ obj.name ] = obj.value; + } ); + + return { + textParam: context.data.$textbox.attr( 'name' ), + linkParams: linkParams, + baseHref: baseHref + }; + } + + // The function used to render the suggestions. + function renderFunction( text, context ) { + if ( !resultRenderCache ) { + resultRenderCache = computeResultRenderCache( context ); + } + + // linkParams object is modified and reused + resultRenderCache.linkParams[ resultRenderCache.textParam ] = text; + + // this is the container <div>, jQueryfied + this + .append( + // the <span> is needed for $.autoEllipsis to work + $( '<span>' ) + .css( 'whiteSpace', 'nowrap' ) + .text( text ) + ) + .wrap( + $( '<a>' ) + .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) ) + .addClass( 'mw-searchSuggest-link' ) + ); + } + + function specialRenderFunction( query, context ) { + var $el = this; + + if ( !resultRenderCache ) { + resultRenderCache = computeResultRenderCache( context ); + } + + // linkParams object is modified and reused + resultRenderCache.linkParams[ resultRenderCache.textParam ] = query; + + if ( $el.children().length === 0 ) { + $el + .append( + $( '<div>' ) + .addClass( 'special-label' ) + .text( mw.msg( 'searchsuggest-containing' ) ), + $( '<div>' ) + .addClass( 'special-query' ) + .text( query ) + .autoEllipsis() + ) + .show(); + } else { + $el.find( '.special-query' ) + .text( query ) + .autoEllipsis(); + } + + if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) { + $el.parent().attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' ); + } else { + $el.wrap( + $( '<a>' ) + .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' ) + .addClass( 'mw-searchSuggest-link' ) + ); + } + } + // General suggestions functionality for all search boxes searchboxesSelectors = [ // Primary searchbox on every page in standard skins '#searchInput', - // Secondary searchbox in legacy skins (LegacyTemplate::searchForm uses id "searchInput + unique id") - '#searchInput2', // Special:Search '#powerSearchText', '#searchText', @@ -56,39 +139,31 @@ $( searchboxesSelectors.join(', ') ) .suggestions( { fetch: function ( query ) { - var $el, jqXhr; + var $el; if ( query.length !== 0 ) { - $el = $(this); - jqXhr = $.ajax( { - url: mw.util.wikiScript( 'api' ), - data: { - format: 'json', - action: 'opensearch', - search: query, - namespace: 0, - suggest: '' - }, - dataType: 'json', - success: function ( data ) { - if ( $.isArray( data ) && data.length ) { - $el.suggestions( 'suggestions', data[1] ); - } - } - }); - $el.data( 'request', jqXhr ); + $el = $( this ); + $el.data( 'request', ( new mw.Api() ).get( { + action: 'opensearch', + search: query, + namespace: 0, + suggest: '' + } ).done( function ( data ) { + $el.suggestions( 'suggestions', data[1] ); + } ) ); } }, cancel: function () { - var jqXhr = $(this).data( 'request' ); + var apiPromise = $( this ).data( 'request' ); // If the delay setting has caused the fetch to have not even happened - // yet, the jqXHR object will have never been set. - if ( jqXhr && $.isFunction( jqXhr.abort ) ) { - jqXhr.abort(); - $(this).removeData( 'request' ); + // yet, the apiPromise object will have never been set. + if ( apiPromise && $.isFunction( apiPromise.abort ) ) { + apiPromise.abort(); + $( this ).removeData( 'request' ); } }, result: { + render: renderFunction, select: function ( $input ) { $input.closest( 'form' ).submit(); } @@ -110,39 +185,16 @@ return; } - // Placeholder text for search box - $searchInput - .attr( 'placeholder', mw.msg( 'searchsuggest-search' ) ) - .placeholder(); - // Special suggestions functionality for skin-provided search box $searchInput.suggestions( { result: { + render: renderFunction, select: function ( $input ) { $input.closest( 'form' ).submit(); } }, special: { - render: function ( query ) { - var $el = this; - if ( $el.children().length === 0 ) { - $el - .append( - $( '<div>' ) - .addClass( 'special-label' ) - .text( mw.msg( 'searchsuggest-containing' ) ), - $( '<div>' ) - .addClass( 'special-query' ) - .text( query ) - .autoEllipsis() - ) - .show(); - } else { - $el.find( '.special-query' ) - .text( query ) - .autoEllipsis(); - } - }, + render: specialRenderFunction, select: function ( $input ) { $input.closest( 'form' ).append( $( '<input type="hidden" name="fulltext" value="1"/>' ) diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js index e64d2e84..3e375fb6 100644 --- a/resources/mediawiki/mediawiki.user.js +++ b/resources/mediawiki/mediawiki.user.js @@ -1,67 +1,60 @@ -/* - * Implementation for mediaWiki.user +/** + * @class mw.user + * @singleton */ - ( function ( mw, $ ) { + var user, + callbacks = {}, + // Extend the skeleton mw.user from mediawiki.js + // This is kind of ugly but we're stuck with this for b/c reasons + options = mw.user.options || new mw.Map(), + tokens = mw.user.tokens || new mw.Map(); /** - * User object + * Get the current user's groups or rights + * + * @private + * @param {string} info One of 'groups' or 'rights' + * @param {Function} callback */ - function User( options, tokens ) { - var user, callbacks; - - /* Private Members */ - - user = this; - callbacks = {}; - - /** - * Gets the current user's groups or rights. - * @param {String} info: One of 'groups' or 'rights'. - * @param {Function} callback - */ - function getUserInfo( info, callback ) { - var api; - if ( callbacks[info] ) { - callbacks[info].add( callback ); - return; - } - callbacks.rights = $.Callbacks('once memory'); - callbacks.groups = $.Callbacks('once memory'); + function getUserInfo( info, callback ) { + var api; + if ( callbacks[info] ) { callbacks[info].add( callback ); - api = new mw.Api(); - api.get( { - action: 'query', - meta: 'userinfo', - uiprop: 'rights|groups' - } ).always( function ( data ) { - var rights, groups; - if ( data.query && data.query.userinfo ) { - rights = data.query.userinfo.rights; - groups = data.query.userinfo.groups; - } - callbacks.rights.fire( rights || [] ); - callbacks.groups.fire( groups || [] ); - } ); + return; } + callbacks.rights = $.Callbacks('once memory'); + callbacks.groups = $.Callbacks('once memory'); + callbacks[info].add( callback ); + api = new mw.Api(); + api.get( { + action: 'query', + meta: 'userinfo', + uiprop: 'rights|groups' + } ).always( function ( data ) { + var rights, groups; + if ( data.query && data.query.userinfo ) { + rights = data.query.userinfo.rights; + groups = data.query.userinfo.groups; + } + callbacks.rights.fire( rights || [] ); + callbacks.groups.fire( groups || [] ); + } ); + } - /* Public Members */ - - this.options = options || new mw.Map(); - - this.tokens = tokens || new mw.Map(); - - /* Public Methods */ + mw.user = user = { + options: options, + tokens: tokens, /** - * Generates a random user session ID (32 alpha-numeric characters). + * Generate a random user session ID (32 alpha-numeric characters) * * This information would potentially be stored in a cookie to identify a user during a * session or series of sessions. Its uniqueness should not be depended on. * - * @return String: Random set of 32 alpha-numeric characters + * @return {string} Random set of 32 alpha-numeric characters */ - function generateId() { + generateRandomSessionId: function () { var i, r, id = '', seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -70,126 +63,156 @@ id += seed.substring( r, r + 1 ); } return id; - } + }, /** - * Gets the current user's name. + * Get the current user's database id + * + * Not to be confused with #id. * - * @return Mixed: User name string or null if users is anonymous + * @return {number} Current user's id, or 0 if user is anonymous */ - this.getName = function () { + getId: function () { + return mw.config.get( 'wgUserId', 0 ); + }, + + /** + * Get the current user's name + * + * @return {string|null} User name string or null if user is anonymous + */ + getName: function () { return mw.config.get( 'wgUserName' ); - }; + }, + + /** + * @inheritdoc #getName + * @deprecated since 1.20 use #getName instead + */ + name: function () { + return user.getName(); + }, /** - * @deprecated since 1.20 use mw.user.getName() instead + * Get date user registered, if available + * + * @return {Date|boolean|null} Date user registered, or false for anonymous users, or + * null when data is not available */ - this.name = function () { - return this.getName(); - }; + getRegistration: function () { + var registration = mw.config.get( 'wgUserRegistration' ); + if ( user.isAnon() ) { + return false; + } else if ( registration === null ) { + // Information may not be available if they signed up before + // MW began storing this. + return null; + } else { + return new Date( registration ); + } + }, /** - * Checks if the current user is anonymous. + * Whether the current user is anonymous * - * @return Boolean + * @return {boolean} */ - this.isAnon = function () { + isAnon: function () { return user.getName() === null; - }; + }, /** - * @deprecated since 1.20 use mw.user.isAnon() instead + * @inheritdoc #isAnon + * @deprecated since 1.20 use #isAnon instead */ - this.anonymous = function () { + anonymous: function () { return user.isAnon(); - }; + }, /** - * Gets a random session ID automatically generated and kept in a cookie. + * Get an automatically generated random ID (stored in a session cookie) * * This ID is ephemeral for everyone, staying in their browser only until they close * their browser. * - * @return String: User name or random session ID + * @return {string} Random session ID */ - this.sessionId = function () { + sessionId: function () { var sessionId = $.cookie( 'mediaWiki.user.sessionId' ); - if ( typeof sessionId === 'undefined' || sessionId === null ) { - sessionId = generateId(); - $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } ); + if ( sessionId === undefined || sessionId === null ) { + sessionId = user.generateRandomSessionId(); + $.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } ); } return sessionId; - }; + }, /** - * Gets the current user's name or the session ID + * Get the current user's name or the session ID * - * @return String: User name or random session ID + * Not to be confused with #getId. + * + * @return {string} User name or random session ID */ - this.id = function() { - var name = user.getName(); - if ( name ) { - return name; - } - return user.sessionId(); - }; + id: function () { + return user.getName() || user.sessionId(); + }, /** - * Gets the user's bucket, placing them in one at random based on set odds if needed. - * - * @param key String: Name of bucket - * @param options Object: Bucket configuration options - * @param options.buckets Object: List of bucket-name/relative-probability pairs (required, - * must have at least one pair) - * @param options.version Number: Version of bucket test, changing this forces rebucketing - * (optional, default: 0) - * @param options.tracked Boolean: Track the event of bucketing through the API module of - * the ClickTracking extension (optional, default: false) - * @param options.expires Number: Length of time (in days) until the user gets rebucketed - * (optional, default: 30) - * @return String: Bucket name - the randomly chosen key of the options.buckets object + * Get the user's bucket (place them in one if not done already) * - * @example * mw.user.bucket( 'test', { - * 'buckets': { 'ignored': 50, 'control': 25, 'test': 25 }, - * 'version': 1, - * 'tracked': true, - * 'expires': 7 + * buckets: { ignored: 50, control: 25, test: 25 }, + * version: 1, + * expires: 7 * } ); + * + * @param {string} key Name of bucket + * @param {Object} options Bucket configuration options + * @param {Object} options.buckets List of bucket-name/relative-probability pairs (required, + * must have at least one pair) + * @param {number} [options.version=0] Version of bucket test, changing this forces + * rebucketing + * @param {number} [options.expires=30] Length of time (in days) until the user gets + * rebucketed + * @return {string} Bucket name - the randomly chosen key of the `options.buckets` object */ - this.bucket = function ( key, options ) { + bucket: function ( key, options ) { var cookie, parts, version, bucket, range, k, rand, total; options = $.extend( { buckets: {}, version: 0, - tracked: false, expires: 30 }, options || {} ); cookie = $.cookie( 'mediaWiki.user.bucket:' + key ); // Bucket information is stored as 2 integers, together as version:bucket like: "1:2" - if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) > 0 ) { + if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) !== -1 ) { parts = cookie.split( ':' ); if ( parts.length > 1 && Number( parts[0] ) === options.version ) { version = Number( parts[0] ); bucket = String( parts[1] ); } } + if ( bucket === undefined ) { if ( !$.isPlainObject( options.buckets ) ) { - throw 'Invalid buckets error. Object expected for options.buckets.'; + throw new Error( 'Invalid bucket. Object expected for options.buckets.' ); } + version = Number( options.version ); + // Find range range = 0; for ( k in options.buckets ) { range += options.buckets[k]; } + // Select random value within range rand = Math.random() * range; + // Determine which bucket the value landed in total = 0; for ( k in options.buckets ) { @@ -199,39 +222,34 @@ break; } } - if ( options.tracked ) { - mw.loader.using( 'jquery.clickTracking', function () { - $.trackAction( - 'mediaWiki.user.bucket:' + key + '@' + version + ':' + bucket - ); - } ); - } + $.cookie( 'mediaWiki.user.bucket:' + key, version + ':' + bucket, - { 'path': '/', 'expires': Number( options.expires ) } + { path: '/', expires: Number( options.expires ) } ); } + return bucket; - }; + }, /** - * Gets the current user's groups. + * Get the current user's groups + * + * @param {Function} callback */ - this.getGroups = function ( callback ) { + getGroups: function ( callback ) { getUserInfo( 'groups', callback ); - }; + }, /** - * Gets the current user's rights. + * Get the current user's rights + * + * @param {Function} callback */ - this.getRights = function ( callback ) { + getRights: function ( callback ) { getUserInfo( 'rights', callback ); - }; - } - - // Extend the skeleton mw.user from mediawiki.js - // This is kind of ugly but we're stuck with this for b/c reasons - mw.user = new User( mw.user.options, mw.user.tokens ); + } + }; }( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js index 29284384..7383df2d 100644 --- a/resources/mediawiki/mediawiki.util.js +++ b/resources/mediawiki/mediawiki.util.js @@ -1,10 +1,11 @@ -/** - * Implements mediaWiki.util library - */ ( function ( mw, $ ) { 'use strict'; - // Local cache and alias + /** + * Utility library + * @class mw.util + * @singleton + */ var util = { /** @@ -12,7 +13,7 @@ * (don't call before document ready) */ init: function () { - var profile, $tocTitle, $tocToggleLink, hideTocCookie; + var profile; /* Set tooltipAccessKeyPrefix */ profile = $.client.profile(); @@ -28,13 +29,10 @@ profile.platform === 'mac' // Chrome on Mac ? 'ctrl-option-' - : profile.platform === 'win' - // Chrome on Windows - // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu - // which alt-shift-f does not) - ? 'alt-shift-' - // Chrome on other (Ubuntu?) - : 'alt-' + // Chrome on Windows or Linux + // (both alt- and alt-shift work, but alt with E, D, F etc does not + // work since they are browser shortcuts) + : 'alt-shift-' ); // Non-Windows Safari with webkit_version > 526 @@ -55,14 +53,16 @@ || profile.name === 'konqueror' ) ) { util.tooltipAccessKeyPrefix = 'ctrl-'; - // Firefox 2.x and later - } else if ( profile.name === 'firefox' && profile.versionBase > '1' ) { + // Firefox/Iceweasel 2.x and later + } else if ( ( profile.name === 'firefox' || profile.name === 'iceweasel' ) + && profile.versionBase > '1' ) { util.tooltipAccessKeyPrefix = 'alt-shift-'; } /* Fill $content var */ util.$content = ( function () { - var $content, selectors = [ + var i, l, $content, selectors; + selectors = [ // The preferred standard for setting $content (class="mw-body") // You may also use (class="mw-body mw-body-primary") if you use // mw-body in multiple locations. @@ -94,7 +94,7 @@ // not inserted bodytext yet. But in any case <body> should always exist 'body' ]; - for ( var i = 0, l = selectors.length; i < l; i++ ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { $content = $( selectors[i] ).first(); if ( $content.length ) { return $content; @@ -106,29 +106,32 @@ } )(); // Table of contents toggle - $tocTitle = $( '#toctitle' ); - $tocToggleLink = $( '#togglelink' ); - // Only add it if there is a TOC and there is no toggle added already - if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) { - hideTocCookie = $.cookie( 'mw_hidetoc' ); + mw.hook( 'wikipage.content' ).add( function () { + var $tocTitle, $tocToggleLink, hideTocCookie; + $tocTitle = $( '#toctitle' ); + $tocToggleLink = $( '#togglelink' ); + // Only add it if there is a TOC and there is no toggle added already + if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) { + hideTocCookie = $.cookie( 'mw_hidetoc' ); $tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' ) .text( mw.msg( 'hidetoc' ) ) .click( function ( e ) { e.preventDefault(); util.toggleToc( $(this) ); } ); - $tocTitle.append( - $tocToggleLink - .wrap( '<span class="toctoggle"></span>' ) - .parent() - .prepend( ' [' ) - .append( '] ' ) - ); - - if ( hideTocCookie === '1' ) { - util.toggleToc( $tocToggleLink ); + $tocTitle.append( + $tocToggleLink + .wrap( '<span class="toctoggle"></span>' ) + .parent() + .prepend( ' [' ) + .append( '] ' ) + ); + + if ( hideTocCookie === '1' ) { + util.toggleToc( $tocToggleLink ); + } } - } + } ); }, /* Main body */ @@ -136,7 +139,7 @@ /** * Encode the string like PHP's rawurlencode * - * @param str string String to be encoded + * @param {string} str String to be encoded. */ rawurlencode: function ( str ) { str = String( str ); @@ -150,7 +153,7 @@ * We want / and : to be included as literal characters in our title URLs * as they otherwise fatally break the title * - * @param str string String to be encoded + * @param {string} str String to be encoded. */ wikiUrlencode: function ( str ) { return util.rawurlencode( str ) @@ -158,19 +161,26 @@ }, /** - * Get the link to a page name (relative to wgServer) + * Get the link to a page name (relative to `wgServer`), * - * @param str String: Page name to get the link for. - * @return String: Location for a page with name of 'str' or boolean false on error. + * @param {string} str Page name to get the link for. + * @param {Object} params A mapping of query parameter names to values, + * e.g. { action: 'edit' }. Optional. + * @return {string} Location for a page with name of `str` or boolean false on error. */ - wikiGetlink: function ( str ) { - return mw.config.get( 'wgArticlePath' ).replace( '$1', + getUrl: function ( str, params ) { + var url = mw.config.get( 'wgArticlePath' ).replace( '$1', util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) ); + if ( params && !$.isEmptyObject( params ) ) { + url += url.indexOf( '?' ) !== -1 ? '&' : '?'; + url += $.param( params ); + } + return url; }, /** * Get address to a script in the wiki root. - * For index.php use mw.config.get( 'wgScript' ) + * For index.php use `mw.config.get( 'wgScript' )`. * * @since 1.18 * @param str string Name of script (eg. 'api'), defaults to 'index' @@ -190,20 +200,18 @@ /** * Append a new style block to the head and return the CSSStyleSheet object. - * Use .ownerNode to access the <style> element, or use mw.loader.addStyleTag. + * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag. * This function returns the styleSheet object for convience (due to cross-browsers * difference as to where it is located). - * @example - * <code> - * var sheet = mw.util.addCSS('.foobar { display: none; }'); - * $(foo).click(function () { - * // Toggle the sheet on and off - * sheet.disabled = !sheet.disabled; - * }); - * </code> * - * @param text string CSS to be appended - * @return CSSStyleSheet (use .ownerNode to get to the <style> element) + * var sheet = mw.util.addCSS('.foobar { display: none; }'); + * $(foo).click(function () { + * // Toggle the sheet on and off + * sheet.disabled = !sheet.disabled; + * }); + * + * @param {string} text CSS to be appended + * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element. */ addCSS: function ( text ) { var s = mw.loader.addStyleTag( text ); @@ -213,10 +221,10 @@ /** * Hide/show the table of contents element * - * @param $toggleLink jQuery A jQuery object of the toggle link. - * @param callback function Function to be called after the toggle is - * completed (including the animation) (optional) - * @return mixed Boolean visibility of the toc (true if it's visible) + * @param {jQuery} $toggleLink A jQuery object of the toggle link. + * @param {Function} [callback] Function to be called after the toggle is + * completed (including the animation). + * @return {Mixed} Boolean visibility of the toc (true if it's visible) * or Null if there was no table of contents. */ toggleToc: function ( $toggleLink, callback ) { @@ -253,12 +261,14 @@ * Grab the URL parameter value for the given parameter. * Returns null if not found. * - * @param param string The parameter name. - * @param url string URL to search through (optional) - * @return mixed Parameter value or null. + * @param {string} param The parameter name. + * @param {string} [url=document.location.href] URL to search through, defaulting to the current document's URL. + * @return {Mixed} Parameter value or null. */ getParamValue: function ( param, url ) { - url = url || document.location.href; + if ( url === undefined ) { + url = document.location.href; + } // Get last match, stop at hash var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ), m = re.exec( url ); @@ -271,17 +281,26 @@ }, /** - * @var string + * @property {string} * Access key prefix. Will be re-defined based on browser/operating system - * detection in mw.util.init(). + * detection in mw.util#init. */ tooltipAccessKeyPrefix: 'alt-', /** - * @var RegExp + * @property {RegExp} * Regex to match accesskey tooltips. + * + * Should match: + * + * - "ctrl-option-" + * - "alt-shift-" + * - "ctrl-alt-" + * - "ctrl-" + * + * The accesskey is matched in group $6. */ - tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/, + tooltipAccessKeyRegexp: /\[(ctrl-)?(option-)?(alt-)?(shift-)?(esc-)?(.)\]$/, /** * Add the appropriate prefix to the accesskey shown in the tooltip. @@ -289,8 +308,7 @@ * otherwise, all the nodes that will probably have accesskeys by * default are updated. * - * @param $nodes {Array|jQuery} [optional] A jQuery object, or array - * of elements to update. + * @param {Array|jQuery} [$nodes] A jQuery object, or array of nodes to update. */ updateTooltipAccessKeys: function ( $nodes ) { if ( !$nodes ) { @@ -303,18 +321,18 @@ } $nodes.attr( 'title', function ( i, val ) { - if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) { + if ( val && util.tooltipAccessKeyRegexp.test( val ) ) { return val.replace( util.tooltipAccessKeyRegexp, - '[' + util.tooltipAccessKeyPrefix + '$5]' ); + '[' + util.tooltipAccessKeyPrefix + '$6]' ); } return val; } ); }, /* - * @var jQuery - * A jQuery object that refers to the content area element - * Populated by init(). + * @property {jQuery} + * A jQuery object that refers to the content area element. + * Populated by #init. */ $content: null, @@ -329,28 +347,28 @@ * * By default the new link will be added to the end of the list. To * add the link before a given existing item, pass the DOM node - * (document.getElementById( 'foobar' )) or the jQuery-selector - * ( '#foobar' ) of that item. + * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector + * (e.g. `'#foobar'`) for that item. * - * @example mw.util.addPortletLink( - * 'p-tb', 'http://mediawiki.org/', - * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print' - * ) + * mw.util.addPortletLink( + * 'p-tb', 'http://mediawiki.org/', + * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print' + * ); * - * @param portlet string ID of the target portlet ( 'p-cactions' or 'p-personal' etc.) - * @param href string Link URL - * @param text string Link text - * @param id string ID of the new item, should be unique and preferably have - * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' ) - * @param tooltip string Text to show when hovering over the link, without accesskey suffix - * @param accesskey string Access key to activate this link (one character, try - * to avoid conflicts. Use $( '[accesskey=x]' ).get() in the console to - * see if 'x' is already used. - * @param nextnode mixed DOM Node or jQuery-selector string of the item that the new - * item should be added before, should be another item in the same - * list, it will be ignored otherwise + * @param {string} portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.) + * @param {string} href Link URL + * @param {string} text Link text + * @param {string} [id] ID of the new item, should be unique and preferably have + * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' ) + * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix + * @param {string} [accesskey] Access key to activate this link (one character, try + * to avoid conflicts. Use `$( '[accesskey=x]' ).get()` in the console to + * see if 'x' is already used. + * @param {HTMLElement|jQuery|string} [nextnode] Element or jQuery-selector string to the item that + * the new item should be added before, should be another item in the same + * list, it will be ignored otherwise * - * @return mixed The DOM Node of the added item (a ListItem or Anchor element, + * @return {HTMLElement|null} The added element (a ListItem or Anchor element, * depending on the skin) or null if no element was added to the document. */ addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) { @@ -366,88 +384,86 @@ $link.attr( 'title', tooltip ); } - // Some skins don't have any portlets - // just add it to the bottom of their 'sidebar' element as a fallback - switch ( mw.config.get( 'skin' ) ) { - case 'standard': - case 'cologneblue': - $( '#quickbar' ).append( $link.after( '<br/>' ) ); - return $link[0]; - case 'nostalgia': - $( '#searchform' ).before( $link ).before( ' | ' ); - return $link[0]; - default: // Skins like chick, modern, monobook, myskin, simple, vector... - - // Select the specified portlet - $portlet = $( '#' + portlet ); - if ( $portlet.length === 0 ) { - return null; - } - // Select the first (most likely only) unordered list inside the portlet - $ul = $portlet.find( 'ul' ).eq( 0 ); + // Select the specified portlet + $portlet = $( '#' + portlet ); + if ( $portlet.length === 0 ) { + return null; + } + // Select the first (most likely only) unordered list inside the portlet + $ul = $portlet.find( 'ul' ).eq( 0 ); - // If it didn't have an unordered list yet, create it - if ( $ul.length === 0 ) { + // If it didn't have an unordered list yet, create it + if ( $ul.length === 0 ) { - $ul = $( '<ul>' ); + $ul = $( '<ul>' ); - // If there's no <div> inside, append it to the portlet directly - if ( $portlet.find( 'div:first' ).length === 0 ) { - $portlet.append( $ul ); - } else { - // otherwise if there's a div (such as div.body or div.pBody) - // append the <ul> to last (most likely only) div - $portlet.find( 'div' ).eq( -1 ).append( $ul ); - } - } - // Just in case.. - if ( $ul.length === 0 ) { - return null; + // If there's no <div> inside, append it to the portlet directly + if ( $portlet.find( 'div:first' ).length === 0 ) { + $portlet.append( $ul ); + } else { + // otherwise if there's a div (such as div.body or div.pBody) + // append the <ul> to last (most likely only) div + $portlet.find( 'div' ).eq( -1 ).append( $ul ); } + } + // Just in case.. + if ( $ul.length === 0 ) { + return null; + } - // Unhide portlet if it was hidden before - $portlet.removeClass( 'emptyPortlet' ); + // Unhide portlet if it was hidden before + $portlet.removeClass( 'emptyPortlet' ); - // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab) - // and back up the selector to the list item - if ( $portlet.hasClass( 'vectorTabs' ) ) { - $item = $link.wrap( '<li><span></span></li>' ).parent().parent(); - } else { - $item = $link.wrap( '<li></li>' ).parent(); - } + // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab) + // and back up the selector to the list item + if ( $portlet.hasClass( 'vectorTabs' ) ) { + $item = $link.wrap( '<li><span></span></li>' ).parent().parent(); + } else { + $item = $link.wrap( '<li></li>' ).parent(); + } - // Implement the properties passed to the function - if ( id ) { - $item.attr( 'id', id ); - } + // Implement the properties passed to the function + if ( id ) { + $item.attr( 'id', id ); + } + + if ( tooltip ) { + // Trim any existing accesskey hint and the trailing space + tooltip = $.trim( tooltip.replace( util.tooltipAccessKeyRegexp, '' ) ); if ( accesskey ) { - $link.attr( 'accesskey', accesskey ); tooltip += ' [' + accesskey + ']'; - $link.attr( 'title', tooltip ); } - if ( accesskey && tooltip ) { + $link.attr( 'title', tooltip ); + if ( accesskey ) { util.updateTooltipAccessKeys( $link ); } + } - // Where to put our node ? - // - nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js) - if ( nextnode && nextnode.parentNode === $ul[0] ) { - $(nextnode).before( $item ); - - // - nextnode is a CSS selector for jQuery - } else if ( typeof nextnode === 'string' && $ul.find( nextnode ).length !== 0 ) { - $ul.find( nextnode ).eq( 0 ).before( $item ); + if ( accesskey ) { + $link.attr( 'accesskey', accesskey ); + } - // If the jQuery selector isn't found within the <ul>, - // or if nextnode was invalid or not passed at all, - // then just append it at the end of the <ul> (this is the default behaviour) - } else { + if ( nextnode ) { + if ( nextnode.nodeType || typeof nextnode === 'string' ) { + // nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js) + // or nextnode is a CSS selector for jQuery + nextnode = $ul.find( nextnode ); + } else if ( !nextnode.jquery || ( nextnode.length && nextnode[0].parentNode !== $ul[0] ) ) { + // Fallback $ul.append( $item ); + return $item[0]; } + if ( nextnode.length === 1 ) { + // nextnode is a jQuery object that represents exactly one element + nextnode.before( $item ); + return $item[0]; + } + } + // Fallback (this is the default behavior) + $ul.append( $item ); + return $item[0]; - return $item[0]; - } }, /** @@ -455,9 +471,9 @@ * something, replacing any previous message. * Calling with no arguments, with an empty string or null will hide the message * - * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box. + * @param {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box. * to allow CSS/JS to hide different boxes. null = no class used. - * @depreceated Use mw.notify + * @deprecated since 1.20 Use mw#notify */ jsMessage: function ( message ) { if ( !arguments.length || message === '' || message === null ) { @@ -475,87 +491,80 @@ * according to HTML5 specification. Please note the specification * does not validate a domain with one character. * - * @todo FIXME: should be moved to or replaced by a JavaScript validation module. + * FIXME: should be moved to or replaced by a validation module. * - * @param mailtxt string E-mail address to be validated. - * @return mixed Null if mailtxt was an empty string, otherwise true/false - * is determined by validation. + * @param {string} mailtxt E-mail address to be validated. + * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false + * as determined by validation. */ validateEmail: function ( mailtxt ) { - var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp; + var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp; if ( mailtxt === '' ) { return null; } - /** - * HTML5 defines a string as valid e-mail address if it matches - * the ABNF: - * 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str ) - * With: - * - atext : defined in RFC 5322 section 3.2.3 - * - ldh-str : defined in RFC 1034 section 3.5 - * - * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68): - */ - - /** - * First, define the RFC 5322 'atext' which is pretty easy: - * atext = ALPHA / DIGIT / ; Printable US-ASCII - "!" / "#" / ; characters not including - "$" / "%" / ; specials. Used for atoms. - "&" / "'" / - "*" / "+" / - "-" / "/" / - "=" / "?" / - "^" / "_" / - "`" / "{" / - "|" / "}" / - "~" - */ - rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~"; - - /** - * Next define the RFC 1034 'ldh-str' - * <domain> ::= <subdomain> | " " - * <subdomain> ::= <label> | <subdomain> "." <label> - * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] - * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> - * <let-dig-hyp> ::= <let-dig> | "-" - * <let-dig> ::= <letter> | <digit> - */ - rfc1034_ldh_str = "a-z0-9\\-"; - - HTML5_email_regexp = new RegExp( + // HTML5 defines a string as valid e-mail address if it matches + // the ABNF: + // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str ) + // With: + // - atext : defined in RFC 5322 section 3.2.3 + // - ldh-str : defined in RFC 1034 section 3.5 + // + // (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68) + // First, define the RFC 5322 'atext' which is pretty easy: + // atext = ALPHA / DIGIT / ; Printable US-ASCII + // "!" / "#" / ; characters not including + // "$" / "%" / ; specials. Used for atoms. + // "&" / "'" / + // "*" / "+" / + // "-" / "/" / + // "=" / "?" / + // "^" / "_" / + // "`" / "{" / + // "|" / "}" / + // "~" + rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~'; + + // Next define the RFC 1034 'ldh-str' + // <domain> ::= <subdomain> | " " + // <subdomain> ::= <label> | <subdomain> "." <label> + // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] + // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> + // <let-dig-hyp> ::= <let-dig> | "-" + // <let-dig> ::= <letter> | <digit> + rfc1034LdhStr = 'a-z0-9\\-'; + + html5EmailRegexp = new RegExp( // start of string '^' + // User part which is liberal :p - '[' + rfc5322_atext + '\\.]+' + '[' + rfc5322Atext + '\\.]+' + // 'at' '@' + // Domain first part - '[' + rfc1034_ldh_str + ']+' + '[' + rfc1034LdhStr + ']+' + // Optional second part and following are separated by a dot - '(?:\\.[' + rfc1034_ldh_str + ']+)*' + '(?:\\.[' + rfc1034LdhStr + ']+)*' + // End of string '$', // RegExp is case insensitive 'i' ); - return (null !== mailtxt.match( HTML5_email_regexp ) ); + return (null !== mailtxt.match( html5EmailRegexp ) ); }, /** * Note: borrows from IP::isIPv4 * - * @param address string - * @param allowBlock boolean - * @return boolean + * @param {string} address + * @param {boolean} allowBlock + * @return {boolean} */ isIPv4Address: function ( address, allowBlock ) { if ( typeof address !== 'string' ) { @@ -572,9 +581,9 @@ /** * Note: borrows from IP::isIPv6 * - * @param address string - * @param allowBlock boolean - * @return boolean + * @param {string} address + * @param {boolean} allowBlock + * @return {boolean} */ isIPv6Address: function ( address, allowBlock ) { if ( typeof address !== 'string' ) { @@ -603,6 +612,13 @@ } }; + /** + * @method wikiGetlink + * @inheritdoc #getUrl + * @deprecated since 1.23 Use #getUrl instead. + */ + mw.log.deprecate( util, 'wikiGetlink', util.getUrl, 'Use mw.util.getUrl instead.' ); + mw.util = util; }( mediaWiki, jQuery ) ); |