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