From: Krinkle Date: Sat, 18 Jun 2011 09:17:09 +0000 (+0000) Subject: Implement mw.Title in core X-Git-Tag: 1.31.0-rc.0~29450 X-Git-Url: http://git.cyclocoop.org/data/modifier.php?a=commitdiff_plain;h=3c4a5e21fe5f348d0cd557a05c28cbc65d636114;p=lhc%2Fweb%2Fwiklou.git Implement mw.Title in core * Based on UploadWizard/resources/mw.Title.js * Refactored to use local scope and prototypes instead of re-declaring them per-instance in the private scope through 'this' (re-use by reference, faster instantiation and performance) * Fix potential ReferenceError in the check for wgArticlePath (inline if statements will fail for undeclared variables, needs typeof undefined check). Using mw.config instead to avoid this problem. * The following two methods were not ported from UploadWizard because they were or became redundant and/or merged with another method: -- setNameText (redundant with the improved setName) -- setPrefix (redundant wit the improved setNamespace) * Ported all jasmine tests to QUnit. Left them exactly the same to make sure it's compatible with UploadWizard. Perhaps I'll expand or adjust the suite later to be less file-specific, but for now make letting this revision go through TestSwarm to be sure it's compatible and behaves exactly the same. * Added getName() method instead, replacing direct access to '_name' This in order to check for wgCaseSensitiveNamespaces (bug 29441; r90234) -- Prevents breakages on wiktionary and other wikis with case sensitivity. ie. on a Wiktionary: new mw.Title('apple').toString() > "Apple" -- This fix will make it return 'apple' instead of 'Apple' (that is, if 0 is in wgCaseSensitiveNamespaces). * There used to be a strip-bug in scenarios where a namespace-like string appears inside of a title. Imagine pagename: "Category:Wikipedia:Foo bar" (exist on several wikis; NS_CATEGORY= 14) new mw.Title( 'Wikipedia:Foo bar', 14 ).toString() > "Category:Foo_bar" // Stripped "Wikipedia:" !! In order to fix this: -- Removed assumption that every title has a namespace prefix. UploadWizard/mw.Title has one initialization RegExp (which was ported as-is to "setAll"). In addition there is now a "setNameAndExtension" method (which doesn't extract or set the namespace). Now the above case: new mw.Title( 'Wikipedia:Foo bar', 14 ).toString() > "Category:Wikipedia_Foo_bar" // Better, but now the colon is gone.. -- In order to fix that, "\x3a" was removed from the clean() function. Colons are valid in MediaWiki titles, no need to escape. new mw.Title( 'Wikipedia:Foo bar', 14 ).toString() > "Category:Wikipedia:Foo_bar" // Yay! * Last but not least, another little bug fixed due to the previous point. It also fixed a thrown exception in case a colon is part of the title in the main namespace (not rare for movies and books): new mw.Title( 'The Wiki: Awesomeness') > Error: mw.Title> Unrecognized canonical namespace: the_wiki This exception is thrown from setNamespace(). That exception would make sense if setNamespace() was called by the user direcly, but when called from setAll() it should gracefully fallback by putting the prefix in the name instead and assuming NS_MAIN (just like the server side does). To achieve this I added a try/catch around setAll() and fallback to the new setNameAndExtension(). * Passes JSHint. * Additional feature: exists(). Return true/false if known, otherwise null. Extensions can populate this for titles they are interested in and the front-end can construct url's and UI elements with correct redlink-status. Gadgets can populate it as well but that would require an API-request to get the information. A bit of a stub for later use, although I think it works fine. * Bugfix in jquery.qunit.completenessTest.js (first triggered by the introduction of mw.Title). Don't traverse the 'constructor' property (recursive loop, ouch!) --- (bug 29397) Implement mw.Title module in core --- diff --git a/RELEASE-NOTES-1.19 b/RELEASE-NOTES-1.19 index 2c078b2244..f0490a1dd3 100644 --- a/RELEASE-NOTES-1.19 +++ b/RELEASE-NOTES-1.19 @@ -53,6 +53,7 @@ production. * (bug 28904) Update jQuery version from 1.4.4 to 1.6.1 (the latest version) * (bug 29441) Expose CapitalLinks config in JS to allow modules to properly handle titles on case-sensitive wikis. +* (bug 29397) Implement mw.Title module in core. === Bug fixes in 1.19 === * (bug 28868) Show total pages in the subtitle of an image on the diff --git a/resources/Resources.php b/resources/Resources.php index ce6e365039..0291d3946f 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -439,6 +439,9 @@ return array( 'mediawiki.htmlform' => array( 'scripts' => 'resources/mediawiki/mediawiki.htmlform.js', ), + 'mediawiki.Title' => array( + 'scripts' => 'resources/mediawiki/mediawiki.Title.js', + ), 'mediawiki.user' => array( 'scripts' => 'resources/mediawiki/mediawiki.user.js', 'dependencies' => array( diff --git a/resources/jquery/jquery.qunit.completenessTest.js b/resources/jquery/jquery.qunit.completenessTest.js index e98ed87567..7d4f346123 100644 --- a/resources/jquery/jquery.qunit.completenessTest.js +++ b/resources/jquery/jquery.qunit.completenessTest.js @@ -155,6 +155,7 @@ CompletenessTest.fn = CompletenessTest.prototype = { // ...the prototypes are fine tho $.each( currVar.prototype, function( key, value ) { + if ( key === 'constructor' ) return; // Clone and brake reference to parentPathArray var tmpPathArray = $.extend( [], parentPathArray ); @@ -182,6 +183,7 @@ CompletenessTest.fn = CompletenessTest.prototype = { // ... the prototypes are fine tho $.each( currVar.prototype, function( key, value ) { + if ( key === 'constructor' ) return; // Clone and brake reference to parentPathArray var tmpPathArray = $.extend( [], parentPathArray ); diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js new file mode 100644 index 0000000000..a3459ac3f7 --- /dev/null +++ b/resources/mediawiki/mediawiki.Title.js @@ -0,0 +1,316 @@ +/** + * mediaWiki.Title + * + * @author Neil Kandalgaonkar, 2010 + * @author Timo Tijhof, 2011 + * @since 1.19 + * + * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink + */ +(function( $ ) { + + /* Local space */ + + /** + * Title + * @constructor + * + * @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 + */ +var Title = function( title, namespace ) { + this._ns = 0; // integer namespace id + this._name = null; // name in canonical 'database' form + this._ext = null; // extension + + if ( arguments.length === 2 ) { + this.setNameAndExtension( title ).setNamespaceById( namespace ); + } else if ( arguments.length === 1 ) { + // If title is like "Blabla: Hello" ignore exception by setNamespace(), + // and instead assume NS_MAIN and keep prefix + try { + this.setAll( title ); + } catch(e) { + this.setNameAndExtension( title ); + } + } + return this; + }, + + /** + * 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, '_' ); + } + }, + + /** + * Convert db-key to readable text. + * @param s {String} + * @return {String} + */ + text = function ( s ) { + if ( s !== null && s !== undefined ) { + return s.replace( /_/g, ' ' ); + } else { + return ''; + } + }; + + /* Static space */ + + /** + * Wether 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. + */ + Title.exists = function( title ) { + var type = $.type( title ), obj = Title.exist.pages, match; + if ( type === 'string' ) { + match = obj[title]; + } else if ( type === 'object' && title instanceof Title ) { + match = obj[title.toString()]; + } 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. + */ + pages: {}, + /** + * @example Declare existing titles: Title.exist.set(['User:John_Doe', ...]); + * @example Declare titles inexisting: 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} + */ + set: function( titles, state ) { + titles = $.isArray( titles ) ? titles : [titles]; + state = state === undefined ? true : !!state; + var pages = this.pages, i, len = titles.length; + for ( i = 0; i < len; i++ ) { + pages[ titles[i] ] = state; + } + return true; + } + }; + + /* Public methods */ + + var fn = { + constructor: Title, + /** + * @param id {Number} Canonical namespace id. + * @return {mw.Title} this + */ + setNamespaceById: function( id ) { + // wgFormattedNamespaces is an object of *string* key-vals, + var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()]; + + // Cannot cast to boolean, ns may be '' (main namespace) + if ( ns === undefined ) { + this._ns = false; + } else { + this._ns = Number( id ); + } + return this; + }, + + /** + * Set namespace by any known namespace/id pair (localized, canonical or alias) + * On a German wiki this could be 'File', 'Datei', 'Image' or even 'Bild' for NS_FILE. + * @param ns {String} A namespace name (case insensitive, space insensitive) + * @return {mw.Title} this + */ + setNamespace: function( ns ) { + ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize + var id = mw.config.get( 'wgNamespaceIds' )[ns]; + if ( id === undefined ) { + throw new Error( 'mw.Title: Unrecognized canonical namespace: ' + ns ); + } + return this.setNamespaceById( id ); + }, + + /** + * Get the namespace number. + * @return {Number} + */ + getNamespaceId: function(){ + return this._ns; + }, + + /** + * Get the namespace prefix (in the content-language). + * 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 ? '' : ':'); + }, + + /** + * Set the "name" portion, removing illegal characters. + * @param s {String} Page name (without namespace prefix) + * @return {mw.Title} this + */ + setName: function( s ) { + this._name = clean( $.trim( s ) ); + return this; + }, + + /** + * The name, like "Foo_bar" + * @return {String} + */ + getName: function() { + if ( $.inArray( this._ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this._name; + } else { + return $.ucFirst( this._name ); + } + }, + + /** + * The name, like "Foo bar" + * @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". + */ + getPrefixedDb: function() { + return this.getNamespacePrefix() + this.getMain(); + }, + + /** + * Get full name in text form, like "File:Foo bar.jpg". + * @return {String} + */ + getPrefixedText: function() { + return text( this.getPrefixedDb() ); + }, + + /** + * The main title (without namespace), like "Foo_bar.jpg" + * @return {String} + */ + getMain: function() { + return this.getName() + this.getDotExtension(); + }, + + /** + * The "text" form, like "Foo bar.jpg" + * @return {String} + */ + getMainText: function() { + return text( this.getMain() ); + }, + + /** + * Set the "extension" portion, removing illegal characters. + * @param s {String} + * @return {mw.Title} this + */ + setExtension: function( s ) { + this._ext = clean( s.toLowerCase() ); + return this; + }, + + /** + * Get the extension (returns null if there was none) + * @return {String|null} extension + */ + getExtension: function() { + return this._ext; + }, + + /** + * Convenience method: return string like ".jpg", or "" if no extension + * @return {String} + */ + getDotExtension: function() { + return this._ext === null ? '' : '.' + this._ext; + }, + + /** + * @param s {String} + * @return {mw.Title} this + */ + setAll: function( s ) { + var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w{1,5}))?$/ ); + if ( matches.length ) { + if ( matches[1] ) { this.setNamespace( matches[1] ); } + if ( matches[2] ) { this.setName( matches[2] ); } + if ( matches[3] ) { this.setExtension( matches[3] ); } + } else { + throw new Error( 'mw.Title: Could not parse title "' + s + '"' ); + } + return this; + }, + + /** + * @param s {String} + * @return {mw.Title} this + */ + setNameAndExtension: function( s ) { + var matches = s.match( /^(?:)?(.*?)(?:\.(\w{1,5}))?$/ ); + if ( matches.length ) { + if ( matches[1] ) { this.setName( matches[1] ); } + if ( matches[2] ) { this.setExtension( matches[2] ); } + } else { + throw new Error( 'mw.Title: Could not parse title "' + s + '"' ); + } + return this; + }, + + /** + * Return the URL to this title + * @return {String} + */ + getUrl: function() { + return mw.util.wikiGetlink( this.toString() ); + }, + + /** + * Wether this title exists on the wiki. + * @return {mixed} Boolean true/false if the information is available. Otherwise null. + */ + exists: function() { + return Title.exists( this ); + } + }; + + // Alias + fn.toString = fn.getPrefixedDb; + fn.toText = fn.getPrefixedText; + + // Assign + Title.prototype = fn; + + // Expose + mw.Title = Title; + +})(jQuery); diff --git a/tests/qunit/index.html b/tests/qunit/index.html index 17eb3dd81f..b3f85d0642 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -36,6 +36,7 @@ + @@ -52,6 +53,7 @@ +