Merge "Update docs/hooks.txt for ShowSearchHitTitle"
[mediawiki.git] / resources / src / mediawiki / mediawiki.Title.js
blob0e2af5060a37fad1006038eb478265f7bf8c1287
1 /*!
2  * @author Neil Kandalgaonkar, 2010
3  * @author Timo Tijhof, 2011-2013
4  * @since 1.18
5  */
7 /* eslint-disable no-use-before-define */
9 ( function ( mw, $ ) {
10         /**
11          * Parse titles into an object structure. Note that when using the constructor
12          * directly, passing invalid titles will result in an exception. Use #newFromText to use the
13          * logic directly and get null for invalid titles which is easier to work with.
14          *
15          * Note that in the constructor and #newFromText method, `namespace` is the **default** namespace
16          * only, and can be overridden by a namespace prefix in `title`. If you do not want this behavior,
17          * use #makeTitle. Compare:
18          *
19          *     new mw.Title( 'Foo', NS_TEMPLATE ).getPrefixedText();                  // => 'Template:Foo'
20          *     mw.Title.newFromText( 'Foo', NS_TEMPLATE ).getPrefixedText();          // => 'Template:Foo'
21          *     mw.Title.makeTitle( NS_TEMPLATE, 'Foo' ).getPrefixedText();            // => 'Template:Foo'
22          *
23          *     new mw.Title( 'Category:Foo', NS_TEMPLATE ).getPrefixedText();         // => 'Category:Foo'
24          *     mw.Title.newFromText( 'Category:Foo', NS_TEMPLATE ).getPrefixedText(); // => 'Category:Foo'
25          *     mw.Title.makeTitle( NS_TEMPLATE, 'Category:Foo' ).getPrefixedText();   // => 'Template:Category:Foo'
26          *
27          *     new mw.Title( 'Template:Foo', NS_TEMPLATE ).getPrefixedText();         // => 'Template:Foo'
28          *     mw.Title.newFromText( 'Template:Foo', NS_TEMPLATE ).getPrefixedText(); // => 'Template:Foo'
29          *     mw.Title.makeTitle( NS_TEMPLATE, 'Template:Foo' ).getPrefixedText();   // => 'Template:Template:Foo'
30          *
31          * @class mw.Title
32          * @constructor
33          * @param {string} title Title of the page. If no second argument given,
34          *  this will be searched for a namespace
35          * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title
36          * @throws {Error} When the title is invalid
37          */
38         function Title( title, namespace ) {
39                 var parsed = parse( title, namespace );
40                 if ( !parsed ) {
41                         throw new Error( 'Unable to parse title' );
42                 }
44                 this.namespace = parsed.namespace;
45                 this.title = parsed.title;
46                 this.ext = parsed.ext;
47                 this.fragment = parsed.fragment;
48         }
50         /* Private members */
52         // eslint-disable-next-line vars-on-top
53         var
54                 namespaceIds = mw.config.get( 'wgNamespaceIds' ),
56                 /**
57                  * @private
58                  * @static
59                  * @property NS_MAIN
60                  */
61                 NS_MAIN = namespaceIds[ '' ],
63                 /**
64                  * @private
65                  * @static
66                  * @property NS_TALK
67                  */
68                 NS_TALK = namespaceIds.talk,
70                 /**
71                  * @private
72                  * @static
73                  * @property NS_SPECIAL
74                  */
75                 NS_SPECIAL = namespaceIds.special,
77                 /**
78                  * @private
79                  * @static
80                  * @property NS_MEDIA
81                  */
82                 NS_MEDIA = namespaceIds.media,
84                 /**
85                  * @private
86                  * @static
87                  * @property NS_FILE
88                  */
89                 NS_FILE = namespaceIds.file,
91                 /**
92                  * @private
93                  * @static
94                  * @property FILENAME_MAX_BYTES
95                  */
96                 FILENAME_MAX_BYTES = 240,
98                 /**
99                  * @private
100                  * @static
101                  * @property TITLE_MAX_BYTES
102                  */
103                 TITLE_MAX_BYTES = 255,
105                 /**
106                  * Get the namespace id from a namespace name (either from the localized, canonical or alias
107                  * name).
108                  *
109                  * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or
110                  * even 'Bild'.
111                  *
112                  * @private
113                  * @static
114                  * @method getNsIdByName
115                  * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored)
116                  * @return {number|boolean} Namespace id or boolean false
117                  */
118                 getNsIdByName = function ( ns ) {
119                         var id;
121                         // Don't cast non-strings to strings, because null or undefined should not result in
122                         // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki)
123                         // Also, toLowerCase throws exception on null/undefined, because it is a String method.
124                         if ( typeof ns !== 'string' ) {
125                                 return false;
126                         }
127                         // TODO: Should just use local var namespaceIds here but it
128                         // breaks test which modify the config
129                         id = mw.config.get( 'wgNamespaceIds' )[ ns.toLowerCase() ];
130                         if ( id === undefined ) {
131                                 return false;
132                         }
133                         return id;
134                 },
136                 /**
137                  * @private
138                  * @method getNamespacePrefix_
139                  * @param {number} namespace
140                  * @return {string}
141                  */
142                 getNamespacePrefix = function ( namespace ) {
143                         return namespace === NS_MAIN ?
144                                 '' :
145                                 ( mw.config.get( 'wgFormattedNamespaces' )[ namespace ].replace( / /g, '_' ) + ':' );
146                 },
148                 rUnderscoreTrim = /^_+|_+$/g,
150                 rSplit = /^(.+?)_*:_*(.*)$/,
152                 // See MediaWikiTitleCodec.php#getTitleInvalidRegex
153                 rInvalid = new RegExp(
154                         '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
155                         // URL percent encoding sequences interfere with the ability
156                         // to round-trip titles -- you can't link to them consistently.
157                         '|%[0-9A-Fa-f]{2}' +
158                         // XML/HTML character references produce similar issues.
159                         '|&[A-Za-z0-9\u0080-\uFFFF]+;' +
160                         '|&#[0-9]+;' +
161                         '|&#x[0-9A-Fa-f]+;'
162                 ),
164                 // From MediaWikiTitleCodec::splitTitleString() in PHP
165                 // Note that this is not equivalent to /\s/, e.g. underscore is included, tab is not included.
166                 rWhitespace = /[ _\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+/g,
168                 // From MediaWikiTitleCodec::splitTitleString() in PHP
169                 rUnicodeBidi = /[\u200E\u200F\u202A-\u202E]/g,
171                 /**
172                  * Slightly modified from Flinfo. Credit goes to Lupo and Flominator.
173                  * @private
174                  * @static
175                  * @property sanitationRules
176                  */
177                 sanitationRules = [
178                         // "signature"
179                         {
180                                 pattern: /~{3}/g,
181                                 replace: '',
182                                 generalRule: true
183                         },
184                         // control characters
185                         {
186                                 // eslint-disable-next-line no-control-regex
187                                 pattern: /[\x00-\x1f\x7f]/g,
188                                 replace: '',
189                                 generalRule: true
190                         },
191                         // URL encoding (possibly)
192                         {
193                                 pattern: /%([0-9A-Fa-f]{2})/g,
194                                 replace: '% $1',
195                                 generalRule: true
196                         },
197                         // HTML-character-entities
198                         {
199                                 pattern: /&(([A-Za-z0-9\x80-\xff]+|#[0-9]+|#x[0-9A-Fa-f]+);)/g,
200                                 replace: '& $1',
201                                 generalRule: true
202                         },
203                         // slash, colon (not supported by file systems like NTFS/Windows, Mac OS 9 [:], ext4 [/])
204                         {
205                                 pattern: new RegExp( '[' + mw.config.get( 'wgIllegalFileChars', '' ) + ']', 'g' ),
206                                 replace: '-',
207                                 fileRule: true
208                         },
209                         // brackets, greater than
210                         {
211                                 pattern: /[\]\}>]/g,
212                                 replace: ')',
213                                 generalRule: true
214                         },
215                         // brackets, lower than
216                         {
217                                 pattern: /[\[\{<]/g,
218                                 replace: '(',
219                                 generalRule: true
220                         },
221                         // everything that wasn't covered yet
222                         {
223                                 pattern: new RegExp( rInvalid.source, 'g' ),
224                                 replace: '-',
225                                 generalRule: true
226                         },
227                         // directory structures
228                         {
229                                 pattern: /^(\.|\.\.|\.\/.*|\.\.\/.*|.*\/\.\/.*|.*\/\.\.\/.*|.*\/\.|.*\/\.\.)$/g,
230                                 replace: '',
231                                 generalRule: true
232                         }
233                 ],
235                 /**
236                  * Internal helper for #constructor and #newFromText.
237                  *
238                  * Based on Title.php#secureAndSplit
239                  *
240                  * @private
241                  * @static
242                  * @method parse
243                  * @param {string} title
244                  * @param {number} [defaultNamespace=NS_MAIN]
245                  * @return {Object|boolean}
246                  */
247                 parse = function ( title, defaultNamespace ) {
248                         var namespace, m, id, i, fragment, ext;
250                         namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
252                         title = title
253                                 // Strip Unicode bidi override characters
254                                 .replace( rUnicodeBidi, '' )
255                                 // Normalise whitespace to underscores and remove duplicates
256                                 .replace( rWhitespace, '_' )
257                                 // Trim underscores
258                                 .replace( rUnderscoreTrim, '' );
260                         // Process initial colon
261                         if ( title !== '' && title[ 0 ] === ':' ) {
262                                 // Initial colon means main namespace instead of specified default
263                                 namespace = NS_MAIN;
264                                 title = title
265                                         // Strip colon
266                                         .slice( 1 )
267                                         // Trim underscores
268                                         .replace( rUnderscoreTrim, '' );
269                         }
271                         if ( title === '' ) {
272                                 return false;
273                         }
275                         // Process namespace prefix (if any)
276                         m = title.match( rSplit );
277                         if ( m ) {
278                                 id = getNsIdByName( m[ 1 ] );
279                                 if ( id !== false ) {
280                                         // Ordinary namespace
281                                         namespace = id;
282                                         title = m[ 2 ];
284                                         // For Talk:X pages, make sure X has no "namespace" prefix
285                                         if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) {
286                                                 // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x)
287                                                 if ( getNsIdByName( m[ 1 ] ) !== false ) {
288                                                         return false;
289                                                 }
290                                         }
291                                 }
292                         }
294                         // Process fragment
295                         i = title.indexOf( '#' );
296                         if ( i === -1 ) {
297                                 fragment = null;
298                         } else {
299                                 fragment = title
300                                         // Get segment starting after the hash
301                                         .slice( i + 1 )
302                                         // Convert to text
303                                         // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo")
304                                         .replace( /_/g, ' ' );
306                                 title = title
307                                         // Strip hash
308                                         .slice( 0, i )
309                                         // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux")
310                                         .replace( rUnderscoreTrim, '' );
311                         }
313                         // Reject illegal characters
314                         if ( title.match( rInvalid ) ) {
315                                 return false;
316                         }
318                         // Disallow titles that browsers or servers might resolve as directory navigation
319                         if (
320                                 title.indexOf( '.' ) !== -1 && (
321                                         title === '.' || title === '..' ||
322                                         title.indexOf( './' ) === 0 ||
323                                         title.indexOf( '../' ) === 0 ||
324                                         title.indexOf( '/./' ) !== -1 ||
325                                         title.indexOf( '/../' ) !== -1 ||
326                                         title.slice( -2 ) === '/.' ||
327                                         title.slice( -3 ) === '/..'
328                                 )
329                         ) {
330                                 return false;
331                         }
333                         // Disallow magic tilde sequence
334                         if ( title.indexOf( '~~~' ) !== -1 ) {
335                                 return false;
336                         }
338                         // Disallow titles exceeding the TITLE_MAX_BYTES byte size limit (size of underlying database field)
339                         // Except for special pages, e.g. [[Special:Block/Long name]]
340                         // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should
341                         // be less than 512 bytes.
342                         if ( namespace !== NS_SPECIAL && $.byteLength( title ) > TITLE_MAX_BYTES ) {
343                                 return false;
344                         }
346                         // Can't make a link to a namespace alone.
347                         if ( title === '' && namespace !== NS_MAIN ) {
348                                 return false;
349                         }
351                         // Any remaining initial :s are illegal.
352                         if ( title[ 0 ] === ':' ) {
353                                 return false;
354                         }
356                         // For backwards-compatibility with old mw.Title, we separate the extension from the
357                         // rest of the title.
358                         i = title.lastIndexOf( '.' );
359                         if ( i === -1 || title.length <= i + 1 ) {
360                                 // Extensions are the non-empty segment after the last dot
361                                 ext = null;
362                         } else {
363                                 ext = title.slice( i + 1 );
364                                 title = title.slice( 0, i );
365                         }
367                         return {
368                                 namespace: namespace,
369                                 title: title,
370                                 ext: ext,
371                                 fragment: fragment
372                         };
373                 },
375                 /**
376                  * Convert db-key to readable text.
377                  *
378                  * @private
379                  * @static
380                  * @method text
381                  * @param {string} s
382                  * @return {string}
383                  */
384                 text = function ( s ) {
385                         if ( s !== null && s !== undefined ) {
386                                 return s.replace( /_/g, ' ' );
387                         } else {
388                                 return '';
389                         }
390                 },
392                 /**
393                  * Sanitizes a string based on a rule set and a filter
394                  *
395                  * @private
396                  * @static
397                  * @method sanitize
398                  * @param {string} s
399                  * @param {Array} filter
400                  * @return {string}
401                  */
402                 sanitize = function ( s, filter ) {
403                         var i, ruleLength, rule, m, filterLength,
404                                 rules = sanitationRules;
406                         for ( i = 0, ruleLength = rules.length; i < ruleLength; ++i ) {
407                                 rule = rules[ i ];
408                                 for ( m = 0, filterLength = filter.length; m < filterLength; ++m ) {
409                                         if ( rule[ filter[ m ] ] ) {
410                                                 s = s.replace( rule.pattern, rule.replace );
411                                         }
412                                 }
413                         }
414                         return s;
415                 },
417                 /**
418                  * Cuts a string to a specific byte length, assuming UTF-8
419                  * or less, if the last character is a multi-byte one
420                  *
421                  * @private
422                  * @static
423                  * @method trimToByteLength
424                  * @param {string} s
425                  * @param {number} length
426                  * @return {string}
427                  */
428                 trimToByteLength = function ( s, length ) {
429                         var byteLength, chopOffChars, chopOffBytes;
431                         // bytelength is always greater or equal to the length in characters
432                         s = s.substr( 0, length );
433                         while ( ( byteLength = $.byteLength( s ) ) > length ) {
434                                 // Calculate how many characters can be safely removed
435                                 // First, we need to know how many bytes the string exceeds the threshold
436                                 chopOffBytes = byteLength - length;
437                                 // A character in UTF-8 is at most 4 bytes
438                                 // One character must be removed in any case because the
439                                 // string is too long
440                                 chopOffChars = Math.max( 1, Math.floor( chopOffBytes / 4 ) );
441                                 s = s.substr( 0, s.length - chopOffChars );
442                         }
443                         return s;
444                 },
446                 /**
447                  * Cuts a file name to a specific byte length
448                  *
449                  * @private
450                  * @static
451                  * @method trimFileNameToByteLength
452                  * @param {string} name without extension
453                  * @param {string} extension file extension
454                  * @return {string} The full name, including extension
455                  */
456                 trimFileNameToByteLength = function ( name, extension ) {
457                         // There is a special byte limit for file names and ... remember the dot
458                         return trimToByteLength( name, FILENAME_MAX_BYTES - extension.length - 1 ) + '.' + extension;
459                 },
461                 // Polyfill for ES5 Object.create
462                 createObject = Object.create || ( function () {
463                         return function ( o ) {
464                                 function Title() {}
465                                 if ( o !== Object( o ) ) {
466                                         throw new Error( 'Cannot inherit from a non-object' );
467                                 }
468                                 Title.prototype = o;
469                                 return new Title();
470                         };
471                 }() );
473         /* Static members */
475         /**
476          * Constructor for Title objects with a null return instead of an exception for invalid titles.
477          *
478          * Note that `namespace` is the **default** namespace only, and can be overridden by a namespace
479          * prefix in `title`. If you do not want this behavior, use #makeTitle. See #constructor for
480          * details.
481          *
482          * @static
483          * @param {string} title
484          * @param {number} [namespace=NS_MAIN] Default namespace
485          * @return {mw.Title|null} A valid Title object or null if the title is invalid
486          */
487         Title.newFromText = function ( title, namespace ) {
488                 var t, parsed = parse( title, namespace );
489                 if ( !parsed ) {
490                         return null;
491                 }
493                 t = createObject( Title.prototype );
494                 t.namespace = parsed.namespace;
495                 t.title = parsed.title;
496                 t.ext = parsed.ext;
497                 t.fragment = parsed.fragment;
499                 return t;
500         };
502         /**
503          * Constructor for Title objects with predefined namespace.
504          *
505          * Unlike #newFromText or #constructor, this function doesn't allow the given `namespace` to be
506          * overridden by a namespace prefix in `title`. See #constructor for details about this behavior.
507          *
508          * The single exception to this is when `namespace` is 0, indicating the main namespace. The
509          * function behaves like #newFromText in that case.
510          *
511          * @static
512          * @param {number} namespace Namespace to use for the title
513          * @param {string} title
514          * @return {mw.Title|null} A valid Title object or null if the title is invalid
515          */
516         Title.makeTitle = function ( namespace, title ) {
517                 return mw.Title.newFromText( getNamespacePrefix( namespace ) + title );
518         };
520         /**
521          * Constructor for Title objects from user input altering that input to
522          * produce a title that MediaWiki will accept as legal
523          *
524          * @static
525          * @param {string} title
526          * @param {number} [defaultNamespace=NS_MAIN]
527          *  If given, will used as default namespace for the given title.
528          * @param {Object} [options] additional options
529          * @param {boolean} [options.forUploading=true]
530          *  Makes sure that a file is uploadable under the title returned.
531          *  There are pages in the file namespace under which file upload is impossible.
532          *  Automatically assumed if the title is created in the Media namespace.
533          * @return {mw.Title|null} A valid Title object or null if the input cannot be turned into a valid title
534          */
535         Title.newFromUserInput = function ( title, defaultNamespace, options ) {
536                 var namespace, m, id, ext, parts;
538                 // defaultNamespace is optional; check whether options moves up
539                 if ( arguments.length < 3 && $.type( defaultNamespace ) === 'object' ) {
540                         options = defaultNamespace;
541                         defaultNamespace = undefined;
542                 }
544                 // merge options into defaults
545                 options = $.extend( {
546                         forUploading: true
547                 }, options );
549                 namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
551                 // Normalise additional whitespace
552                 title = $.trim( title.replace( /\s/g, ' ' ) );
554                 // Process initial colon
555                 if ( title !== '' && title[ 0 ] === ':' ) {
556                         // Initial colon means main namespace instead of specified default
557                         namespace = NS_MAIN;
558                         title = title
559                                 // Strip colon
560                                 .substr( 1 )
561                                 // Trim underscores
562                                 .replace( rUnderscoreTrim, '' );
563                 }
565                 // Process namespace prefix (if any)
566                 m = title.match( rSplit );
567                 if ( m ) {
568                         id = getNsIdByName( m[ 1 ] );
569                         if ( id !== false ) {
570                                 // Ordinary namespace
571                                 namespace = id;
572                                 title = m[ 2 ];
573                         }
574                 }
576                 if (
577                         namespace === NS_MEDIA ||
578                         ( options.forUploading && ( namespace === NS_FILE ) )
579                 ) {
581                         title = sanitize( title, [ 'generalRule', 'fileRule' ] );
583                         // Operate on the file extension
584                         // Although it is possible having spaces between the name and the ".ext" this isn't nice for
585                         // operating systems hiding file extensions -> strip them later on
586                         parts = title.split( '.' );
588                         if ( parts.length > 1 ) {
590                                 // Get the last part, which is supposed to be the file extension
591                                 ext = parts.pop();
593                                 // Remove whitespace of the name part (that W/O extension)
594                                 title = $.trim( parts.join( '.' ) );
596                                 // Cut, if too long and append file extension
597                                 title = trimFileNameToByteLength( title, ext );
599                         } else {
601                                 // Missing file extension
602                                 title = $.trim( parts.join( '.' ) );
604                                 // Name has no file extension and a fallback wasn't provided either
605                                 return null;
606                         }
607                 } else {
609                         title = sanitize( title, [ 'generalRule' ] );
611                         // Cut titles exceeding the TITLE_MAX_BYTES byte size limit
612                         // (size of underlying database field)
613                         if ( namespace !== NS_SPECIAL ) {
614                                 title = trimToByteLength( title, TITLE_MAX_BYTES );
615                         }
616                 }
618                 // Any remaining initial :s are illegal.
619                 title = title.replace( /^\:+/, '' );
621                 return Title.newFromText( title, namespace );
622         };
624         /**
625          * Sanitizes a file name as supplied by the user, originating in the user's file system
626          * so it is most likely a valid MediaWiki title and file name after processing.
627          * Returns null on fatal errors.
628          *
629          * @static
630          * @param {string} uncleanName The unclean file name including file extension but
631          *   without namespace
632          * @return {mw.Title|null} A valid Title object or null if the title is invalid
633          */
634         Title.newFromFileName = function ( uncleanName ) {
636                 return Title.newFromUserInput( 'File:' + uncleanName, {
637                         forUploading: true
638                 } );
639         };
641         /**
642          * Get the file title from an image element
643          *
644          *     var title = mw.Title.newFromImg( $( 'img:first' ) );
645          *
646          * @static
647          * @param {HTMLElement|jQuery} img The image to use as a base
648          * @return {mw.Title|null} The file title or null if unsuccessful
649          */
650         Title.newFromImg = function ( img ) {
651                 var matches, i, regex, src, decodedSrc,
653                         // thumb.php-generated thumbnails
654                         thumbPhpRegex = /thumb\.php/,
655                         regexes = [
656                                 // Thumbnails
657                                 /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[^\s\/]+-[^\s\/]*$/,
659                                 // Full size images
660                                 /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/,
662                                 // Thumbnails in non-hashed upload directories
663                                 /\/([^\s\/]+)\/[^\s\/]+-(?:\1|thumbnail)[^\s\/]*$/,
665                                 // Full-size images in non-hashed upload directories
666                                 /\/([^\s\/]+)$/
667                         ],
669                         recount = regexes.length;
671                 src = img.jquery ? img[ 0 ].src : img.src;
673                 matches = src.match( thumbPhpRegex );
675                 if ( matches ) {
676                         return mw.Title.newFromText( 'File:' + mw.util.getParamValue( 'f', src ) );
677                 }
679                 decodedSrc = decodeURIComponent( src );
681                 for ( i = 0; i < recount; i++ ) {
682                         regex = regexes[ i ];
683                         matches = decodedSrc.match( regex );
685                         if ( matches && matches[ 1 ] ) {
686                                 return mw.Title.newFromText( 'File:' + matches[ 1 ] );
687                         }
688                 }
690                 return null;
691         };
693         /**
694          * Whether this title exists on the wiki.
695          *
696          * @static
697          * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title
698          * @return {boolean|null} Boolean if the information is available, otherwise null
699          */
700         Title.exists = function ( title ) {
701                 var match,
702                         type = $.type( title ),
703                         obj = Title.exist.pages;
705                 if ( type === 'string' ) {
706                         match = obj[ title ];
707                 } else if ( type === 'object' && title instanceof Title ) {
708                         match = obj[ title.toString() ];
709                 } else {
710                         throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' );
711                 }
713                 if ( typeof match === 'boolean' ) {
714                         return match;
715                 }
717                 return null;
718         };
720         /**
721          * Store page existence
722          *
723          * @static
724          * @property {Object} exist
725          * @property {Object} exist.pages Keyed by title. Boolean true value indicates page does exist.
726          *
727          * @property {Function} exist.set The setter function.
728          *
729          *  Example to declare existing titles:
730          *
731          *     Title.exist.set( ['User:John_Doe', ...] );
732          *
733          *  Example to declare titles nonexistent:
734          *
735          *     Title.exist.set( ['File:Foo_bar.jpg', ...], false );
736          *
737          * @property {string|Array} exist.set.titles Title(s) in strict prefixedDb title form
738          * @property {boolean} [exist.set.state=true] State of the given titles
739          * @return {boolean}
740          */
741         Title.exist = {
742                 pages: {},
744                 set: function ( titles, state ) {
745                         var i, len,
746                                 pages = this.pages;
748                         titles = $.isArray( titles ) ? titles : [ titles ];
749                         state = state === undefined ? true : !!state;
751                         for ( i = 0, len = titles.length; i < len; i++ ) {
752                                 pages[ titles[ i ] ] = state;
753                         }
754                         return true;
755                 }
756         };
758         /**
759          * Normalize a file extension to the common form, making it lowercase and checking some synonyms,
760          * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded.
761          * Keep in sync with File::normalizeExtension() in PHP.
762          *
763          * @param {string} extension File extension (without the leading dot)
764          * @return {string} File extension in canonical form
765          */
766         Title.normalizeExtension = function ( extension ) {
767                 var
768                         lower = extension.toLowerCase(),
769                         squish = {
770                                 htm: 'html',
771                                 jpeg: 'jpg',
772                                 mpeg: 'mpg',
773                                 tiff: 'tif',
774                                 ogv: 'ogg'
775                         };
776                 if ( squish.hasOwnProperty( lower ) ) {
777                         return squish[ lower ];
778                 } else if ( /^[0-9a-z]+$/.test( lower ) ) {
779                         return lower;
780                 } else {
781                         return '';
782                 }
783         };
785         /* Public members */
787         Title.prototype = {
788                 constructor: Title,
790                 /**
791                  * Get the namespace number
792                  *
793                  * Example: 6 for "File:Example_image.svg".
794                  *
795                  * @return {number}
796                  */
797                 getNamespaceId: function () {
798                         return this.namespace;
799                 },
801                 /**
802                  * Get the namespace prefix (in the content language)
803                  *
804                  * Example: "File:" for "File:Example_image.svg".
805                  * In #NS_MAIN this is '', otherwise namespace name plus ':'
806                  *
807                  * @return {string}
808                  */
809                 getNamespacePrefix: function () {
810                         return getNamespacePrefix( this.namespace );
811                 },
813                 /**
814                  * Get the page name without extension or namespace prefix
815                  *
816                  * Example: "Example_image" for "File:Example_image.svg".
817                  *
818                  * For the page title (full page name without namespace prefix), see #getMain.
819                  *
820                  * @return {string}
821                  */
822                 getName: function () {
823                         if (
824                                 $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ||
825                                 !this.title.length
826                         ) {
827                                 return this.title;
828                         }
829                         // PHP's strtoupper differs from String.toUpperCase in a number of cases
830                         // Bug: T147646
831                         return mw.Title.phpCharToUpper( this.title[ 0 ] ) + this.title.slice( 1 );
832                 },
834                 /**
835                  * Get the page name (transformed by #text)
836                  *
837                  * Example: "Example image" for "File:Example_image.svg".
838                  *
839                  * For the page title (full page name without namespace prefix), see #getMainText.
840                  *
841                  * @return {string}
842                  */
843                 getNameText: function () {
844                         return text( this.getName() );
845                 },
847                 /**
848                  * Get the extension of the page name (if any)
849                  *
850                  * @return {string|null} Name extension or null if there is none
851                  */
852                 getExtension: function () {
853                         return this.ext;
854                 },
856                 /**
857                  * Shortcut for appendable string to form the main page name.
858                  *
859                  * Returns a string like ".json", or "" if no extension.
860                  *
861                  * @return {string}
862                  */
863                 getDotExtension: function () {
864                         return this.ext === null ? '' : '.' + this.ext;
865                 },
867                 /**
868                  * Get the main page name
869                  *
870                  * Example: "Example_image.svg" for "File:Example_image.svg".
871                  *
872                  * @return {string}
873                  */
874                 getMain: function () {
875                         return this.getName() + this.getDotExtension();
876                 },
878                 /**
879                  * Get the main page name (transformed by #text)
880                  *
881                  * Example: "Example image.svg" for "File:Example_image.svg".
882                  *
883                  * @return {string}
884                  */
885                 getMainText: function () {
886                         return text( this.getMain() );
887                 },
889                 /**
890                  * Get the full page name
891                  *
892                  * Example: "File:Example_image.svg".
893                  * Most useful for API calls, anything that must identify the "title".
894                  *
895                  * @return {string}
896                  */
897                 getPrefixedDb: function () {
898                         return this.getNamespacePrefix() + this.getMain();
899                 },
901                 /**
902                  * Get the full page name (transformed by #text)
903                  *
904                  * Example: "File:Example image.svg" for "File:Example_image.svg".
905                  *
906                  * @return {string}
907                  */
908                 getPrefixedText: function () {
909                         return text( this.getPrefixedDb() );
910                 },
912                 /**
913                  * Get the page name relative to a namespace
914                  *
915                  * Example:
916                  *
917                  * - "Foo:Bar" relative to the Foo namespace becomes "Bar".
918                  * - "Bar" relative to any non-main namespace becomes ":Bar".
919                  * - "Foo:Bar" relative to any namespace other than Foo stays "Foo:Bar".
920                  *
921                  * @param {number} namespace The namespace to be relative to
922                  * @return {string}
923                  */
924                 getRelativeText: function ( namespace ) {
925                         if ( this.getNamespaceId() === namespace ) {
926                                 return this.getMainText();
927                         } else if ( this.getNamespaceId() === NS_MAIN ) {
928                                 return ':' + this.getPrefixedText();
929                         } else {
930                                 return this.getPrefixedText();
931                         }
932                 },
934                 /**
935                  * Get the fragment (if any).
936                  *
937                  * Note that this method (by design) does not include the hash character and
938                  * the value is not url encoded.
939                  *
940                  * @return {string|null}
941                  */
942                 getFragment: function () {
943                         return this.fragment;
944                 },
946                 /**
947                  * Get the URL to this title
948                  *
949                  * @see mw.util#getUrl
950                  * @param {Object} [params] A mapping of query parameter names to values,
951                  *     e.g. `{ action: 'edit' }`.
952                  * @return {string}
953                  */
954                 getUrl: function ( params ) {
955                         var fragment = this.getFragment();
956                         if ( fragment ) {
957                                 return mw.util.getUrl( this.toString() + '#' + fragment, params );
958                         } else {
959                                 return mw.util.getUrl( this.toString(), params );
960                         }
961                 },
963                 /**
964                  * Whether this title exists on the wiki.
965                  *
966                  * @see #static-method-exists
967                  * @return {boolean|null} Boolean if the information is available, otherwise null
968                  */
969                 exists: function () {
970                         return Title.exists( this );
971                 }
972         };
974         /**
975          * @alias #getPrefixedDb
976          * @method
977          */
978         Title.prototype.toString = Title.prototype.getPrefixedDb;
980         /**
981          * @alias #getPrefixedText
982          * @method
983          */
984         Title.prototype.toText = Title.prototype.getPrefixedText;
986         // Expose
987         mw.Title = Title;
989 }( mediaWiki, jQuery ) );