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