return false;
}
- if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
+ if ( strpos( $dbkey, UTF8_REPLACEMENT ) !== false ) {
# Contained illegal UTF-8 sequences or forbidden Unicode chars.
return false;
}
# Can't make a link to a namespace alone... "empty" local links can only be
# self-links with a fragment identifier.
+ # TODO: Why do we exclude NS_MAIN (bug 54044)
if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) {
return false;
}
if ( $createRedirect ) {
$contentHandler = ContentHandler::getForTitle( $this );
- $redirectContent = $contentHandler->makeRedirectContent( $nt );
+ $redirectContent = $contentHandler->makeRedirectContent( $nt,
+ wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
// NOTE: If this page's content model does not support redirects, $redirectContent will be null.
} else {
* @since 1.21
*
* @param Title $destination the page to redirect to.
+ * @param string $text text to include in the redirect, if possible.
*
* @return Content
*/
- public function makeRedirectContent( Title $destination ) {
+ public function makeRedirectContent( Title $destination, $text = '' ) {
return null;
}
* @see ContentHandler::makeRedirectContent
*
* @param Title $destination the page to redirect to.
+ * @param string $text text to include in the redirect, if possible.
*
* @return Content
*/
- public function makeRedirectContent( Title $destination ) {
+ public function makeRedirectContent( Title $destination, $text = '' ) {
$optionalColon = '';
if ( $destination->getNamespace() == NS_CATEGORY ) {
$mwRedir = MagicWord::get( 'redirect' );
$redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $optionalColon . $destination->getFullText() . ']]';
+ if ( $text != '' ) {
+ $redirectText .= "\n" . $text;
+ }
return new WikitextContent( $redirectText );
}
}
public function getCoalesceLocationInternal() {
- return $this->cluster ? "DBCluster:{$this->cluster}" : "LBFactory:{$this->wiki}";
+ return $this->cluster
+ ? "DBCluster:{$this->cluster}:{$this->wiki}"
+ : "LBFactory:{$this->wiki}";
}
protected function doGetSiblingQueuesWithJobs( array $types ) {
<?php
/**
- * lessphp v0.4.0@6e8e724fc7
+ * lessphp v0.4.0@261f1bd28f
* http://leafo.net/lessphp
*
- * LESS css compiler, adapted from http://lesscss.org
+ * LESS CSS compiler, adapted from http://lesscss.org
*
* For ease of distribution, lessphp 0.4.0 is under a dual license.
* You are free to pick which one suits your needs.
/**
- * The less compiler and parser.
+ * The LESS compiler and parser.
*
* Converting LESS to CSS is a three stage process. The incoming file is parsed
* by `lessc_parser` into a syntax tree, then it is compiled into another tree
*/
class lessc {
static public $VERSION = "v0.4.0";
- static protected $TRUE = array("keyword", "true");
- static protected $FALSE = array("keyword", "false");
+
+ static public $TRUE = array("keyword", "true");
+ static public $FALSE = array("keyword", "false");
protected $libFunctions = array();
protected $registeredVars = array();
foreach ($this->sortProps($block->props) as $prop) {
$this->compileProp($prop, $block, $out);
}
+ $out->lines = $this->deduplicate($out->lines);
+ }
+
+ /**
+ * Deduplicate lines in a block. Comments are not deduplicated. If a
+ * duplicate rule is detected, the comments immediately preceding each
+ * occurence are consolidated.
+ */
+ protected function deduplicate($lines) {
+ $unique = array();
+ $comments = array();
- $out->lines = array_values(array_unique($out->lines));
+ foreach($lines as $line) {
+ if (strpos($line, '/*') === 0) {
+ $comments[] = $line;
+ continue;
+ }
+ if (!in_array($line, $unique)) {
+ $unique[] = $line;
+ }
+ array_splice($unique, array_search($line, $unique), 0, $comments);
+ $comments = array();
+ }
+ return array_merge($unique, $comments);
}
protected function sortProps($props, $split = false) {
$vars = array();
$imports = array();
$other = array();
+ $stack = array();
foreach ($props as $prop) {
switch ($prop[0]) {
+ case "comment":
+ $stack[] = $prop;
+ break;
case "assign":
+ $stack[] = $prop;
if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
- $vars[] = $prop;
+ $vars = array_merge($vars, $stack);
} else {
- $other[] = $prop;
+ $other = array_merge($other, $stack);
}
+ $stack = array();
break;
case "import":
$id = self::$nextImportId++;
$prop[] = $id;
- $imports[] = $prop;
+ $stack[] = $prop;
+ $imports = array_merge($imports, $stack);
$other[] = array("import_mixin", $id);
+ $stack = array();
break;
default:
- $other[] = $prop;
+ $stack[] = $prop;
+ $other = array_merge($other, $stack);
+ $stack = array();
+ break;
}
}
+ $other = array_merge($other, $stack);
if ($split) {
return array(array_merge($vars, $imports), $other);
* Helper function to get arguments for color manipulation functions.
* takes a list that contains a color like thing and a percentage
*/
- protected function colorArgs($args) {
+ public function colorArgs($args) {
if ($args[0] != 'list' || count($args[2]) < 2) {
return array(array('color', 0, 0, 0), 0);
}
return $lightColor;
}
- protected function assertColor($value, $error = "expected color value") {
+ public function assertColor($value, $error = "expected color value") {
$color = $this->coerceColor($value);
if (is_null($color)) $this->throwError($error);
return $color;
}
- protected function assertNumber($value, $error = "expecting number") {
+ public function assertNumber($value, $error = "expecting number") {
if ($value[0] == "number") return $value[1];
$this->throwError($error);
}
- protected function assertArgs($value, $expectedArgs, $name="") {
+ public function assertArgs($value, $expectedArgs, $name="") {
if ($expectedArgs == 1) {
return $value;
} else {
return $value;
}
- protected function toBool($a) {
+ public function toBool($a) {
if ($a) return self::$TRUE;
else return self::$FALSE;
}
/**
* Uses the current value of $this->count to show line and line number
*/
- protected function throwError($msg = null) {
+ public function throwError($msg = null) {
if ($this->sourceLoc >= 0) {
$this->sourceParser->throwError($msg, $this->sourceLoc);
}
$this->whitespace();
// parse the entire file
- $lastCount = $this->count;
while (false !== $this->parseChunk());
if ($this->count != strlen($this->buffer))
if (empty($this->buffer)) return false;
$s = $this->seek();
+ if ($this->whitespace()) {
+ return true;
+ }
+
// setting a property
if ($this->keyword($key) && $this->assign() &&
$this->propertyValue($value, $key) && $this->end())
}
// opening a simple block
- if ($this->tags($tags) && $this->literal('{')) {
+ if ($this->tags($tags) && $this->literal('{', false)) {
$tags = $this->fixTags($tags);
$this->pushBlock($tags);
return true;
// an import statement
protected function import(&$out) {
- $s = $this->seek();
if (!$this->literal('@import')) return false;
// @import "something.css" media;
// list of tags of specifying mixin path
// optionally separated by > (lazy, accepts extra >)
protected function mixinTags(&$tags) {
- $s = $this->seek();
$tags = array();
while ($this->tag($tt, true)) {
$tags[] = $tt;
// consume an end of statement delimiter
protected function end() {
- if ($this->literal(';')) {
+ if ($this->literal(';', false)) {
return true;
} elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
// if there is end of file or a closing block next then we don't need a ;
if ($this->writeComments) {
$gotWhite = false;
while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
- if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
+ if (isset($m[1]) && empty($this->seenComments[$this->count])) {
$this->append(array("comment", $m[1]));
- $this->commentsSeen[$this->count] = true;
+ $this->seenComments[$this->count] = true;
}
$this->count += strlen($m[0]);
$gotWhite = true;
}
}
- // Old message 'contribsub' had one parameter, but that doesn't work for
- // languages that want to put the "for" bit right after $user but before
- // $links. If 'contribsub' is around, use it for reverse compatibility,
- // otherwise use 'contribsub2'.
- // @todo Should this be removed at some point?
- $oldMsg = $this->msg( 'contribsub' );
- if ( $oldMsg->exists() ) {
- $linksWithParentheses = $this->msg( 'parentheses' )->rawParams( $links )->escaped();
-
- return $oldMsg->rawParams( "$user $linksWithParentheses" );
- }
-
- return $this->msg( 'contribsub2' )->rawParams( $user, $links );
+ return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
}
/**
}
}
- // Old message 'contribsub' had one parameter, but that doesn't work for
- // languages that want to put the "for" bit right after $user but before
- // $links. If 'contribsub' is around, use it for reverse compatibility,
- // otherwise use 'contribsub2'.
- $oldMsg = $this->msg( 'contribsub' );
- if ( $oldMsg->exists() ) {
- return $oldMsg->rawParams( "$user ($links)" );
- }
-
- return $this->msg( 'contribsub2' )->rawParams( $user, $links );
+ return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
}
/**
'contributions-summary' => '', # do not translate or duplicate this message to other languages
'contributions-title' => 'User contributions for $1',
'mycontris' => 'Contributions',
-'contribsub2' => 'For $1 ($2)',
+'contribsub2' => 'For {{GENDER:$3|$1}} ($2)',
'nocontribs' => 'No changes were found matching these criteria.',
'uctop' => '(current)',
'month' => 'From month (and earlier):',
'movesubpagetext' => 'This page has $1 {{PLURAL:$1|subpage|subpages}} shown below.',
'movenosubpage' => 'This page has no subpages.',
'movereason' => 'Reason:',
+'move-redirect-text' => '', # do not translate or duplicate this message to other languages
'revertmove' => 'revert',
'delete_and_move' => 'Delete and move',
'delete_and_move_text' => '== Deletion required ==
* {{msg-mw|Tooltip-pt-mycontris}}
{{Identical|Contribution}}',
'contribsub2' => 'Contributions for "user" (links). Parameters:
-* $1 - any one of the following:
-** IP address (if anonymous user)
-** username, with a link which points to the user page (if registered user)
-* $2 - list of tool links. The list contains a link which has text {{msg-mw|Sp-contributions-talk}}
+* $1 is an IP address or a username, with a link which points to the user page (if registered user).
+* $2 is list of tool links. The list contains a link which has text {{msg-mw|Sp-contributions-talk}}.
+* $3 is a plain text username used for GENDER.
{{Identical|For $1}}',
'nocontribs' => 'Used in [[Special:Contributions]] and [[Special:DeletedContributions]].
'redirect-text',
'edithelppage',
'autocomment-prefix',
+ 'move-redirect-text',
);
/** Optional messages, which may be translated only if changed in the target language. */
'movesubpagetext',
'movenosubpage',
'movereason',
+ 'move-redirect-text',
'revertmove',
'delete_and_move',
'delete_and_move_text',
/*!
* @author Neil Kandalgaonkar, 2010
- * @author Timo Tijhof, 2011
+ * @author Timo Tijhof, 2011-2013
* @since 1.18
- *
- * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink
*/
( function ( mw, $ ) {
- /* Local space */
-
/**
* @class mw.Title
*
+ * Parse titles into an object struture. Note that when using the constructor
+ * directly, passing invalid titles will result in an exception. Use
+ * #newFromText to use the logic directly and get null for invalid titles
+ * which is easier to work with.
+ *
* @constructor
* @param {string} title Title of the page. If no second argument given,
- * this will be searched for a namespace.
- * @param {number} [namespace] Namespace id. If given, title will be taken as-is.
+ * this will be searched for a namespace
+ * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title
+ * @throws {Error} When the title is invalid
*/
function Title( title, namespace ) {
- this.ns = 0; // integer namespace id
- this.name = null; // name in canonical 'database' form
- this.ext = null; // extension
-
- if ( arguments.length === 2 ) {
- setNameAndExtension( this, title );
- this.ns = fixNsId( namespace );
- } else if ( arguments.length === 1 ) {
- setAll( this, title );
+ var parsed = parse( title, namespace );
+ if ( !parsed ) {
+ throw new Error( 'Unable to parse title' );
}
+
+ this.namespace = parsed.namespace;
+ this.title = parsed.title;
+ this.ext = parsed.ext;
+ this.fragment = parsed.fragment;
+
return this;
}
-var
- /* Public methods (defined later) */
- fn,
+ /* Private members */
+
+ var
/**
- * Strip some illegal chars: control chars, colon, less than, greater than,
- * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity
- * intact, like unicode bidi chars, but it's a good start..
- * @ignore
- * @param {string} s
- * @return {string}
+ * @private
+ * @static
+ * @property NS_MAIN
*/
- clean = function ( s ) {
- if ( s !== undefined ) {
- return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' );
- }
- },
+ NS_MAIN = 0,
/**
- * Convert db-key to readable text.
- * @ignore
- * @param {string} s
- * @return {string}
+ * @private
+ * @static
+ * @property NS_TALK
*/
- text = function ( s ) {
- if ( s !== null && s !== undefined ) {
- return s.replace( /_/g, ' ' );
- } else {
- return '';
- }
- },
+ NS_TALK = 1,
/**
- * Sanitize name.
- * @ignore
+ * @private
+ * @static
+ * @property NS_SPECIAL
*/
- fixName = function ( s ) {
- return clean( $.trim( s ) );
- },
+ NS_SPECIAL = -1,
/**
- * Sanitize extension.
- * @ignore
+ * Get the namespace id from a namespace name (either from the localized, canonical or alias
+ * name).
+ *
+ * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or
+ * even 'Bild'.
+ *
+ * @private
+ * @static
+ * @method getNsIdByName
+ * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored)
+ * @return {number|boolean} Namespace id or boolean false
*/
- fixExt = function ( s ) {
- return clean( s );
+ getNsIdByName = function ( ns ) {
+ var id;
+
+ // Don't cast non-strings to strings, because null or undefined should not result in
+ // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki)
+ // Also, toLowerCase throws exception on null/undefined, because it is a String method.
+ if ( typeof ns !== 'string' ) {
+ return false;
+ }
+ ns = ns.toLowerCase();
+ id = mw.config.get( 'wgNamespaceIds' )[ns];
+ if ( id === undefined ) {
+ return false;
+ }
+ return id;
},
+ rUnderscoreTrim = /^_+|_+$/g,
+
+ rSplit = /^(.+?)_*:_*(.*)$/,
+
+ // See Title.php#getTitleInvalidRegex
+ rInvalid = new RegExp(
+ '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
+ // URL percent encoding sequences interfere with the ability
+ // to round-trip titles -- you can't link to them consistently.
+ '|%[0-9A-Fa-f]{2}' +
+ // XML/HTML character references produce similar issues.
+ '|&[A-Za-z0-9\u0080-\uFFFF]+;' +
+ '|&#[0-9]+;' +
+ '|&#x[0-9A-Fa-f]+;'
+ ),
+
/**
- * Sanitize namespace id.
- * @ignore
- * @param id {Number} Namespace id.
- * @return {Number|Boolean} The id as-is or boolean false if invalid.
+ * Internal helper for #constructor and #newFromtext.
+ *
+ * Based on Title.php#secureAndSplit
+ *
+ * @private
+ * @static
+ * @method parse
+ * @param {string} title
+ * @param {number} [defaultNamespace=NS_MAIN]
+ * @return {Object|boolean}
*/
- fixNsId = function ( id ) {
- // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] )
- var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()];
+ parse = function ( title, defaultNamespace ) {
+ var namespace, m, id, i, fragment, ext;
+
+ namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
+
+ title = title
+ // Normalise whitespace to underscores and remove duplicates
+ .replace( /[ _\s]+/g, '_' )
+ // Trim underscores
+ .replace( rUnderscoreTrim, '' );
- // Check only undefined (may be false-y, such as '' (main namespace) ).
- if ( ns === undefined ) {
+ if ( title === '' ) {
return false;
+ }
+
+ // Process initial colon
+ if ( title.charAt( 0 ) === ':' ) {
+ // Initial colon means main namespace instead of specified default
+ namespace = NS_MAIN;
+ title = title
+ // Strip colon
+ .substr( 1 )
+ // Trim underscores
+ .replace( rUnderscoreTrim, '' );
+ }
+
+ // Process namespace prefix (if any)
+ m = title.match( rSplit );
+ if ( m ) {
+ id = getNsIdByName( m[1] );
+ if ( id !== false ) {
+ // Ordinary namespace
+ namespace = id;
+ title = m[2];
+
+ // For Talk:X pages, make sure X has no "namespace" prefix
+ if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) {
+ // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x)
+ if ( getNsIdByName( m[1] ) !== false ) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Process fragment
+ i = title.indexOf( '#' );
+ if ( i === -1 ) {
+ fragment = null;
} else {
- return Number( id );
+ fragment = title
+ // Get segment starting after the hash
+ .substr( i + 1 )
+ // Convert to text
+ // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo")
+ .replace( /_/g, ' ' );
+
+ title = title
+ // Strip hash
+ .substr( 0, i )
+ // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux")
+ .replace( rUnderscoreTrim, '' );
}
- },
- /**
- * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias).
- * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'.
- * @ignore
- * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored).
- * @return {Number|Boolean} Namespace id or boolean false if unrecognized.
- */
- getNsIdByName = function ( ns ) {
- // Don't cast non-strings to strings, because null or undefined
- // should not result in returning the id of a potential namespace
- // called "Null:" (e.g. on nullwiki.example.org)
- // Also, toLowerCase throws exception on null/undefined, because
- // it is a String.prototype method.
- if ( typeof ns !== 'string' ) {
+
+ // Reject illegal characters
+ if ( title.match( rInvalid ) ) {
return false;
}
- ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize
- var id = mw.config.get( 'wgNamespaceIds' )[ns];
- if ( id === undefined ) {
- mw.log( 'mw.Title: Unrecognized namespace: ' + ns );
+
+ // Disallow titles that browsers or servers might resolve as directory navigation
+ if (
+ title.indexOf( '.' ) !== -1 && (
+ title === '.' || title === '..' ||
+ title.indexOf( './' ) === 0 ||
+ title.indexOf( '../' ) === 0 ||
+ title.indexOf( '/./' ) !== -1 ||
+ title.indexOf( '/../' ) !== -1 ||
+ title.substr( -2 ) === '/.' ||
+ title.substr( -3 ) === '/..'
+ )
+ ) {
return false;
}
- return fixNsId( id );
+
+ // Disallow magic tilde sequence
+ if ( title.indexOf( '~~~' ) !== -1 ) {
+ return false;
+ }
+
+ // Disallow titles exceeding the 255 byte size limit (size of underlying database field)
+ // Except for special pages, e.g. [[Special:Block/Long name]]
+ // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should
+ // be less than 512 bytes.
+ if ( namespace !== NS_SPECIAL && $.byteLength( title ) > 255 ) {
+ return false;
+ }
+
+ // Can't make a link to a namespace alone.
+ if ( title === '' && namespace !== NS_MAIN ) {
+ return false;
+ }
+
+ // Any remaining initial :s are illegal.
+ if ( title.charAt( 0 ) === ':' ) {
+ return false;
+ }
+
+ // For backwards-compatibility with old mw.Title, we separate the extension from the
+ // rest of the title.
+ i = title.lastIndexOf( '.' );
+ if ( i === -1 || title.length <= i + 1 ) {
+ // Extensions are the non-empty segment after the last dot
+ ext = null;
+ } else {
+ ext = title.substr( i + 1 );
+ title = title.substr( 0, i );
+ }
+
+ return {
+ namespace: namespace,
+ title: title,
+ ext: ext,
+ fragment: fragment
+ };
},
/**
- * Helper to extract namespace, name and extension from a string.
+ * Convert db-key to readable text.
*
- * @ignore
- * @param {mw.Title} title
- * @param {string} raw
- * @return {mw.Title}
+ * @private
+ * @static
+ * @method text
+ * @param {string} s
+ * @return {string}
*/
- setAll = function ( title, s ) {
- // In normal browsers the match-array contains null/undefined if there's no match,
- // IE returns an empty string.
- var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ),
- nsMatch = getNsIdByName( matches[1] );
-
- // Namespace must be valid, and title must be a non-empty string.
- if ( nsMatch && typeof matches[2] === 'string' && matches[2] !== '' ) {
- title.ns = nsMatch;
- title.name = fixName( matches[2] );
- if ( typeof matches[3] === 'string' && matches[3] !== '' ) {
- title.ext = fixExt( matches[3] );
- }
+ text = function ( s ) {
+ if ( s !== null && s !== undefined ) {
+ return s.replace( /_/g, ' ' );
} else {
- // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace.
- title.ns = 0;
- setNameAndExtension( title, s );
+ return '';
}
- return title;
},
+ // Polyfill for ES5 Object.create
+ createObject = Object.create || ( function () {
+ return function ( o ) {
+ function Title() {}
+ if ( o !== Object( o ) ) {
+ throw new Error( 'Cannot inherit from a non-object' );
+ }
+ Title.prototype = o;
+ return new Title();
+ };
+ }() );
+
+
+ /* Static members */
+
/**
- * Helper to extract name and extension from a string.
+ * Constructor for Title objects with a null return instead of an exception for invalid titles.
*
- * @ignore
- * @param {mw.Title} title
- * @param {string} raw
- * @return {mw.Title}
+ * @static
+ * @method
+ * @param {string} title
+ * @param {number} [namespace=NS_MAIN] Default namespace
+ * @return {mw.Title|null} A valid Title object or null if the title is invalid
*/
- setNameAndExtension = function ( title, raw ) {
- // In normal browsers the match-array contains null/undefined if there's no match,
- // IE returns an empty string.
- var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ );
-
- // Title must be a non-empty string.
- if ( typeof matches[1] === 'string' && matches[1] !== '' ) {
- title.name = fixName( matches[1] );
- if ( typeof matches[2] === 'string' && matches[2] !== '' ) {
- title.ext = fixExt( matches[2] );
- }
- } else {
- throw new Error( 'mw.Title: Could not parse title "' + raw + '"' );
+ Title.newFromText = function ( title, namespace ) {
+ var t, parsed = parse( title, namespace );
+ if ( !parsed ) {
+ return null;
}
- return title;
- };
+ t = createObject( Title.prototype );
+ t.namespace = parsed.namespace;
+ t.title = parsed.title;
+ t.ext = parsed.ext;
+ t.fragment = parsed.fragment;
- /* Static space */
+ return t;
+ };
/**
* Whether this title exists on the wiki.
+ *
* @static
- * @param {Mixed} title prefixed db-key name (string) or instance of Title
- * @return {Mixed} Boolean true/false if the information is available. Otherwise null.
+ * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title
+ * @return {boolean|null} Boolean if the information is available, otherwise null
*/
Title.exists = function ( title ) {
- var type = $.type( title ), obj = Title.exist.pages, match;
+ var match,
+ type = $.type( title ),
+ obj = Title.exist.pages;
+
if ( type === 'string' ) {
match = obj[title];
} else if ( type === 'object' && title instanceof Title ) {
} else {
throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' );
}
+
if ( typeof match === 'boolean' ) {
return match;
}
+
return null;
};
- /**
- * @static
- * @property
- */
Title.exist = {
/**
+ * Boolean true value indicates page does exist.
+ *
* @static
* @property {Object} exist.pages Keyed by PrefixedDb title.
- * Boolean true value indicates page does exist.
*/
pages: {},
+
/**
* Example to declare existing titles:
* Title.exist.set(['User:John_Doe', ...]);
*
* @static
* @property exist.set
- * @param {string|Array} titles Title(s) in strict prefixedDb title form.
- * @param {boolean} [state] State of the given titles. Defaults to true.
+ * @param {string|Array} titles Title(s) in strict prefixedDb title form
+ * @param {boolean} [state=true] State of the given titles
* @return {boolean}
*/
set: function ( titles, state ) {
}
};
- /* Public methods */
+ /* Public members */
- fn = {
+ Title.prototype = {
constructor: Title,
/**
- * Get the namespace number.
+ * Get the namespace number
+ *
+ * Example: 6 for "File:Example_image.svg".
+ *
* @return {number}
*/
- getNamespaceId: function (){
- return this.ns;
+ getNamespaceId: function () {
+ return this.namespace;
},
/**
- * Get the namespace prefix (in the content-language).
- * In NS_MAIN this is '', otherwise namespace name plus ':'
+ * Get the namespace prefix (in the content language)
+ *
+ * Example: "File:" for "File:Example_image.svg".
+ * In #NS_MAIN this is '', otherwise namespace name plus ':'
+ *
* @return {string}
*/
- getNamespacePrefix: function (){
- return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':');
+ getNamespacePrefix: function () {
+ return this.namespace === NS_MAIN ?
+ '' :
+ ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' );
},
/**
- * The name, like "Foo_bar"
+ * Get the page name without extension or namespace prefix
+ *
+ * Example: "Example_image" for "File:Example_image.svg".
+ *
+ * For the page title (full page name without namespace prefix), see #getMain.
+ *
* @return {string}
*/
getName: function () {
- if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) {
- return this.name;
+ if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) {
+ return this.title;
} else {
- return $.ucFirst( this.name );
+ return $.ucFirst( this.title );
}
},
/**
- * The name, like "Foo bar"
+ * Get the page name (transformed by #text)
+ *
+ * Example: "Example image" for "File:Example_image.svg".
+ *
+ * For the page title (full page name without namespace prefix), see #getMainText.
+ *
* @return {string}
*/
getNameText: function () {
},
/**
- * Get full name in prefixed DB form, like File:Foo_bar.jpg,
- * most useful for API calls, anything that must identify the "title".
- * @return {string}
+ * Get the extension of the page name (if any)
+ *
+ * @return {string|null} Name extension or null if there is none
*/
- getPrefixedDb: function () {
- return this.getNamespacePrefix() + this.getMain();
+ getExtension: function () {
+ return this.ext;
},
/**
- * Get full name in text form, like "File:Foo bar.jpg".
+ * Shortcut for appendable string to form the main page name.
+ *
+ * Returns a string like ".json", or "" if no extension.
+ *
* @return {string}
*/
- getPrefixedText: function () {
- return text( this.getPrefixedDb() );
+ getDotExtension: function () {
+ return this.ext === null ? '' : '.' + this.ext;
},
/**
- * The main title (without namespace), like "Foo_bar.jpg"
+ * Get the main page name (transformed by #text)
+ *
+ * Example: "Example_image.svg" for "File:Example_image.svg".
+ *
* @return {string}
*/
getMain: function () {
},
/**
- * The "text" form, like "Foo bar.jpg"
+ * Get the main page name (transformed by #text)
+ *
+ * Example: "Example image.svg" for "File:Example_image.svg".
+ *
* @return {string}
*/
getMainText: function () {
},
/**
- * Get the extension (returns null if there was none)
- * @return {string|null}
+ * Get the full page name
+ *
+ * Eaxample: "File:Example_image.svg".
+ * Most useful for API calls, anything that must identify the "title".
+ *
+ * @return {string}
*/
- getExtension: function () {
- return this.ext;
+ getPrefixedDb: function () {
+ return this.getNamespacePrefix() + this.getMain();
},
/**
- * Convenience method: return string like ".jpg", or "" if no extension
+ * Get the full page name (transformed by #text)
+ *
+ * Example: "File:Example image.svg" for "File:Example_image.svg".
+ *
* @return {string}
*/
- getDotExtension: function () {
- return this.ext === null ? '' : '.' + this.ext;
+ getPrefixedText: function () {
+ return text( this.getPrefixedDb() );
},
/**
- * Return the URL to this title
+ * Get the fragment (if any).
+ *
+ * Note that this method (by design) does not include the hash character and
+ * the value is not url encoded.
+ *
+ * @return {string|null}
+ */
+ getFragment: function () {
+ return this.fragment;
+ },
+
+ /**
+ * Get the URL to this title
+ *
* @see mw.util#wikiGetlink
* @return {string}
*/
/**
* Whether this title exists on the wiki.
+ *
* @see #static-method-exists
- * @return {boolean|null} If the information is available. Otherwise null.
+ * @return {boolean|null} Boolean if the information is available, otherwise null
*/
exists: function () {
return Title.exists( this );
}
};
- // Alias
- fn.toString = fn.getPrefixedDb;
- fn.toText = fn.getPrefixedText;
+ /**
+ * @alias #getPrefixedDb
+ * @method
+ */
+ Title.prototype.toString = Title.prototype.getPrefixedDb;
+
- // Assign
- Title.prototype = fn;
+ /**
+ * @alias #getPrefixedText
+ * @method
+ */
+ Title.prototype.toText = Title.prototype.getPrefixedText;
// Expose
mw.Title = Title;
}
/* Default style for semantic tags */
-abbr,
-acronym,
-.explain {
+abbr[title],
+.explain[title] {
border-bottom: 1px dotted;
cursor: help;
}
}
}
+ /**
+ * See also mediawiki.Title.test.js
+ */
+ function testSecureAndSplit() {
+ // Valid
+ foreach ( array(
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ // Length is 256 total, but only title part matters
+ 'Category:' . str_repeat( 'x', 248 ),
+ str_repeat( 'x', 252 )
+ ) as $text ) {
+ $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" );
+ }
+
+ // Invalid
+ foreach ( array(
+ '',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: Commented out because they are not marked invalid by the PHP test as
+ // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
+ //'A é B',
+ //'A é B',
+ //'A é B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ str_repeat( 'x', 256 ),
+ // Namespace prefix without actual title
+ // ':', // bug 54044
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ) as $text ) {
+ $this->assertNull( Title::newFromText( $text ), "Invalid: $text" );
+ }
+ }
+
public static function provideConvertByteClassToUnicodeClass() {
return array(
array(
description: 'Pass the limit and a callback as input filter',
$input: $( '<input type="text"/>' )
.byteLimit( 6, function ( val ) {
- // Invalid title
- if ( val === '' ) {
- return '';
- }
-
+ var title = mw.Title.newFromText( String( val ) );
// Return without namespace prefix
- return new mw.Title( String( val ) ).getMain();
+ return title ? title.getMain() : '';
} ),
sample: 'User:Sample',
expected: 'User:Sample'
$input: $( '<input type="text"/>' )
.attr( 'maxlength', '6' )
.byteLimit( function ( val ) {
- // Invalid title
- if ( val === '' ) {
- return '';
- }
-
+ var title = mw.Title.newFromText( String( val ) );
// Return without namespace prefix
- return new mw.Title( String( val ) ).getMain();
+ return title ? title.getMain() : '';
} ),
sample: 'User:Sample',
expected: 'User:Sample'
description: 'Pass the limit and a callback as input filter',
$input: $( '<input type="text"/>' )
.byteLimit( 6, function ( val ) {
- // Invalid title
- if ( val === '' ) {
- return '';
- }
-
+ var title = mw.Title.newFromText( String( val ) );
// Return without namespace prefix
- return new mw.Title( String( val ) ).getMain();
+ return title ? title.getMain() : '';
} ),
sample: 'User:Example',
// The callback alters the value to be used to calculeate
-( function ( mw ) {
+( function ( mw, $ ) {
// mw.Title relies on these three config vars
// Restore them after each test run
var config = {
antarctic_waterfowl: 100
},
wgCaseSensitiveNamespaces: []
+ },
+ repeat = function ( input, multiplier ) {
+ return new Array( multiplier + 1 ).join( input );
+ },
+ cases = {
+ // See also TitleTest.php#testSecureAndSplit
+ valid: [
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ // Length is 256 total, but only title part matters
+ 'Category:' + repeat( 'x', 248 ),
+ repeat( 'x', 252 )
+ ],
+ invalid: [
+ '',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: The ones with # are commented out as those are interpreted as fragment and
+ // as such end up being valid.
+ 'A é B',
+ //'A é B',
+ //'A é B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ repeat( 'x', 256 ),
+ // Extension separation is a js invention, for length
+ // purposes it is part of the title
+ repeat( 'x', 252 ) + '.json',
+ // Namespace prefix without actual title
+ // ':', // bug 54044
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ]
};
QUnit.module( 'mediawiki.Title', QUnit.newMwEnvironment( { config: config } ) );
- QUnit.test( 'Transformation', 8, function ( assert ) {
+ QUnit.test( 'constructor', cases.invalid.length, function ( assert ) {
+ var i, title;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ title = new mw.Title( cases.valid[i] );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ /*jshint loopfunc:true */
+ title = cases.invalid[i];
+ assert.throws( function () {
+ return new mw.Title( title );
+ }, cases.invalid[i] );
+ }
+ } );
+
+ QUnit.test( 'newFromText', cases.valid.length + cases.invalid.length, function ( assert ) {
+ var i;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.valid[i] ) ),
+ 'object',
+ cases.valid[i]
+ );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.invalid[i] ) ),
+ 'null',
+ cases.invalid[i]
+ );
+ }
+ } );
+
+ QUnit.test( 'Basic parsing', 12, function ( assert ) {
+ var title;
+ title = new mw.Title( 'File:Foo_bar.JPG' );
+
+ assert.equal( title.getNamespaceId(), 6 );
+ assert.equal( title.getNamespacePrefix(), 'File:' );
+ assert.equal( title.getName(), 'Foo_bar' );
+ assert.equal( title.getNameText(), 'Foo bar' );
+ assert.equal( title.getExtension(), 'JPG' );
+ assert.equal( title.getDotExtension(), '.JPG' );
+ assert.equal( title.getMain(), 'Foo_bar.JPG' );
+ assert.equal( title.getMainText(), 'Foo bar.JPG' );
+ assert.equal( title.getPrefixedDb(), 'File:Foo_bar.JPG' );
+ assert.equal( title.getPrefixedText(), 'File:Foo bar.JPG' );
+
+ title = new mw.Title( 'Foo#bar' );
+ assert.equal( title.getPrefixedText(), 'Foo' );
+ assert.equal( title.getFragment(), 'bar' );
+ } );
+
+ QUnit.test( 'Transformation', 11, function ( assert ) {
var title;
title = new mw.Title( 'File:quux pif.jpg' );
- assert.equal( title.getName(), 'Quux_pif' );
+ assert.equal( title.getNameText(), 'Quux pif', 'First character of title' );
title = new mw.Title( 'File:Glarg_foo_glang.jpg' );
- assert.equal( title.getNameText(), 'Glarg foo glang' );
+ assert.equal( title.getNameText(), 'Glarg foo glang', 'Underscores' );
title = new mw.Title( 'User:ABC.DEF' );
- assert.equal( title.toText(), 'User:ABC.DEF' );
- assert.equal( title.getNamespaceId(), 2 );
- assert.equal( title.getNamespacePrefix(), 'User:' );
+ assert.equal( title.toText(), 'User:ABC.DEF', 'Round trip text' );
+ assert.equal( title.getNamespaceId(), 2, 'Parse canonical namespace prefix' );
+
+ title = new mw.Title( 'Image:quux pix.jpg' );
+ assert.equal( title.getNamespacePrefix(), 'File:', 'Transform alias to canonical namespace' );
title = new mw.Title( 'uSEr:hAshAr' );
assert.equal( title.toText(), 'User:HAshAr' );
- assert.equal( title.getNamespaceId(), 2 );
+ assert.equal( title.getNamespaceId(), 2, 'Case-insensitive namespace prefix' );
- title = new mw.Title( ' MediaWiki: Foo bar .js ' );
- // Don't ask why, it's the way the backend works. One space is kept of each set
- assert.equal( title.getName(), 'Foo_bar_.js', 'Merge multiple spaces to a single space.' );
- } );
+ // Don't ask why, it's the way the backend works. One space is kept of each set.
+ title = new mw.Title( 'Foo __ \t __ bar' );
+ assert.equal( title.getMain(), 'Foo_bar', 'Merge multiple types of whitespace/underscores into a single underscore' );
- QUnit.test( 'Main text for filename', 8, function ( assert ) {
- var title = new mw.Title( 'File:foo_bar.JPG' );
+ // Regression test: Previously it would only detect an extension if there is no space after it
+ title = new mw.Title( 'Example.js ' );
+ assert.equal( title.getExtension(), 'js', 'Space after an extension is stripped' );
- assert.equal( title.getNamespaceId(), 6 );
- assert.equal( title.getNamespacePrefix(), 'File:' );
- assert.equal( title.getName(), 'Foo_bar' );
- assert.equal( title.getNameText(), 'Foo bar' );
- assert.equal( title.getMain(), 'Foo_bar.JPG' );
- assert.equal( title.getMainText(), 'Foo bar.JPG' );
- assert.equal( title.getExtension(), 'JPG' );
- assert.equal( title.getDotExtension(), '.JPG' );
+ title = new mw.Title( 'Example#foo' );
+ assert.equal( title.getFragment(), 'foo', 'Fragment' );
+
+ title = new mw.Title( 'Example#_foo_bar baz_' );
+ assert.equal( title.getFragment(), ' foo bar baz', 'Fragment' );
} );
- QUnit.test( 'Namespace detection and conversion', 6, function ( assert ) {
+ QUnit.test( 'Namespace detection and conversion', 10, function ( assert ) {
var title;
+ title = new mw.Title( 'File:User:Example' );
+ assert.equal( title.getNamespaceId(), 6, 'Titles can contain namespace prefixes, which are otherwise ignored' );
+
+ title = new mw.Title( 'Example', 6 );
+ assert.equal( title.getNamespaceId(), 6, 'Default namespace passed is used' );
+
+ title = new mw.Title( 'User:Example', 6 );
+ assert.equal( title.getNamespaceId(), 2, 'Included namespace prefix overrides the given default' );
+
+ title = new mw.Title( ':Example', 6 );
+ assert.equal( title.getNamespaceId(), 0, 'Colon forces main namespace' );
+
title = new mw.Title( 'something.PDF', 6 );
assert.equal( title.toString(), 'File:Something.PDF' );
assert.equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' );
} );
-}( mediaWiki ) );
+}( mediaWiki, jQuery ) );