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