Implement mw.Title in core
authorKrinkle <krinkle@users.mediawiki.org>
Sat, 18 Jun 2011 09:17:09 +0000 (09:17 +0000)
committerKrinkle <krinkle@users.mediawiki.org>
Sat, 18 Jun 2011 09:17:09 +0000 (09:17 +0000)
* 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

RELEASE-NOTES-1.19
resources/Resources.php
resources/jquery/jquery.qunit.completenessTest.js
resources/mediawiki/mediawiki.Title.js [new file with mode: 0644]
tests/qunit/index.html
tests/qunit/jquery.qunit.completenessTest.config.js

index 2c078b2..f0490a1 100644 (file)
@@ -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
index ce6e365..0291d39 100644 (file)
@@ -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(
index e98ed87..7d4f346 100644 (file)
@@ -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 (file)
index 0000000..a3459ac
--- /dev/null
@@ -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);
index 17eb3dd..b3f85d0 100644 (file)
@@ -36,6 +36,7 @@
        <script src="../../resources/jquery/jquery.autoEllipsis.js"></script>
        <script src="../../resources/jquery/jquery.colorUtil.js"></script>
        <script src="../../resources/jquery/jquery.tabIndex.js"></script>
+       <script src="../../resources/mediawiki/mediawiki.Title.js"></script>
 
        <!-- QUnit: Load framework -->
        <link rel="stylesheet" href="../../resources/jquery/jquery.qunit.css" />
@@ -52,6 +53,7 @@
        <script src="suites/resources/jquery/jquery.autoEllipsis.js"></script>
        <script src="suites/resources/jquery/jquery.colorUtil.js"></script>
        <script src="suites/resources/jquery/jquery.tabIndex.js"></script>
+       <script src="suites/resources/mediawiki/mediawiki.Title.js"></script>
 
        <!-- TestSwarm: If a test swarm is running this,
             the following script will allow it to extract the results.
index d3eefe2..c03ddc9 100644 (file)
@@ -1,13 +1,18 @@
 // Return true to ignore
 var mwTestIgnore = function( val, tester, funcPath ) {
 
-       // Don't record methods of the properties of mw.Map instances
-       // Because we're therefor skipping any injection for
-       // "new mw.Map()", manually set it to true here.
+       // Don't record methods of the properties of constructors,
+       // to avoid getting into a loop (prototype.constructor.prototype..).
+       // Since we're therefor skipping any injection for
+       // "new mw.Foo()", manually set it to true here.
        if ( val instanceof mw.Map ) {
                tester.methodCallTracker['Map'] = true;
                return true;
        }
+       if ( val instanceof mw.Title ) {
+               tester.methodCallTracker['Title'] = true;
+               return true;
+       }
 
        // Don't record methods of the properties of a jQuery object
        if ( val instanceof $ ) {