Merge "@since for User::newSystemUser"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 14 May 2016 08:24:00 +0000 (08:24 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 14 May 2016 08:24:00 +0000 (08:24 +0000)
64 files changed:
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Html.php
includes/MediaWikiServices.php
includes/ServiceWiring.php
includes/api/ApiMain.php
includes/api/ApiManageTags.php
includes/api/ApiStashEdit.php
includes/cache/LinkBatch.php
includes/cache/LinkCache.php
includes/changetags/ChangeTags.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/htmlform/OOUIHTMLForm.php
includes/libs/Xhprof.php
includes/libs/XhprofData.php [new file with mode: 0644]
includes/parser/CoreParserFunctions.php
includes/parser/LinkHolderArray.php
includes/parser/StripState.php
includes/profiler/ProfilerXhprof.php
includes/specials/SpecialTags.php
includes/title/MediaWikiTitleCodec.php
includes/user/User.php
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/cs.json
languages/i18n/de.json
languages/i18n/en.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/he.json
languages/i18n/ht.json
languages/i18n/id.json
languages/i18n/inh.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/ku-latn.json
languages/i18n/pl.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sl.json
languages/i18n/sv.json
languages/i18n/ur.json
languages/i18n/zh-hans.json
resources/src/mediawiki/mediawiki.htmlform.ooui.css
resources/src/mediawiki/mediawiki.jqueryMsg.js
tests/parser/parserTest.inc
tests/parser/parserTests.txt
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/LinkerTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/XmlSelectTest.php
tests/phpunit/includes/XmlTest.php
tests/phpunit/includes/content/ContentHandlerTest.php
tests/phpunit/includes/content/JsonContentTest.php
tests/phpunit/includes/libs/XhprofDataTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/XhprofTest.php
tests/phpunit/includes/objectcache/RedisBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/NewParserTest.php
tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js

index 1e656e4..eb47300 100644 (file)
@@ -1465,6 +1465,7 @@ $wgAutoloadLocalClasses = [
        'XMPReader' => __DIR__ . '/includes/media/XMP.php',
        'XMPValidate' => __DIR__ . '/includes/media/XMPValidate.php',
        'Xhprof' => __DIR__ . '/includes/libs/Xhprof.php',
+       'XhprofData' => __DIR__ . '/includes/libs/XhprofData.php',
        'Xml' => __DIR__ . '/includes/Xml.php',
        'XmlDumpWriter' => __DIR__ . '/includes/export/XmlDumpWriter.php',
        'XmlJsCode' => __DIR__ . '/includes/Xml.php',
index 383f0ad..bb10372 100644 (file)
@@ -3130,24 +3130,6 @@ $wgHTMLFormAllowTableFormat = true;
  */
 $wgUseMediaWikiUIEverywhere = false;
 
-/**
- * Should we try to make our HTML output well-formed XML?  If set to false,
- * output will be a few bytes shorter, and the HTML will arguably be more
- * readable.  If set to true, life will be much easier for the authors of
- * screen-scraping bots, and the HTML will arguably be more readable.
- *
- * Setting this to false may omit quotation marks on some attributes, omit
- * slashes from some self-closing tags, omit some ending tags, etc., where
- * permitted by HTML5.  Setting it to true will not guarantee that all pages
- * will be well-formed, although non-well-formed pages should be rare and it's
- * a bug if you find one.  Conversely, setting it to false doesn't mean that
- * all XML-y constructs will be omitted, just that they might be.
- *
- * Because of compatibility with screen-scraping bots, and because it's
- * controversial, this is currently left to true by default.
- */
-$wgWellFormedXml = true;
-
 /**
  * Permit other namespaces in addition to the w3.org default.
  *
@@ -4903,6 +4885,7 @@ $wgGroupPermissions['sysop']['suppressredirect'] = true;
 # $wgGroupPermissions['sysop']['upload_by_url'] = true;
 $wgGroupPermissions['sysop']['mergehistory'] = true;
 $wgGroupPermissions['sysop']['managechangetags'] = true;
+$wgGroupPermissions['sysop']['deletechangetags'] = true;
 
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
index 02093ff..870e2e0 100644 (file)
@@ -3860,7 +3860,7 @@ HTML
                        ],
                        $showSignature ? [
                                'id'     => 'mw-editbutton-signature',
-                               'open'   => '--~~~~',
+                               'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
                                'close'  => '',
                                'sample' => '',
                                'tip'    => wfMessage( 'sig_tip' )->text(),
index 890beb0..e5128d1 100644 (file)
@@ -38,8 +38,6 @@
  *
  * $wgMimeType: If this is set to an xml MIME type then output should be
  *     valid XHTML5.
- * $wgWellFormedXml: If this is set to true, then all output should be
- *     well-formed XML (quotes on attributes, self-closing tags, etc.).
  *
  * This class is meant to be confined to utility functions that are called from
  * trusted code paths.  It does not do enforcement of policy like not allowing
@@ -199,8 +197,7 @@ class Html {
         * This is quite similar to Xml::tags(), but it implements some useful
         * HTML-specific logic.  For instance, there is no $allowShortTag
         * parameter: the closing tag is magically omitted if $element has an empty
-        * content model.  If $wgWellFormedXml is false, then a few bytes will be
-        * shaved off the HTML output as well.
+        * content model.
         *
         * @param string $element The element's name, e.g., 'a'
         * @param array $attribs Associative array of attributes, e.g., array(
@@ -211,14 +208,10 @@ class Html {
         * @return string Raw HTML
         */
        public static function rawElement( $element, $attribs = [], $contents = '' ) {
-               global $wgWellFormedXml;
                $start = self::openElement( $element, $attribs );
                if ( in_array( $element, self::$voidElements ) ) {
-                       if ( $wgWellFormedXml ) {
-                               // Silly XML.
-                               return substr( $start, 0, -1 ) . '/>';
-                       }
-                       return $start;
+                       // Silly XML.
+                       return substr( $start, 0, -1 ) . '/>';
                } else {
                        return "$start$contents" . self::closeElement( $element );
                }
@@ -443,8 +436,6 @@ class Html {
         * 'http://www.mediawiki.org/' ) becomes something like
         * ' href="http://www.mediawiki.org"'.  Again, this is like
         * Xml::expandAttributes(), but it implements some HTML-specific logic.
-        * For instance, it will omit quotation marks if $wgWellFormedXml is false,
-        * and will treat boolean attributes specially.
         *
         * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
         * values are allowed as well, which will automagically be normalized
@@ -479,8 +470,6 @@ class Html {
         *   (starting with a space if at least one attribute is output)
         */
        public static function expandAttributes( array $attribs ) {
-               global $wgWellFormedXml;
-
                $ret = '';
                foreach ( $attribs as $key => $value ) {
                        // Support intuitive array( 'checked' => true/false ) form
@@ -564,31 +553,10 @@ class Html {
                                throw new MWException( "HTML attribute $key can not contain a list of values" );
                        }
 
-                       // See the "Attributes" section in the HTML syntax part of HTML5,
-                       // 9.1.2.3 as of 2009-08-10.  Most attributes can have quotation
-                       // marks omitted, but not all.  (Although a literal " is not
-                       // permitted, we don't check for that, since it will be escaped
-                       // anyway.)
-
-                       // See also research done on further characters that need to be
-                       // escaped: http://code.google.com/p/html5lib/issues/detail?id=93
-                       $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
-                               . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
-                               . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
-                       if ( $wgWellFormedXml || $value === '' || preg_match( "![$badChars]!u", $value ) ) {
-                               $quote = '"';
-                       } else {
-                               $quote = '';
-                       }
+                       $quote = '"';
 
                        if ( in_array( $key, self::$boolAttribs ) ) {
-                               // In HTML5, we can leave the value empty. If we don't need
-                               // well-formed XML, we can omit the = entirely.
-                               if ( !$wgWellFormedXml ) {
-                                       $ret .= " $key";
-                               } else {
-                                       $ret .= " $key=\"\"";
-                               }
+                               $ret .= " $key=\"\"";
                        } else {
                                // Apparently we need to entity-encode \n, \r, \t, although the
                                // spec doesn't mention that.  Since we're doing strtr() anyway,
@@ -599,22 +567,18 @@ class Html {
                                // don't because we're stubborn and like our marginal savings on
                                // byte size from not having to encode unnecessary quotes.
                                // The only difference between this transform and the one by
-                               // Sanitizer::encodeAttribute() is '<' is only encoded here if
-                               // $wgWellFormedXml is set, and ' is not encoded.
+                               // Sanitizer::encodeAttribute() is ' is not encoded.
                                $map = [
                                        '&' => '&amp;',
                                        '"' => '&quot;',
                                        '>' => '&gt;',
+                                       // '<' allegedly allowed per spec
+                                       // but breaks some tools if not escaped.
+                                       "<" => '&lt;',
                                        "\n" => '&#10;',
                                        "\r" => '&#13;',
                                        "\t" => '&#9;'
                                ];
-                               if ( $wgWellFormedXml ) {
-                                       // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
-                                       // But reportedly it breaks some XML tools?
-                                       // @todo FIXME: Is this really true?
-                                       $map['<'] = '&lt;';
-                               }
                                $ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
                        }
                }
@@ -631,11 +595,9 @@ class Html {
         * @return string Raw HTML
         */
        public static function inlineScript( $contents ) {
-               global $wgWellFormedXml;
-
                $attrs = [];
 
-               if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+               if ( preg_match( '/[<&]/', $contents ) ) {
                        $contents = "/*<![CDATA[*/$contents/*]]>*/";
                }
 
@@ -665,9 +627,7 @@ class Html {
         * @return string Raw HTML
         */
        public static function inlineStyle( $contents, $media = 'all' ) {
-               global $wgWellFormedXml;
-
-               if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+               if ( preg_match( '/[<&]/', $contents ) ) {
                        $contents = "/*<![CDATA[*/$contents/*]]>*/";
                }
 
index 5bb5597..891f426 100644 (file)
@@ -8,6 +8,7 @@ use GenderCache;
 use GlobalVarConfig;
 use Hooks;
 use LBFactory;
+use LinkCache;
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
 use LoadBalancer;
 use MediaWiki\Services\ServiceContainer;
@@ -460,6 +461,15 @@ class MediaWikiServices extends ServiceContainer {
        public function getGenderCache() {
                return $this->getService( 'GenderCache' );
        }
+
+       /**
+        * @since 1.28
+        * @return LinkCache
+        */
+       public function getLinkCache() {
+               return $this->getService( 'LinkCache' );
+       }
+
        /**
         * @since 1.28
         * @return TitleFormatter
index aa99a71..293e6eb 100644 (file)
@@ -139,6 +139,12 @@ return [
                return $store;
        },
 
+       'LinkCache' => function( MediaWikiServices $services ) {
+               return new LinkCache(
+                       $services->getTitleFormatter()
+               );
+       },
+
        'GenderCache' => function( MediaWikiServices $services ) {
                return new GenderCache();
        },
index 685a9ef..15cea44 100644 (file)
@@ -1135,7 +1135,7 @@ class ApiMain extends ApiBase {
                                                                        TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
                                                                );
                                                        }
-                                                       Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes ] );
+                                                       Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
                                                        $lastMod = max( $modifiedTimes );
                                                        $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
                                                }
index 60fb4dc..617db22 100644 (file)
@@ -29,8 +29,14 @@ class ApiManageTags extends ApiBase {
                $params = $this->extractRequestParams();
 
                // make sure the user is allowed
-               if ( !$this->getUser()->isAllowed( 'managechangetags' ) ) {
-                       $this->dieUsage( "You don't have permission to manage change tags", 'permissiondenied' );
+               if ( $params['operation'] !== 'delete'
+                       && !$this->getUser()->isAllowed( 'managechangetags' )
+               ) {
+                       $this->dieUsage( "You don't have permission to manage change tags",
+                               'permissiondenied' );
+               } elseif ( !$this->getUser()->isAllowed( 'deletechangetags' ) ) {
+                       $this->dieUsage( "You don't have permission to delete change tags",
+                               'permissiondenied' );
                }
 
                $result = $this->getResult();
index 5efefbd..3539eed 100644 (file)
@@ -147,13 +147,15 @@ class ApiStashEdit extends ApiBase {
                        Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] );
 
                        list( $stashInfo, $ttl ) = self::buildStashValue(
-                               $editInfo->pstContent, $editInfo->output, $editInfo->timestamp
+                               $editInfo->pstContent,
+                               $editInfo->output,
+                               $editInfo->timestamp,
+                               $user
                        );
 
                        if ( $stashInfo ) {
                                $ok = $cache->set( $key, $stashInfo, $ttl );
                                if ( $ok ) {
-
                                        $logger->debug( "Cached parser output for key '$key'." );
                                        return self::ERROR_NONE;
                                } else {
@@ -223,7 +225,7 @@ class ApiStashEdit extends ApiBase {
                $pOut->setCacheTime( wfTimestampNow() );
 
                // Build a value to cache with a proper TTL
-               list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp );
+               list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp, $user );
                if ( !$stashInfo ) {
                        $logger->info( "Uncacheable parser output for key '$key' (rev/TTL)." );
                        return false;
@@ -291,6 +293,10 @@ class ApiStashEdit extends ApiBase {
                        $stats->increment( 'editstash.cache_hits.presumed_fresh' );
                        $logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." );
                        return $editInfo; // assume nothing changed
+               } elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) {
+                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                       $logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." );
+                       return $editInfo; // use made no local upload/template edits in the meantime
                }
 
                $dbr = wfGetDB( DB_SLAVE );
@@ -385,12 +391,14 @@ class ApiStashEdit extends ApiBase {
         * @param Content $pstContent
         * @param ParserOutput $parserOutput
         * @param string $timestamp TS_MW
+        * @param User $user
         * @return array (stash info array, TTL in seconds) or (null, 0)
         */
        protected static function buildStashValue(
-               Content $pstContent, ParserOutput $parserOutput, $timestamp
+               Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user
        ) {
-               // If an item is renewed, mind the cache TTL determined by config and parser functions
+               // If an item is renewed, mind the cache TTL determined by config and parser functions.
+               // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
                $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() );
                $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 );
 
@@ -399,7 +407,8 @@ class ApiStashEdit extends ApiBase {
                        $stashInfo = (object)[
                                'pstContent' => $pstContent,
                                'output'     => $parserOutput,
-                               'timestamp'  => $timestamp
+                               'timestamp'  => $timestamp,
+                               'edits'      => $user->getEditCount()
                        ];
                        return [ $stashInfo, $ttl ];
                }
index c5bd290..a7dd570 100644 (file)
@@ -179,8 +179,6 @@ class LinkBatch {
         * @return bool|ResultWrapper
         */
        public function doQuery() {
-               global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
-
                if ( $this->isEmpty() ) {
                        return false;
                }
@@ -188,15 +186,10 @@ class LinkBatch {
                // This is similar to LinkHolderArray::replaceInternal
                $dbr = wfGetDB( DB_SLAVE );
                $table = 'page';
-               $fields = [ 'page_id', 'page_namespace', 'page_title', 'page_len',
-                       'page_is_redirect', 'page_latest' ];
-
-               if ( $wgContentHandlerUseDB ) {
-                       $fields[] = 'page_content_model';
-               }
-               if ( $wgPageLanguageUseDB ) {
-                       $fields[] = 'page_lang';
-               }
+               $fields = array_merge(
+                       LinkCache::getSelectFields(),
+                       [ 'page_namespace', 'page_title' ]
+               );
 
                $conds = $this->constructSet( 'page', $dbr );
 
index b8f2329..de44f9b 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  * @ingroup Cache
  */
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Cache for article titles (prefixed DB keys) and ids linked from one source
@@ -38,56 +40,30 @@ class LinkCache {
        private $mForUpdate = false;
 
        /**
-        * How many Titles to store. There are two caches, so the amount actually
-        * stored in memory can be up to twice this.
+        * @var TitleFormatter
         */
-       const MAX_SIZE = 10000;
+       private $titleFormatter;
 
        /**
-        * @var LinkCache
+        * How many Titles to store. There are two caches, so the amount actually
+        * stored in memory can be up to twice this.
         */
-       protected static $instance;
+       const MAX_SIZE = 10000;
 
-       public function __construct() {
+       public function __construct( TitleFormatter $titleFormatter ) {
                $this->mGoodLinks = new HashBagOStuff( [ 'maxKeys' => self::MAX_SIZE ] );
                $this->mBadLinks = new HashBagOStuff( [ 'maxKeys' => self::MAX_SIZE ] );
+               $this->titleFormatter = $titleFormatter;
        }
 
        /**
         * Get an instance of this class.
         *
         * @return LinkCache
+        * @deprecated since 1.28, use MediaWikiServices instead
         */
        public static function singleton() {
-               if ( !self::$instance ) {
-                       self::$instance = new LinkCache;
-               }
-
-               return self::$instance;
-       }
-
-       /**
-        * Destroy the singleton instance
-        *
-        * A new one will be created next time singleton() is called.
-        *
-        * @since 1.22
-        */
-       public static function destroySingleton() {
-               self::$instance = null;
-       }
-
-       /**
-        * Set the singleton instance to a given object.
-        *
-        * Since we do not have an interface for LinkCache, you have to be sure the
-        * given object implements all the LinkCache public methods.
-        *
-        * @param LinkCache $instance
-        * @since 1.22
-        */
-       public static function setSingleton( LinkCache $instance ) {
-               self::$instance = $instance;
+               return MediaWikiServices::getInstance()->getLinkCache();
        }
 
        /**
@@ -119,12 +95,12 @@ class LinkCache {
        /**
         * Get a field of a title object from cache.
         * If this link is not a cached good title, it will return NULL.
-        * @param Title $title
+        * @param LinkTarget $target
         * @param string $field ('length','redirect','revision','model')
         * @return string|int|null
         */
-       public function getGoodLinkFieldObj( Title $title, $field ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function getGoodLinkFieldObj( LinkTarget $target, $field ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $info = $this->mGoodLinks->get( $dbkey );
                if ( !$info ) {
                        return null;
@@ -145,17 +121,17 @@ class LinkCache {
         * Add a link for the title to the link cache
         *
         * @param int $id Page's ID
-        * @param Title $title
+        * @param LinkTarget $target
         * @param int $len Text's length
         * @param int $redir Whether the page is a redirect
         * @param int $revision Latest revision's ID
         * @param string|null $model Latest revision's content model ID
         * @param string|null $lang Language code of the page, if not the content language
         */
-       public function addGoodLinkObj( $id, Title $title, $len = -1, $redir = null,
+       public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null,
                $revision = 0, $model = null, $lang = null
        ) {
-               $dbkey = $title->getPrefixedDBkey();
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mGoodLinks->set( $dbkey, [
                        'id' => (int)$id,
                        'length' => (int)$len,
@@ -169,12 +145,12 @@ class LinkCache {
        /**
         * Same as above with better interface.
         * @since 1.19
-        * @param Title $title
+        * @param LinkTarget $target
         * @param stdClass $row Object which has the fields page_id, page_is_redirect,
         *  page_latest and page_content_model
         */
-       public function addGoodLinkObjFromRow( Title $title, $row ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function addGoodLinkObjFromRow( LinkTarget $target, $row ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mGoodLinks->set( $dbkey, [
                        'id' => intval( $row->page_id ),
                        'length' => intval( $row->page_len ),
@@ -186,10 +162,10 @@ class LinkCache {
        }
 
        /**
-        * @param Title $title
+        * @param LinkTarget $target
         */
-       public function addBadLinkObj( Title $title ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function addBadLinkObj( LinkTarget $target ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                if ( !$this->isBadLink( $dbkey ) ) {
                        $this->mBadLinks->set( $dbkey, 1 );
                }
@@ -203,10 +179,10 @@ class LinkCache {
        }
 
        /**
-        * @param Title $title
+        * @param LinkTarget $target
         */
-       public function clearLink( Title $title ) {
-               $dbkey = $title->getPrefixedDBkey();
+       public function clearLink( LinkTarget $target ) {
+               $dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
                $this->mBadLinks->delete( $dbkey );
                $this->mGoodLinks->delete( $dbkey );
        }
@@ -214,6 +190,7 @@ class LinkCache {
        /**
         * Add a title to the link cache, return the page_id or zero if non-existent
         *
+        * @deprecated since 1.27, unused
         * @param string $title Prefixed DB key
         * @return int Page ID or zero
         */
@@ -226,15 +203,33 @@ class LinkCache {
        }
 
        /**
-        * Add a title to the link cache, return the page_id or zero if non-existent
+        * Fields that LinkCache needs to select
         *
-        * @param Title $nt Title object to add
-        * @return int Page ID or zero
+        * @since 1.28
+        * @return array
         */
-       public function addLinkObj( Title $nt ) {
+       public static function getSelectFields() {
                global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
 
-               $key = $nt->getPrefixedDBkey();
+               $fields = [ 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ];
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+               if ( $wgPageLanguageUseDB ) {
+                       $fields[] = 'page_lang';
+               }
+
+               return $fields;
+       }
+
+       /**
+        * Add a title to the link cache, return the page_id or zero if non-existent
+        *
+        * @param LinkTarget $nt LinkTarget object to add
+        * @return int Page ID or zero
+        */
+       public function addLinkObj( LinkTarget $nt ) {
+               $key = $this->titleFormatter->getPrefixedDBkey( $nt );
                if ( $this->isBadLink( $key ) || $nt->isExternal() ) {
                        return 0;
                }
@@ -250,15 +245,7 @@ class LinkCache {
                // Some fields heavily used for linking...
                $db = $this->mForUpdate ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
 
-               $fields = [ 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ];
-               if ( $wgContentHandlerUseDB ) {
-                       $fields[] = 'page_content_model';
-               }
-               if ( $wgPageLanguageUseDB ) {
-                       $fields[] = 'page_lang';
-               }
-
-               $row = $db->selectRow( 'page', $fields,
+               $row = $db->selectRow( 'page', self::getSelectFields(),
                        [ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ],
                        __METHOD__
                );
index 2d4d20f..a2945af 100644 (file)
@@ -1055,8 +1055,8 @@ class ChangeTags {
                $tagUsage = self::tagUsageStatistics();
 
                if ( !is_null( $user ) ) {
-                       if ( !$user->isAllowed( 'managechangetags' ) ) {
-                               return Status::newFatal( 'tags-manage-no-permission' );
+                       if ( !$user->isAllowed( 'deletechangetags' ) ) {
+                               return Status::newFatal( 'tags-delete-no-permission' );
                        } elseif ( $user->isBlocked() ) {
                                return Status::newFatal( 'tags-manage-blocked' );
                        }
index 1770639..65a8c0e 100644 (file)
@@ -49,6 +49,9 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
        public function doUpdate() {
                # Page may already be deleted, so don't just getId()
                $id = $this->pageId;
+               // Make sure all links update threads see the changes of each other.
+               // This handles the case when updates have to batched into several COMMITs.
+               $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id );
 
                # Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
@@ -101,6 +104,11 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                                $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ );
                        }
                }
+
+               $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
+                       // Release the lock *after* the final COMMIT for correctness
+                       ScopedCallback::consume( $scopedLock );
+               } );
        }
 
        public function getAsJobSpecification() {
index c0205be..ac08374 100644 (file)
  */
 
 /**
- * See docs/deferred.txt
+ * Class the manages updates of *_link tables as well as similar extension-managed tables
+ *
+ * @note: LinksUpdate is managed by DeferredUpdates::execute(). Do not run this in a transaction.
  *
- * @todo document (e.g. one-sentence top-level class description).
+ * See docs/deferred.txt
  */
 class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
        // @todo make members protected, but make sure extensions don't break
@@ -82,6 +84,8 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         */
        private $user;
 
+       const BATCH_SIZE = 500; // try to keep typical updates in a single transaction
+
        /**
         * Constructor
         *
@@ -91,7 +95,8 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @throws MWException
         */
        function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
-               parent::__construct( false ); // no implicit transaction
+               // Implicit transactions are disabled as they interfere with batching
+               parent::__construct( false );
 
                $this->mTitle = $title;
                $this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
@@ -141,16 +146,46 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
        /**
         * Update link tables with outgoing links from an updated article
+        *
+        * @note: this is managed by DeferredUpdates::execute(). Do not run this in a transaction.
         */
        public function doUpdate() {
+               // Make sure all links update threads see the changes of each other.
+               // This handles the case when updates have to batched into several COMMITs.
+               $scopedLock = self::acquirePageLock( $this->mDb, $this->mId );
+
                Hooks::run( 'LinksUpdate', [ &$this ] );
                $this->doIncrementalUpdate();
 
-               $this->mDb->onTransactionIdle( function() {
+               $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
                        Hooks::run( 'LinksUpdateComplete', [ &$this ] );
+                       // Release the lock *after* the final COMMIT for correctness
+                       ScopedCallback::consume( $scopedLock );
                } );
        }
 
+       /**
+        * Acquire a lock for performing link table updates for a page on a DB
+        *
+        * @param IDatabase $dbw
+        * @param integer $pageId
+        * @return ScopedCallback|null Returns null on failure
+        * @throws RuntimeException
+        * @since 1.27
+        */
+       public static function acquirePageLock( IDatabase $dbw, $pageId ) {
+               $scopedLock = $dbw->getScopedLockAndFlush(
+                       "LinksUpdate:pageid:$pageId",
+                       __METHOD__,
+                       15
+               );
+               if ( !$scopedLock ) {
+                       throw new RuntimeException( "Could not acquire lock on page #$pageId." );
+               }
+
+               return $scopedLock;
+       }
+
        protected function doIncrementalUpdate() {
                # Page links
                $existing = $this->getExistingLinks();
@@ -160,7 +195,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Image links
                $existing = $this->getExistingImages();
-
                $imageDeletes = $this->getImageDeletions( $existing );
                $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes,
                        $this->getImageInsertions( $existing ) );
@@ -191,9 +225,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Category links
                $existing = $this->getExistingCategories();
-
                $categoryDeletes = $this->getCategoryDeletions( $existing );
-
                $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes,
                        $this->getCategoryInsertions( $existing ) );
 
@@ -205,9 +237,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                # Page properties
                $existing = $this->getExistingProperties();
-
                $propertiesDeletes = $this->getPropertyDeletions( $existing );
-
                $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes,
                        $this->getPropertyInsertions( $existing ) );
 
@@ -307,44 +337,69 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         * @param array $deletions
         * @param array $insertions Rows to insert
         */
-       function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
-               if ( $table == 'page_props' ) {
+       private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
+               if ( $table === 'page_props' ) {
                        $fromField = 'pp_page';
                } else {
                        $fromField = "{$prefix}_from";
                }
-               $where = [ $fromField => $this->mId ];
-               if ( $table == 'pagelinks' || $table == 'templatelinks' || $table == 'iwlinks' ) {
-                       if ( $table == 'iwlinks' ) {
-                               $baseKey = 'iwl_prefix';
-                       } else {
-                               $baseKey = "{$prefix}_namespace";
+
+               $deleteWheres = []; // list of WHERE clause arrays for each DB delete() call
+               if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) {
+                       $baseKey =  ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace";
+
+                       $curBatchSize = 0;
+                       $curDeletionBatch = [];
+                       $deletionBatches = [];
+                       foreach ( $deletions as $ns => $dbKeys ) {
+                               foreach ( $dbKeys as $dbKey => $unused ) {
+                                       $curDeletionBatch[$ns][$dbKey] = 1;
+                                       if ( ++$curBatchSize >= self::BATCH_SIZE ) {
+                                               $deletionBatches[] = $curDeletionBatch;
+                                               $curDeletionBatch = [];
+                                               $curBatchSize = 0;
+                                       }
+                               }
                        }
-                       $clause = $this->mDb->makeWhereFrom2d( $deletions, $baseKey, "{$prefix}_title" );
-                       if ( $clause ) {
-                               $where[] = $clause;
-                       } else {
-                               $where = false;
+                       if ( $curDeletionBatch ) {
+                               $deletionBatches[] = $curDeletionBatch;
+                       }
+
+                       foreach ( $deletionBatches as $deletionBatch ) {
+                               $deleteWheres[] = [
+                                       $fromField => $this->mId,
+                                       $this->mDb->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
+                               ];
                        }
                } else {
-                       if ( $table == 'langlinks' ) {
+                       if ( $table === 'langlinks' ) {
                                $toField = 'll_lang';
-                       } elseif ( $table == 'page_props' ) {
+                       } elseif ( $table === 'page_props' ) {
                                $toField = 'pp_propname';
                        } else {
                                $toField = $prefix . '_to';
                        }
-                       if ( count( $deletions ) ) {
-                               $where[$toField] = array_keys( $deletions );
-                       } else {
-                               $where = false;
+
+                       $deletionBatches = array_chunk( array_keys( $deletions ), self::BATCH_SIZE );
+                       foreach ( $deletionBatches as $deletionBatch ) {
+                               $deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ];
                        }
                }
-               if ( $where ) {
-                       $this->mDb->delete( $table, $where, __METHOD__ );
+
+               foreach ( $deleteWheres as $deleteWhere ) {
+                       $this->mDb->delete( $table, $deleteWhere, __METHOD__ );
+                       $this->mDb->commit( __METHOD__, 'flush' );
+                       wfGetLBFactory()->waitForReplication();
+               }
+
+               $insertBatches = array_chunk( $insertions, self::BATCH_SIZE );
+               foreach ( $insertBatches as $insertBatch ) {
+                       $this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
+                       $this->mDb->commit( __METHOD__, 'flush' );
+                       wfGetLBFactory()->waitForReplication();
                }
+
                if ( count( $insertions ) ) {
-                       $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
                        Hooks::run( 'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
                }
        }
index 7a2ed50..711750b 100644 (file)
@@ -221,22 +221,27 @@ class OOUIHTMLForm extends HTMLForm {
                // FIXME This only works for forms with no subsections
                if ( $fieldset instanceof OOUI\FieldsetLayout ) {
                        $classes = [ 'mw-htmlform-ooui-header' ];
-                       if ( !$this->mHeader ) {
-                               $classes[] = 'mw-htmlform-ooui-header-empty';
-                       }
                        if ( $this->oouiErrors ) {
                                $classes[] = 'mw-htmlform-ooui-header-errors';
                        }
-                       $fieldset->addItems( [
-                               new OOUI\FieldLayout(
-                                       new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] ),
-                                       [
-                                               'align' => 'top',
-                                               'errors' => $this->oouiErrors,
-                                               'classes' => $classes,
-                                       ]
-                               )
-                       ], 0 );
+                       if ( $this->mHeader || $this->oouiErrors ) {
+                               // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
+                               if ( $this->mHeader ) {
+                                       $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
+                               } else {
+                                       $element = new OOUI\Widget( [] );
+                               }
+                               $fieldset->addItems( [
+                                       new OOUI\FieldLayout(
+                                               $element,
+                                               [
+                                                       'align' => 'top',
+                                                       'errors' => $this->oouiErrors,
+                                                       'classes' => $classes,
+                                               ]
+                                       )
+                               ], 0 );
+                       }
                }
                return $fieldset;
        }
index d0f067f..9c1ec8e 100644 (file)
  * @file
  */
 
-use RunningStat\RunningStat;
-
 /**
  * Convenience class for working with XHProf
  * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
  * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
  *
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
+ * @since 1.28
  */
 class Xhprof {
-
-       /**
-        * @var array $config
-        */
-       protected $config;
-
-       /**
-        * Hierarchical profiling data returned by xhprof.
-        * @var array $hieraData
-        */
-       protected $hieraData;
-
        /**
-        * Per-function inclusive data.
-        * @var array $inclusive
+        * @var bool $enabled Whether XHProf is currently running.
         */
-       protected $inclusive;
+       protected static $enabled;
 
        /**
-        * Per-function inclusive and exclusive data.
-        * @var array $complete
+        * Start xhprof profiler
         */
-       protected $complete;
-
-       /**
-        * Configuration data can contain:
-        * - flags:   Optional flags to add additional information to the
-        *            profiling data collected.
-        *            (XHPROF_FLAGS_NO_BUILTINS, XHPROF_FLAGS_CPU,
-        *            XHPROF_FLAGS_MEMORY)
-        * - exclude: Array of function names to exclude from profiling.
-        * - include: Array of function names to include in profiling.
-        * - sort:    Key to sort per-function reports on.
-        *
-        * Note: When running under HHVM, xhprof will always behave as though the
-        * XHPROF_FLAGS_NO_BUILTINS flag has been used unless the
-        * Eval.JitEnableRenameFunction option is enabled for the HHVM process.
-        *
-        * @param array $config
-        */
-       public function __construct( array $config = [] ) {
-               $this->config = array_merge(
-                       [
-                               'flags' => 0,
-                               'exclude' => [],
-                               'include' => null,
-                               'sort' => 'wt',
-                       ],
-                       $config
-               );
-
-               xhprof_enable( $this->config['flags'], [
-                       'ignored_functions' => $this->config['exclude']
-               ] );
+       public static function isEnabled() {
+               return self::$enabled;
        }
 
        /**
-        * Stop collecting profiling data.
-        *
-        * Only the first invocation of this method will effect the internal
-        * object state. Subsequent calls will return the data collected by the
-        * initial call.
-        *
-        * @return array Collected profiling data (possibly cached)
+        * Start xhprof profiler
         */
-       public function stop() {
-               if ( $this->hieraData === null ) {
-                       $this->hieraData = $this->pruneData( xhprof_disable() );
+       public static function enable( $flags = 0, $options = [] ) {
+               if ( self::isEnabled() ) {
+                       throw new Exception( 'Xhprof profiling is already enabled.' );
                }
-               return $this->hieraData;
-       }
-
-       /**
-        * Load raw data from a prior run for analysis.
-        * Stops any existing data collection and clears internal caches.
-        *
-        * Any 'include' filters configured for this Xhprof instance will be
-        * enforced on the data as it is loaded. 'exclude' filters will however
-        * not be enforced as they are an XHProf intrinsic behavior.
-        *
-        * @param array $data
-        * @see getRawData()
-        */
-       public function loadRawData( array $data ) {
-               $this->stop();
-               $this->inclusive = null;
-               $this->complete = null;
-               $this->hieraData = $this->pruneData( $data );
-       }
-
-       /**
-        * Get raw data collected by xhprof.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * Each key in the returned array is an edge label for the call graph in
-        * the form "caller==>callee". There is once special case edge labled
-        * simply "main()" which represents the global scope entry point of the
-        * application.
-        *
-        * XHProf will collect different data depending on the flags that are used:
-        * - ct:    Number of matching events seen.
-        * - wt:    Inclusive elapsed wall time for this event in microseconds.
-        * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
-        *          (XHPROF_FLAGS_CPU)
-        * - mu:    Delta of memory usage from start to end of callee in bytes.
-        *          (XHPROF_FLAGS_MEMORY)
-        * - pmu:   Delta of peak memory usage from start to end of callee in
-        *          bytes. (XHPROF_FLAGS_MEMORY)
-        * - alloc: Delta of amount memory requested from malloc() by the callee,
-        *          in bytes. (XHPROF_FLAGS_MALLOC)
-        * - free:  Delta of amount of memory passed to free() by the callee, in
-        *          bytes. (XHPROF_FLAGS_MALLOC)
-        *
-        * @return array
-        * @see stop()
-        * @see getInclusiveMetrics()
-        * @see getCompleteMetrics()
-        */
-       public function getRawData() {
-               return $this->stop();
+               self::$enabled = true;
+               xhprof_enable( $flags, $options );
        }
 
        /**
-        * Convert an xhprof data key into an array of ['parent', 'child']
-        * function names.
-        *
-        * The resulting array is left padded with nulls, so a key
-        * with no parent (eg 'main()') will return [null, 'function'].
+        * Stop xhprof profiler
         *
-        * @return array
+        * @return array|null xhprof data from the run, or null if xhprof was not running.
         */
-       public static function splitKey( $key ) {
-               return array_pad( explode( '==>', $key, 2 ), -2, null );
-       }
-
-       /**
-        * Remove data for functions that are not included in the 'include'
-        * configuration array.
-        *
-        * @param array $data Raw xhprof data
-        * @return array
-        */
-       protected function pruneData( $data ) {
-               if ( !$this->config['include'] ) {
-                       return $data;
-               }
-
-               $want = array_fill_keys( $this->config['include'], true );
-               $want['main()'] = true;
-
-               $keep = [];
-               foreach ( $data as $key => $stats ) {
-                       list( $parent, $child ) = self::splitKey( $key );
-                       if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
-                               $keep[$key] = $stats;
-                       }
+       public static function disable() {
+               if ( self::isEnabled() ) {
+                       self::$enabled = false;
+                       return xhprof_disable();
                }
-               return $keep;
-       }
-
-       /**
-        * Get the inclusive metrics for each function call. Inclusive metrics
-        * for given function include the metrics for all functions that were
-        * called from that function during the measurement period.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * See getRawData() for a description of the metric that are returned for
-        * each funcition call. The values for the wt, cpu, mu and pmu metrics are
-        * arrays with these values:
-        * - total: Cumulative value
-        * - min: Minimum value
-        * - mean: Mean (average) value
-        * - max: Maximum value
-        * - variance: Variance (spread) of the values
-        *
-        * @return array
-        * @see getRawData()
-        * @see getCompleteMetrics()
-        */
-       public function getInclusiveMetrics() {
-               if ( $this->inclusive === null ) {
-                       // Make sure we have data to work with
-                       $this->stop();
-
-                       $main = $this->hieraData['main()'];
-                       $hasCpu = isset( $main['cpu'] );
-                       $hasMu = isset( $main['mu'] );
-                       $hasAlloc = isset( $main['alloc'] );
-
-                       $this->inclusive = [];
-                       foreach ( $this->hieraData as $key => $stats ) {
-                               list( $parent, $child ) = self::splitKey( $key );
-                               if ( !isset( $this->inclusive[$child] ) ) {
-                                       $this->inclusive[$child] = [
-                                               'ct' => 0,
-                                               'wt' => new RunningStat(),
-                                       ];
-                                       if ( $hasCpu ) {
-                                               $this->inclusive[$child]['cpu'] = new RunningStat();
-                                       }
-                                       if ( $hasMu ) {
-                                               $this->inclusive[$child]['mu'] = new RunningStat();
-                                               $this->inclusive[$child]['pmu'] = new RunningStat();
-                                       }
-                                       if ( $hasAlloc ) {
-                                               $this->inclusive[$child]['alloc'] = new RunningStat();
-                                               $this->inclusive[$child]['free'] = new RunningStat();
-                                       }
-                               }
-
-                               $this->inclusive[$child]['ct'] += $stats['ct'];
-                               foreach ( $stats as $stat => $value ) {
-                                       if ( $stat === 'ct' ) {
-                                               continue;
-                                       }
-
-                                       if ( !isset( $this->inclusive[$child][$stat] ) ) {
-                                               // Ignore unknown stats
-                                               continue;
-                                       }
-
-                                       for ( $i = 0; $i < $stats['ct']; $i++ ) {
-                                               $this->inclusive[$child][$stat]->addObservation(
-                                                       $value / $stats['ct']
-                                               );
-                                       }
-                               }
-                       }
-
-                       // Convert RunningStat instances to static arrays and add
-                       // percentage stats.
-                       foreach ( $this->inclusive as $func => $stats ) {
-                               foreach ( $stats as $name => $value ) {
-                                       if ( $value instanceof RunningStat ) {
-                                               $total = $value->m1 * $value->n;
-                                               $percent = ( isset( $main[$name] ) && $main[$name] )
-                                                       ? 100 * $total / $main[$name]
-                                                       : 0;
-                                               $this->inclusive[$func][$name] = [
-                                                       'total' => $total,
-                                                       'min' => $value->min,
-                                                       'mean' => $value->m1,
-                                                       'max' => $value->max,
-                                                       'variance' => $value->m2,
-                                                       'percent' => $percent,
-                                               ];
-                                       }
-                               }
-                       }
-
-                       uasort( $this->inclusive, self::makeSortFunction(
-                               $this->config['sort'], 'total'
-                       ) );
-               }
-               return $this->inclusive;
-       }
-
-       /**
-        * Get the inclusive and exclusive metrics for each function call.
-        *
-        * If data collection has not been stopped yet this method will halt
-        * collection to gather the profiling data.
-        *
-        * In addition to the normal data contained in the inclusive metrics, the
-        * metrics have an additional 'exclusive' measurement which is the total
-        * minus the totals of all child function calls.
-        *
-        * @return array
-        * @see getRawData()
-        * @see getInclusiveMetrics()
-        */
-       public function getCompleteMetrics() {
-               if ( $this->complete === null ) {
-                       // Start with inclusive data
-                       $this->complete = $this->getInclusiveMetrics();
-
-                       foreach ( $this->complete as $func => $stats ) {
-                               foreach ( $stats as $stat => $value ) {
-                                       if ( $stat === 'ct' ) {
-                                               continue;
-                                       }
-                                       // Initialize exclusive data with inclusive totals
-                                       $this->complete[$func][$stat]['exclusive'] = $value['total'];
-                               }
-                               // Add sapce for call tree information to be filled in later
-                               $this->complete[$func]['calls'] = [];
-                               $this->complete[$func]['subcalls'] = [];
-                       }
-
-                       foreach ( $this->hieraData as $key => $stats ) {
-                               list( $parent, $child ) = self::splitKey( $key );
-                               if ( $parent !== null ) {
-                                       // Track call tree information
-                                       $this->complete[$child]['calls'][$parent] = $stats;
-                                       $this->complete[$parent]['subcalls'][$child] = $stats;
-                               }
-
-                               if ( isset( $this->complete[$parent] ) ) {
-                                       // Deduct child inclusive data from exclusive data
-                                       foreach ( $stats as $stat => $value ) {
-                                               if ( $stat === 'ct' ) {
-                                                       continue;
-                                               }
-
-                                               if ( !isset( $this->complete[$parent][$stat] ) ) {
-                                                       // Ignore unknown stats
-                                                       continue;
-                                               }
-
-                                               $this->complete[$parent][$stat]['exclusive'] -= $value;
-                                       }
-                               }
-                       }
-
-                       uasort( $this->complete, self::makeSortFunction(
-                               $this->config['sort'], 'exclusive'
-                       ) );
-               }
-               return $this->complete;
-       }
-
-       /**
-        * Get a list of all callers of a given function.
-        *
-        * @param string $function Function name
-        * @return array
-        * @see getEdges()
-        */
-       public function getCallers( $function ) {
-               $edges = $this->getCompleteMetrics();
-               if ( isset( $edges[$function]['calls'] ) ) {
-                       return array_keys( $edges[$function]['calls'] );
-               } else {
-                       return [];
-               }
-       }
-
-       /**
-        * Get a list of all callees from a given function.
-        *
-        * @param string $function Function name
-        * @return array
-        * @see getEdges()
-        */
-       public function getCallees( $function ) {
-               $edges = $this->getCompleteMetrics();
-               if ( isset( $edges[$function]['subcalls'] ) ) {
-                       return array_keys( $edges[$function]['subcalls'] );
-               } else {
-                       return [];
-               }
-       }
-
-       /**
-        * Find the critical path for the given metric.
-        *
-        * @param string $metric Metric to find critical path for
-        * @return array
-        */
-       public function getCriticalPath( $metric = 'wt' ) {
-               $this->stop();
-               $func = 'main()';
-               $path = [
-                       $func => $this->hieraData[$func],
-               ];
-               while ( $func ) {
-                       $callees = $this->getCallees( $func );
-                       $maxCallee = null;
-                       $maxCall = null;
-                       foreach ( $callees as $callee ) {
-                               $call = "{$func}==>{$callee}";
-                               if ( $maxCall === null ||
-                                       $this->hieraData[$call][$metric] >
-                                               $this->hieraData[$maxCall][$metric]
-                               ) {
-                                       $maxCallee = $callee;
-                                       $maxCall = $call;
-                               }
-                       }
-                       if ( $maxCall !== null ) {
-                               $path[$maxCall] = $this->hieraData[$maxCall];
-                       }
-                       $func = $maxCallee;
-               }
-               return $path;
-       }
-
-       /**
-        * Make a closure to use as a sort function. The resulting function will
-        * sort by descending numeric values (largest value first).
-        *
-        * @param string $key Data key to sort on
-        * @param string $sub Sub key to sort array values on
-        * @return Closure
-        */
-       public static function makeSortFunction( $key, $sub ) {
-               return function ( $a, $b ) use ( $key, $sub ) {
-                       if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
-                               // Descending sort: larger values will be first in result.
-                               // Assumes all values are numeric.
-                               // Values for 'main()' will not have sub keys
-                               $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
-                               $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
-                               return $valB - $valA;
-                       } else {
-                               // Sort datum with the key before those without
-                               return isset( $a[$key] ) ? -1 : 1;
-                       }
-               };
        }
 }
diff --git a/includes/libs/XhprofData.php b/includes/libs/XhprofData.php
new file mode 100644 (file)
index 0000000..c6da432
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use RunningStat\RunningStat;
+
+/**
+ * Convenience class for working with XHProf profiling data
+ * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
+ * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
+ *
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.28
+ */
+class XhprofData {
+
+       /**
+        * @var array $config
+        */
+       protected $config;
+
+       /**
+        * Hierarchical profiling data returned by xhprof.
+        * @var array $hieraData
+        */
+       protected $hieraData;
+
+       /**
+        * Per-function inclusive data.
+        * @var array $inclusive
+        */
+       protected $inclusive;
+
+       /**
+        * Per-function inclusive and exclusive data.
+        * @var array $complete
+        */
+       protected $complete;
+
+       /**
+        * Configuration data can contain:
+        * - include: Array of function names to include in profiling.
+        * - sort:    Key to sort per-function reports on.
+        *
+        * @param array $data Xhprof profiling data, as returned by xhprof_disable()
+        * @param array $config
+        */
+       public function __construct( array $data, array $config = [] ) {
+               $this->config = array_merge( [
+                       'include' => null,
+                       'sort' => 'wt',
+               ], $config );
+
+               $this->hieraData = $this->pruneData( $data );
+       }
+
+       /**
+        * Get raw data collected by xhprof.
+        *
+        * Each key in the returned array is an edge label for the call graph in
+        * the form "caller==>callee". There is once special case edge labled
+        * simply "main()" which represents the global scope entry point of the
+        * application.
+        *
+        * XHProf will collect different data depending on the flags that are used:
+        * - ct:    Number of matching events seen.
+        * - wt:    Inclusive elapsed wall time for this event in microseconds.
+        * - cpu:   Inclusive elapsed cpu time for this event in microseconds.
+        *          (XHPROF_FLAGS_CPU)
+        * - mu:    Delta of memory usage from start to end of callee in bytes.
+        *          (XHPROF_FLAGS_MEMORY)
+        * - pmu:   Delta of peak memory usage from start to end of callee in
+        *          bytes. (XHPROF_FLAGS_MEMORY)
+        * - alloc: Delta of amount memory requested from malloc() by the callee,
+        *          in bytes. (XHPROF_FLAGS_MALLOC)
+        * - free:  Delta of amount of memory passed to free() by the callee, in
+        *          bytes. (XHPROF_FLAGS_MALLOC)
+        *
+        * @return array
+        * @see getInclusiveMetrics()
+        * @see getCompleteMetrics()
+        */
+       public function getRawData() {
+               return $this->hieraData;
+       }
+
+       /**
+        * Convert an xhprof data key into an array of ['parent', 'child']
+        * function names.
+        *
+        * The resulting array is left padded with nulls, so a key
+        * with no parent (eg 'main()') will return [null, 'function'].
+        *
+        * @return array
+        */
+       public static function splitKey( $key ) {
+               return array_pad( explode( '==>', $key, 2 ), -2, null );
+       }
+
+       /**
+        * Remove data for functions that are not included in the 'include'
+        * configuration array.
+        *
+        * @param array $data Raw xhprof data
+        * @return array
+        */
+       protected function pruneData( $data ) {
+               if ( !$this->config['include'] ) {
+                       return $data;
+               }
+
+               $want = array_fill_keys( $this->config['include'], true );
+               $want['main()'] = true;
+
+               $keep = [];
+               foreach ( $data as $key => $stats ) {
+                       list( $parent, $child ) = self::splitKey( $key );
+                       if ( isset( $want[$parent] ) || isset( $want[$child] ) ) {
+                               $keep[$key] = $stats;
+                       }
+               }
+               return $keep;
+       }
+
+       /**
+        * Get the inclusive metrics for each function call. Inclusive metrics
+        * for given function include the metrics for all functions that were
+        * called from that function during the measurement period.
+        *
+        * See getRawData() for a description of the metric that are returned for
+        * each funcition call. The values for the wt, cpu, mu and pmu metrics are
+        * arrays with these values:
+        * - total: Cumulative value
+        * - min: Minimum value
+        * - mean: Mean (average) value
+        * - max: Maximum value
+        * - variance: Variance (spread) of the values
+        *
+        * @return array
+        * @see getRawData()
+        * @see getCompleteMetrics()
+        */
+       public function getInclusiveMetrics() {
+               if ( $this->inclusive === null ) {
+                       $main = $this->hieraData['main()'];
+                       $hasCpu = isset( $main['cpu'] );
+                       $hasMu = isset( $main['mu'] );
+                       $hasAlloc = isset( $main['alloc'] );
+
+                       $this->inclusive = [];
+                       foreach ( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( !isset( $this->inclusive[$child] ) ) {
+                                       $this->inclusive[$child] = [
+                                               'ct' => 0,
+                                               'wt' => new RunningStat(),
+                                       ];
+                                       if ( $hasCpu ) {
+                                               $this->inclusive[$child]['cpu'] = new RunningStat();
+                                       }
+                                       if ( $hasMu ) {
+                                               $this->inclusive[$child]['mu'] = new RunningStat();
+                                               $this->inclusive[$child]['pmu'] = new RunningStat();
+                                       }
+                                       if ( $hasAlloc ) {
+                                               $this->inclusive[$child]['alloc'] = new RunningStat();
+                                               $this->inclusive[$child]['free'] = new RunningStat();
+                                       }
+                               }
+
+                               $this->inclusive[$child]['ct'] += $stats['ct'];
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+
+                                       if ( !isset( $this->inclusive[$child][$stat] ) ) {
+                                               // Ignore unknown stats
+                                               continue;
+                                       }
+
+                                       for ( $i = 0; $i < $stats['ct']; $i++ ) {
+                                               $this->inclusive[$child][$stat]->addObservation(
+                                                       $value / $stats['ct']
+                                               );
+                                       }
+                               }
+                       }
+
+                       // Convert RunningStat instances to static arrays and add
+                       // percentage stats.
+                       foreach ( $this->inclusive as $func => $stats ) {
+                               foreach ( $stats as $name => $value ) {
+                                       if ( $value instanceof RunningStat ) {
+                                               $total = $value->m1 * $value->n;
+                                               $percent = ( isset( $main[$name] ) && $main[$name] )
+                                                       ? 100 * $total / $main[$name]
+                                                       : 0;
+                                               $this->inclusive[$func][$name] = [
+                                                       'total' => $total,
+                                                       'min' => $value->min,
+                                                       'mean' => $value->m1,
+                                                       'max' => $value->max,
+                                                       'variance' => $value->m2,
+                                                       'percent' => $percent,
+                                               ];
+                                       }
+                               }
+                       }
+
+                       uasort( $this->inclusive, self::makeSortFunction(
+                               $this->config['sort'], 'total'
+                       ) );
+               }
+               return $this->inclusive;
+       }
+
+       /**
+        * Get the inclusive and exclusive metrics for each function call.
+        *
+        * In addition to the normal data contained in the inclusive metrics, the
+        * metrics have an additional 'exclusive' measurement which is the total
+        * minus the totals of all child function calls.
+        *
+        * @return array
+        * @see getRawData()
+        * @see getInclusiveMetrics()
+        */
+       public function getCompleteMetrics() {
+               if ( $this->complete === null ) {
+                       // Start with inclusive data
+                       $this->complete = $this->getInclusiveMetrics();
+
+                       foreach ( $this->complete as $func => $stats ) {
+                               foreach ( $stats as $stat => $value ) {
+                                       if ( $stat === 'ct' ) {
+                                               continue;
+                                       }
+                                       // Initialize exclusive data with inclusive totals
+                                       $this->complete[$func][$stat]['exclusive'] = $value['total'];
+                               }
+                               // Add sapce for call tree information to be filled in later
+                               $this->complete[$func]['calls'] = [];
+                               $this->complete[$func]['subcalls'] = [];
+                       }
+
+                       foreach ( $this->hieraData as $key => $stats ) {
+                               list( $parent, $child ) = self::splitKey( $key );
+                               if ( $parent !== null ) {
+                                       // Track call tree information
+                                       $this->complete[$child]['calls'][$parent] = $stats;
+                                       $this->complete[$parent]['subcalls'][$child] = $stats;
+                               }
+
+                               if ( isset( $this->complete[$parent] ) ) {
+                                       // Deduct child inclusive data from exclusive data
+                                       foreach ( $stats as $stat => $value ) {
+                                               if ( $stat === 'ct' ) {
+                                                       continue;
+                                               }
+
+                                               if ( !isset( $this->complete[$parent][$stat] ) ) {
+                                                       // Ignore unknown stats
+                                                       continue;
+                                               }
+
+                                               $this->complete[$parent][$stat]['exclusive'] -= $value;
+                                       }
+                               }
+                       }
+
+                       uasort( $this->complete, self::makeSortFunction(
+                               $this->config['sort'], 'exclusive'
+                       ) );
+               }
+               return $this->complete;
+       }
+
+       /**
+        * Get a list of all callers of a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallers( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['calls'] ) ) {
+                       return array_keys( $edges[$function]['calls'] );
+               } else {
+                       return [];
+               }
+       }
+
+       /**
+        * Get a list of all callees from a given function.
+        *
+        * @param string $function Function name
+        * @return array
+        * @see getEdges()
+        */
+       public function getCallees( $function ) {
+               $edges = $this->getCompleteMetrics();
+               if ( isset( $edges[$function]['subcalls'] ) ) {
+                       return array_keys( $edges[$function]['subcalls'] );
+               } else {
+                       return [];
+               }
+       }
+
+       /**
+        * Find the critical path for the given metric.
+        *
+        * @param string $metric Metric to find critical path for
+        * @return array
+        */
+       public function getCriticalPath( $metric = 'wt' ) {
+               $func = 'main()';
+               $path = [
+                       $func => $this->hieraData[$func],
+               ];
+               while ( $func ) {
+                       $callees = $this->getCallees( $func );
+                       $maxCallee = null;
+                       $maxCall = null;
+                       foreach ( $callees as $callee ) {
+                               $call = "{$func}==>{$callee}";
+                               if ( $maxCall === null ||
+                                       $this->hieraData[$call][$metric] >
+                                               $this->hieraData[$maxCall][$metric]
+                               ) {
+                                       $maxCallee = $callee;
+                                       $maxCall = $call;
+                               }
+                       }
+                       if ( $maxCall !== null ) {
+                               $path[$maxCall] = $this->hieraData[$maxCall];
+                       }
+                       $func = $maxCallee;
+               }
+               return $path;
+       }
+
+       /**
+        * Make a closure to use as a sort function. The resulting function will
+        * sort by descending numeric values (largest value first).
+        *
+        * @param string $key Data key to sort on
+        * @param string $sub Sub key to sort array values on
+        * @return Closure
+        */
+       public static function makeSortFunction( $key, $sub ) {
+               return function ( $a, $b ) use ( $key, $sub ) {
+                       if ( isset( $a[$key] ) && isset( $b[$key] ) ) {
+                               // Descending sort: larger values will be first in result.
+                               // Assumes all values are numeric.
+                               // Values for 'main()' will not have sub keys
+                               $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key];
+                               $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key];
+                               return $valB - $valA;
+                       } else {
+                               // Sort datum with the key before those without
+                               return isset( $a[$key] ) ? -1 : 1;
+                       }
+               };
+       }
+}
index a55ddf3..3b8b513 100644 (file)
@@ -456,10 +456,18 @@ class CoreParserFunctions {
                                                $converter->markNoConversion( wfEscapeWikiText( $text ) )
                                        )->inContentLanguage()->text() .
                                        '</span>';
+                       } else {
+                               return '';
                        }
+               } else {
+                       $converter = $parser->getConverterLanguage()->getConverter();
+                       return '<span class="error">' .
+                               wfMessage( 'restricted-displaytitle',
+                                       // Message should be parsed, but this param should only be escaped.
+                                       $converter->markNoConversion( wfEscapeWikiText( $text ) )
+                               )->inContentLanguage()->text() .
+                               '</span>';
                }
-
-               return '';
        }
 
        /**
index 04b5614..8575e69 100644 (file)
@@ -282,7 +282,7 @@ class LinkHolderArray {
                        return;
                }
 
-               global $wgContLang, $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+               global $wgContLang;
 
                $colours = [];
                $linkCache = LinkCache::singleton();
@@ -297,7 +297,9 @@ class LinkHolderArray {
                $linkcolour_ids = [];
 
                # Generate query
-               $queries = [];
+               $lb = new LinkBatch();
+               $lb->setCaller( __METHOD__ );
+
                foreach ( $this->internals as $ns => $entries ) {
                        foreach ( $entries as $entry ) {
                                /** @var Title $title */
@@ -325,37 +327,21 @@ class LinkHolderArray {
                                                $colours[$pdbk] = 'new';
                                        } else {
                                                # Not in the link cache, add it to the query
-                                               $queries[$ns][] = $title->getDBkey();
+                                               $lb->addObj( $title );
                                        }
                                }
                        }
                }
-               if ( $queries ) {
-                       $where = [];
-                       foreach ( $queries as $ns => $pages ) {
-                               $where[] = $dbr->makeList(
-                                       [
-                                               'page_namespace' => $ns,
-                                               'page_title' => array_unique( $pages ),
-                                       ],
-                                       LIST_AND
-                               );
-                       }
-
-                       $fields = [ 'page_id', 'page_namespace', 'page_title',
-                               'page_is_redirect', 'page_len', 'page_latest' ];
-
-                       if ( $wgContentHandlerUseDB ) {
-                               $fields[] = 'page_content_model';
-                       }
-                       if ( $wgPageLanguageUseDB ) {
-                               $fields[] = 'page_lang';
-                       }
+               if ( !$lb->isEmpty() ) {
+                       $fields = array_merge(
+                               LinkCache::getSelectFields(),
+                               [ 'page_namespace', 'page_title' ]
+                       );
 
                        $res = $dbr->select(
                                'page',
                                $fields,
-                               $dbr->makeList( $where, LIST_OR ),
+                               $lb->constructSet( 'page', $dbr ),
                                __METHOD__
                        );
 
@@ -463,7 +449,7 @@ class LinkHolderArray {
         * @param array $colours
         */
        protected function doVariants( &$colours ) {
-               global $wgContLang, $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+               global $wgContLang;
                $linkBatch = new LinkBatch();
                $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
                $output = $this->parent->getOutput();
@@ -513,9 +499,6 @@ class LinkHolderArray {
                                }
 
                                $variantTitle = Title::makeTitle( $ns, $textVariant );
-                               if ( is_null( $variantTitle ) ) {
-                                       continue;
-                               }
 
                                // Self-link checking for mixed/different variant titles. At this point, we
                                // already know the exact title does not exist, so the link cannot be to a
@@ -552,15 +535,10 @@ class LinkHolderArray {
                if ( !$linkBatch->isEmpty() ) {
                        // construct query
                        $dbr = wfGetDB( DB_SLAVE );
-                       $fields = [ 'page_id', 'page_namespace', 'page_title',
-                               'page_is_redirect', 'page_len', 'page_latest' ];
-
-                       if ( $wgContentHandlerUseDB ) {
-                               $fields[] = 'page_content_model';
-                       }
-                       if ( $wgPageLanguageUseDB ) {
-                               $fields[] = 'page_lang';
-                       }
+                       $fields = array_merge(
+                               LinkCache::getSelectFields(),
+                               [ 'page_namespace', 'page_title' ]
+                       );
 
                        $varRes = $dbr->select( 'page',
                                $fields,
index c168aa6..4ed176c 100644 (file)
@@ -50,7 +50,7 @@ class StripState {
                        'nowiki' => [],
                        'general' => []
                ];
-               $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f]+)" . Parser::MARKER_SUFFIX . '/';
+               $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
                $this->circularRefGuard = [];
        }
 
index 7c4fde4..8fc0b77 100644 (file)
@@ -52,9 +52,9 @@
  */
 class ProfilerXhprof extends Profiler {
        /**
-        * @var Xhprof $xhprof
+        * @var XhprofData|null $xhprofData
         */
-       protected $xhprof;
+       protected $xhprofData;
 
        /**
         * Profiler for explicit, arbitrary, frame labels
@@ -68,10 +68,24 @@ class ProfilerXhprof extends Profiler {
         */
        public function __construct( array $params = [] ) {
                parent::__construct( $params );
-               $this->xhprof = new Xhprof( $params );
+
+               $flags = isset( $params['flags'] ) ? $params['flags'] : 0;
+               $options = isset( $params['exclude'] )
+                       ? [ 'ignored_functions' => $params['exclude'] ] : [];
+               Xhprof::enable( $flags, $options );
                $this->sprofiler = new SectionProfiler();
        }
 
+       /**
+        * @return XhprofData
+        */
+       public function getXhprofData() {
+               if ( !$this->xhprofData ) {
+                       $this->xhprofData = new XhprofData( Xhprof::disable(), $this->params );
+               }
+               return $this->xhprofData;
+       }
+
        public function scopedProfileIn( $section ) {
                $key = 'section.' . ltrim( $section, '.' );
                return $this->sprofiler->scopedProfileIn( $key );
@@ -112,7 +126,7 @@ class ProfilerXhprof extends Profiler {
        }
 
        public function getFunctionStats() {
-               $metrics = $this->xhprof->getCompleteMetrics();
+               $metrics = $this->getXhprofData()->getCompleteMetrics();
                $profile = [];
 
                $main = null; // units in ms
@@ -216,6 +230,6 @@ class ProfilerXhprof extends Profiler {
         * @return array
         */
        public function getRawData() {
-               return $this->xhprof->getRawData();
+               return $this->getXhprofData()->getRawData();
        }
 }
index e79fd6e..521f0ce 100644 (file)
@@ -77,6 +77,7 @@ class SpecialTags extends SpecialPage {
 
                $user = $this->getUser();
                $userCanManage = $user->isAllowed( 'managechangetags' );
+               $userCanDelete = $user->isAllowed( 'deletechangetags' );
                $userCanEditInterface = $user->isAllowed( 'editinterface' );
 
                // Show form to create a tag
@@ -154,12 +155,13 @@ class SpecialTags extends SpecialPage {
 
                // Insert tags that have been applied at least once
                foreach ( $tagStats as $tag => $hitcount ) {
-                       $html .= $this->doTagRow( $tag, $hitcount, $userCanManage, $userCanEditInterface );
+                       $html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
+                               $userCanDelete, $userCanEditInterface );
                }
                // Insert tags defined somewhere but never applied
                foreach ( $definedTags as $tag ) {
                        if ( !isset( $tagStats[$tag] ) ) {
-                               $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanEditInterface );
+                               $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
                        }
                }
 
@@ -170,7 +172,7 @@ class SpecialTags extends SpecialPage {
                ) );
        }
 
-       function doTagRow( $tag, $hitcount, $showActions, $showEditLinks ) {
+       function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
                $newRow = '';
                $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
 
@@ -229,16 +231,17 @@ class SpecialTags extends SpecialPage {
                $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
 
                // actions
-               if ( $showActions ) { // we've already checked that the user had the requisite userright
-                       $actionLinks = [];
+               $actionLinks = [];
 
-                       // delete
-                       if ( ChangeTags::canDeleteTag( $tag )->isOK() ) {
-                               $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ),
-                                       $this->msg( 'tags-delete' )->escaped(),
-                                       [],
-                                       [ 'tag' => $tag ] );
-                       }
+               // delete
+               if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
+                       $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ),
+                               $this->msg( 'tags-delete' )->escaped(),
+                               [],
+                               [ 'tag' => $tag ] );
+               }
+
+               if ( $showManageActions ) { // we've already checked that the user had the requisite userright
 
                        // activate
                        if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
@@ -319,8 +322,8 @@ class SpecialTags extends SpecialPage {
 
        protected function showDeleteTagForm( $tag ) {
                $user = $this->getUser();
-               if ( !$user->isAllowed( 'managechangetags' ) ) {
-                       throw new PermissionsError( 'managechangetags' );
+               if ( !$user->isAllowed( 'deletechangetags' ) ) {
+                       throw new PermissionsError( 'deletechangetags' );
                }
 
                $out = $this->getOutput();
index 5c504f3..38e9ecd 100644 (file)
@@ -192,12 +192,18 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
                if ( $target->isExternal() ) {
                        $key .= $target->getInterwiki() . ':';
                }
-               $nsName = $this->getNamespaceName(
-                       $target->getNamespace(),
-                       $target->getText()
-               );
+               // Try to get a namespace name, but fallback
+               // to empty string if it doesn't exist
+               try {
+                       $nsName = $this->getNamespaceName(
+                               $target->getNamespace(),
+                               $target->getText()
+                       );
+               } catch ( InvalidArgumentException $e ) {
+                       $nsName = '';
+               }
 
-               if ( $nsName !== '' ) {
+               if ( $target->getNamespace() !== 0 ) {
                        $key .= $nsName . ':';
                }
 
index c748e6a..1b0bcc0 100644 (file)
@@ -127,6 +127,7 @@ class User implements IDBAccessObject {
                'createpage',
                'createtalk',
                'delete',
+               'deletechangetags',
                'deletedhistory',
                'deletedtext',
                'deletelogentry',
@@ -3404,12 +3405,14 @@ class User implements IDBAccessObject {
         * @since 1.28
         */
        public function isBot() {
-               $isBot = false;
-               if ( !Hooks::run( "UserIsBot", [ $this, &$isBot ] ) ) {
-                       return $isBot;
+               if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
+                       return true;
                }
 
-               return ( $isBot || in_array( 'bot', $this->getGroups() ) );
+               $isBot = false;
+               Hooks::run( "UserIsBot", [ $this, &$isBot ] );
+
+               return $isBot;
        }
 
        /**
index 5f93c6f..96a7359 100644 (file)
        "passwordremindertitle": "Новы часовы пароль для {{GRAMMAR:родны|{{SITENAME}}}}",
        "passwordremindertext": "Нехта (магчыма Вы, з IP-адрасу $1) запытаў нас даслаць новы пароль для {{GRAMMAR:родны|{{SITENAME}}}} ($4). Для ўдзельніка «$2» быў створаны часовы пароль і ён цяпер «$3». Калі гэта была Вашая ініцыятыва, Вам трэба ўвайсьці ў сыстэму і адразу зьмяніць пароль. Тэрмін дзеяньня Вашага часовага паролю — $5 {{PLURAL:$5|дзень|дні|дзён}}.\n\nКалі гэты запыт адправіў нехта іншы, альбо Вы ўзгадалі свой пароль і ўжо не жадаеце яго зьмяніць, Вы можаце праігнараваць гэты ліст і працягваць карыстацца старым паролем.",
        "noemail": "{{GENDER:$1|Удзельнік «$1» не пазначыў|Удзельніца «$1» не пазначыла}} адрас электроннай пошты.",
-       "noemailcreate": "Вы павінны пазначыць слушны адрас электроннай пошты",
+       "noemailcreate": "Вы павінныя пазначыць слушны адрас электроннай пошты.",
        "passwordsent": "Новы пароль быў дасланы на адрас электроннай пошты ўдзельніка «$1».\nКалі ласка, увайдзіце ў сыстэму пасьля яго атрыманьня.",
        "blocked-mailpassword": "З Вашага IP-адрасу забароненыя рэдагаваньні. Каб пазьбегнуць злоўжываньняў, з гэтага IP-адрасу забаронена аднаўляць пароль.",
        "eauthentsent": "Пацьверджаньне было дасланае на пазначаны адрас электроннай пошты.\nУ лісьце ўтрымліваюцца інструкцыі, па выкананьні якіх Вы зможаце пацьвердзіць, што адрас сапраўды належыць Вам, і на гэты адрас будзе дасылацца пошта адсюль.",
        "right-override-export-depth": "экспартаваньне старонак, уключаючы зьвязаныя старонкі з глыбінёй да 5",
        "right-sendemail": "адпраўка электронных лістоў іншым удзельнікам",
        "right-passwordreset": "прагляд электронных лістоў з ачысткай паролю",
-       "right-managechangetags": "ствараць і выдаляць [[Special:Tags|меткі]] з базы зьвестак",
+       "right-managechangetags": "стварэньне і (дэ)актывацыя [[Special:Tags|метак]]",
        "right-applychangetags": "дадаваць [[Special:Tags|меткі]] пры рэдагаваньні",
        "right-changetags": "дадаваць і выдаляць адвольныя [[Special:Tags|меткі]] да асобных вэрсіяў і запісаў у журнале падзеяў",
        "grant-generic": "Набор правоў «$1»",
        "action-viewmyprivateinfo": "прагляд вашых прыватных зьвестак",
        "action-editmyprivateinfo": "рэдагаваньне вашых прыватных зьвестак",
        "action-editcontentmodel": "рэдагаваньне мадэлі зьместу старонкі",
-       "action-managechangetags": "стварэньне і выдаленьне метак з базы зьвестак",
+       "action-managechangetags": "стварэньне і (дэ)актывацыю метак",
        "action-applychangetags": "дадаваньне метак пры рэдагаваньні",
        "action-changetags": "дадаваньне і выдаленьне адвольных метак да асобных вэрсіяў і запісаў у журнале падзеяў",
        "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
index f018af4..a565f4f 100644 (file)
        "minoredit": "Дробная праўка",
        "watchthis": "Назіраць за гэтай старонкай",
        "savearticle": "Запісаць",
+       "publishpage": "Апублікаваць старонку",
        "preview": "Перадпаказ",
        "showpreview": "Як будзе",
        "showdiff": "Розніца",
index b260279..f3c4a68 100644 (file)
        "right-override-export-depth": "Exportovat stránky včetně odkazovaných stránek až do hloubky 5",
        "right-sendemail": "Odesílání e-mailů ostatním uživatelům",
        "right-passwordreset": "Prohlížení e-mailů pro znovunastavení hesla",
-       "right-managechangetags": "Vytváření [[Special:Tags|značek]] a jejich mazání z databáze",
+       "right-managechangetags": "Vytváření a (de)aktivace [[Special:Tags|značek]]",
        "right-applychangetags": "Přidávání [[Special:Tags|značek]] k vlastním změnám",
        "right-changetags": "Přidávání libovolných [[Special:Tags|značek]] na jednotlivé revize a protokolovací záznamy a jejich odebírání",
+       "right-deletechangetags": "Mazání [[Special:Tags|značek]] z databáze",
        "grant-generic": "Balíček oprávnění „$1“",
        "grant-group-page-interaction": "Interakce se stránkami",
        "grant-group-file-interaction": "Interakce se soubory",
        "action-viewmyprivateinfo": "prohlížet si své soukromé údaje",
        "action-editmyprivateinfo": "změnit své soukromé údaje",
        "action-editcontentmodel": "editovat model obsahu stránky",
-       "action-managechangetags": "vytvářet a mazat značky z databáze",
+       "action-managechangetags": "vytvářet a (de)aktivovat značky",
        "action-applychangetags": "přidávat značky k vlastním změnám",
        "action-changetags": "přidávat libovolné značky na jednotlivé revize a protokolovací záznamy a odebírat je",
+       "action-deletechangetags": "mazat značky z databáze",
        "nchanges": "$1 {{PLURAL:$1|změna|změny|změn}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od poslední návštěvy}}",
        "enhancedrc-history": "historie",
        "tags-delete-not-found": "Značka „$1“ neexistuje.",
        "tags-delete-too-many-uses": "Značkou „$1“ {{PLURAL:$2|je označena více než $2 revize|jsou označeny více než $2 revize|je označeno více než $2 revizí}}, což znamená, že ji nelze smazat.",
        "tags-delete-warnings-after-delete": "Značka „$1“ byla smazána, ale {{PLURAL:$2|bylo zjištěno|byla zjištěna}} následující varování:",
+       "tags-delete-no-permission": "Nemáte oprávnění mazat značky pro změny.",
        "tags-activate-title": "Aktivovat značku",
        "tags-activate-question": "Chystáte se aktivovat značku „$1“.",
        "tags-activate-reason": "Důvod:",
index be7ed04..526fa88 100644 (file)
        "databaseerror-query": "Abfrage: $1",
        "databaseerror-function": "Funktion: $1",
        "databaseerror-error": "Fehler: $1",
-       "transaction-duration-limit-exceeded": "Um eine hohe Nachbildungsverzögerung zu vermeiden, wurde diese Transaktion abgebrochen, da die Schreibdauer ($1) die Grenze von {{PLURAL:$2|einer Sekunde|$2 Sekunden}} überschritten hat. Falls du viele Objekte auf einmal änderst, versuche stattdessen, mehrere kleine Operationen auszuführen.",
+       "transaction-duration-limit-exceeded": "Um eine große Verzögerung in der Datenreplikation zu vermeiden, wurde diese Transaktion abgebrochen. Die Schreibdauer ($1) hat die Grenze von {{PLURAL:$2|einer Sekunde|$2 Sekunden}} überschritten. Falls du viele Objekte auf einmal änderst, versuche stattdessen, die Änderungen auf mehrere Operationen aufzuteilen.",
        "laggedslavemode": "<strong>Achtung:</strong> Die angezeigte Seite könnte unter Umständen nicht die letzten Bearbeitungen enthalten.",
        "readonly": "Datenbank gesperrt",
        "enterlockreason": "Bitte gib einen Grund ein, warum die Datenbank gesperrt werden soll und eine Abschätzung über die Dauer der Sperrung",
        "right-override-export-depth": "Exportiere Seiten einschließlich verlinkter Seiten bis zu einer Tiefe von 5",
        "right-sendemail": "E-Mails an andere Benutzer senden",
        "right-passwordreset": "Passwort eines Benutzers zurücksetzen und die dazu verschickte E-Mail einsehen",
-       "right-managechangetags": "[[Special:Tags|Markierungen]] erstellen und aus der Datenbank löschen",
+       "right-managechangetags": "[[Special:Tags|Markierungen]] erstellen und (de)aktivieren",
        "right-applychangetags": "[[Special:Tags|Markierungen]] zusammen mit den Änderungen anwenden",
        "right-changetags": "Beliebige [[Special:Tags|Markierungen]] zu einzelnen Versionen und Logbucheinträgen hinzufügen und entfernen",
+       "right-deletechangetags": "[[Special:Tags|Markierungen]] aus der Datenbank löschen",
        "grant-generic": "Rechtegruppe „$1“",
        "grant-group-page-interaction": "Mit Seiten interagieren",
        "grant-group-file-interaction": "Mit Medien interagieren",
        "action-viewmyprivateinfo": "deine privaten Informationen einzusehen",
        "action-editmyprivateinfo": "deine privaten Informationen zu bearbeiten",
        "action-editcontentmodel": "das Inhaltsmodell einer Seite zu bearbeiten",
-       "action-managechangetags": "Markierungen zu erstellen und aus der Datenbank zu löschen",
+       "action-managechangetags": "Markierungen zu erstellen und zu (de)aktivieren",
        "action-applychangetags": "Markierungen zusammen mit deinen Änderungen anzuwenden",
        "action-changetags": "beliebige Markierungen zu einzelnen Versionen und Logbucheinträgen hinzuzufügen und zu entfernen",
+       "action-deletechangetags": "Markierungen aus der Datenbank zu löschen",
        "nchanges": "$1 {{PLURAL:$1|Änderung|Änderungen}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|seit dem letzten Besuch}}",
        "enhancedrc-history": "Versionsgeschichte",
        "tags-delete-not-found": "Die Markierung „$1“ ist nicht vorhanden.",
        "tags-delete-too-many-uses": "Die Markierung „$1“ wird bei mehr als {{PLURAL:$2|einer Version|$2 Versionen}} verwendet und kann deshalb nicht gelöscht werden.",
        "tags-delete-warnings-after-delete": "Die Markierung „$1“ wurde gelöscht, aber die {{PLURAL:$2|folgende Warnung ist|folgenden Warnungen sind}} aufgetreten:",
+       "tags-delete-no-permission": "Du hast keine Berechtigung, Änderungsmarkierungen zu löschen.",
        "tags-activate-title": "Markierung aktivieren",
        "tags-activate-question": "Du bist dabei, die Markierung „$1“ zu aktivieren.",
        "tags-activate-reason": "Grund:",
index e7fa4c7..08d95b9 100644 (file)
        "image_tip": "Embedded file",
        "media_sample": "Example.ogg",
        "media_tip": "File link",
+       "sig-text": "--$1",
        "sig_tip": "Your signature with timestamp",
        "hr_tip": "Horizontal line (use sparingly)",
        "summary": "Summary:",
        "right-override-export-depth": "Export pages including linked pages up to a depth of 5",
        "right-sendemail": "Send email to other users",
        "right-passwordreset": "View password reset emails",
-       "right-managechangetags": "Create and delete [[Special:Tags|tags]] from the database",
+       "right-managechangetags": "Create and (de)activate [[Special:Tags|tags]]",
        "right-applychangetags": "Apply [[Special:Tags|tags]] along with one's changes",
        "right-changetags": "Add and remove arbitrary [[Special:Tags|tags]] on individual revisions and log entries",
+       "right-deletechangetags": "Delete [[Special:Tags|tags]] from the database",
        "grant-generic": "\"$1\" rights bundle",
        "grant-group-page-interaction": "Interact with pages",
        "grant-group-file-interaction": "Interact with media",
        "action-viewmyprivateinfo": "view your private information",
        "action-editmyprivateinfo": "edit your private information",
        "action-editcontentmodel": "edit the content model of a page",
-       "action-managechangetags": "create and delete tags from the database",
+       "action-managechangetags": "create and (de)activate tags",
        "action-applychangetags": "apply tags along with your changes",
        "action-changetags": "add and remove arbitrary tags on individual revisions and log entries",
+       "action-deletechangetags": "delete tags from the database",
        "nchanges": "$1 {{PLURAL:$1|change|changes}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|since last visit}}",
        "enhancedrc-history": "history",
        "timezone-local": "Local",
        "duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",
        "duplicate-displaytitle": "<strong>Warning:</strong> Display title \"$2\" overrides earlier display title \"$1\".",
+       "restricted-displaytitle": "<strong>Warning:</strong> Display title \"$1\" was ignored since it is not equivalent to the page's actual title.",
        "invalid-indicator-name": "<strong>Error:</strong> Page status indicators' <code>name</code> attribute must not be empty.",
        "version": "Version",
        "version-summary": "",
        "tags-delete-not-found": "The tag \"$1\" does not exist.",
        "tags-delete-too-many-uses": "The tag \"$1\" is applied to more than $2 {{PLURAL:$2|revision|revisions}}, which means it cannot be deleted.",
        "tags-delete-warnings-after-delete": "The tag \"$1\" was deleted, but the following {{PLURAL:$2|warning was|warnings were}} encountered:",
+       "tags-delete-no-permission": "You do not have permission to delete change tags.",
        "tags-activate-title": "Activate tag",
        "tags-activate-question": "You are about to activate the tag \"$1\".",
        "tags-activate-reason": "Reason:",
index 89834e6..c32549a 100644 (file)
        "right-override-export-depth": "Viedä sivuja sisältäen viitatut sivut viiden syvyydellä",
        "right-sendemail": "Lähettää sähköpostia muille käyttäjille",
        "right-passwordreset": "Tarkastella salasanan alustusviestejä",
-       "right-managechangetags": "Luoda ja poistaa [[Special:Tags|merkkauksia]] tietokannasta",
+       "right-managechangetags": "Luoda ja ottaa käyttöön [[Special:Tags|merkkauksia]]",
        "right-applychangetags": "Asettaa [[Special:Tags|merkkauksia]] omien muutosten yhteyteen",
        "right-changetags": "Lisätä ja poistaa satunnaisia [[Special:Tags|merkkauksia]] yksittäisissä sivuversioissa tai lokimerkinnöissä",
+       "right-deletechangetags": "Poistaa [[Special:Tags|merkkauksia]] tietokannasta",
        "grant-generic": "\"$1\" oikeuksien joukko",
        "grant-group-page-interaction": "Ole vuorovaikutuksessa sivujen kanssa",
        "grant-group-file-interaction": "Ole vuorovaikutuksessa mediatiedostojen kanssa",
        "action-viewmyprivateinfo": "katsoa omia yksityisiä tietojasi",
        "action-editmyprivateinfo": "muokata omia yksityisiä tietojasi",
        "action-editcontentmodel": "muokata sivun sisältömallia",
-       "action-managechangetags": "luoda ja poistaa merkkauksia tietokannasta",
+       "action-managechangetags": "luoda ja ottaa käyttöön merkkauksia",
        "action-applychangetags": "käyttää merkkauksia muutostesi yhteydessä",
        "action-changetags": "lisätä ja poistaa satunnaisia merkkauksia yksittäisissä sivuversioissa ja lokimerkinnöissä",
+       "action-deletechangetags": "poistaa merkkauksia tietokannasta",
        "nchanges": "$1 {{PLURAL:$1|muutos|muutosta}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|viimeisen käynnin jälkeen}}",
        "enhancedrc-history": "historia",
        "changecontentmodel-success-text": "Sisältötyyppiä kohteessa [[:$1]] on muutettu.",
        "changecontentmodel-cannot-convert": "Sisältöä sivulla [[:$1]] ei voida muuntaa tyypiksi $2.",
        "changecontentmodel-nodirectediting": "Sisältömalli $1 ei tue suoraa muokkaamista",
+       "changecontentmodel-emptymodels-title": "Mitään sisältömallia ei ole saatavilla",
+       "changecontentmodel-emptymodels-text": "Sisältöä sivulla [[:$1]] ei voida muuntaa mihinkään muotoon.",
        "log-name-contentmodel": "Sisältömallin muutosloki",
        "log-description-contentmodel": "Tapahtumat, jotka liittyvät sivun sisältömalleihin",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|loi}} sivun $3 käyttäen normaalista poikkeavaa sisältömallia \"$5\"",
        "whatlinkshere-prev": "← {{PLURAL:$1|edellinen sivu|$1 edellistä sivua}}",
        "whatlinkshere-next": "{{PLURAL:$1|seuraava sivu|$1 seuraavaa sivua}} →",
        "whatlinkshere-links": "viittaukset",
-       "whatlinkshere-hideredirs": "$1 ohjaukset",
-       "whatlinkshere-hidetrans": "$1 sisällytykset",
-       "whatlinkshere-hidelinks": "$1 linkit",
-       "whatlinkshere-hideimages": "$1 tiedostolinkit",
+       "whatlinkshere-hideredirs": "Piilota ohjaukset",
+       "whatlinkshere-hidetrans": "Piilota sisällytykset",
+       "whatlinkshere-hidelinks": "Piilota linkit",
+       "whatlinkshere-hideimages": "Piilota tiedostolinkit",
        "whatlinkshere-filters": "Suotimet",
        "whatlinkshere-submit": "Siirry",
        "autoblockid": "Automaattinen esto #$1",
        "lockdbsuccesstext": "Tietokanta on lukittu.<br />\nMuista [[Special:UnlockDB|poistaa tietokannan lukitus]] kun huolto on tehty.",
        "unlockdbsuccesstext": "Tietokannan lukitus on poistettu.",
        "lockfilenotwritable": "Tietokannan lukitustiedostoa ei voi kirjoittaa. Tarkista oikeudet.",
+       "databaselocked": "Tietokanta on jo lukittu.",
        "databasenotlocked": "Tietokantaa ei ole lukittu.",
        "lockedbyandtime": "(lukinnut {{GENDER:$1|$1}} $2 kello $3)",
        "move-page": "Siirrä $1",
        "tags-delete-not-found": "Merkkausta \"$1\" ei ole olemassa.",
        "tags-delete-too-many-uses": "Tämä merkkaus \"$1\" on käytössä useammassa kuin $2 sivuversiossa, joten sitä ei voi poistaa.",
        "tags-delete-warnings-after-delete": "Merkkaus \"$1\" poistettiin, mutta toimenpide sai aikaan {{PLURAL:$2|seuraavan varoituksen|seuraavat varoitukset}}:",
+       "tags-delete-no-permission": "Sinulla ei ole oikeutta poistaa muutoksien yhteydessä olevia merkkauksia.",
        "tags-activate-title": "Aktivoi merkkaus",
        "tags-activate-question": "Olet nyt aktivoimassa merkkausta \"$1\".",
        "tags-activate-reason": "Syy:",
        "feedback-useragent": "User agent:",
        "searchsuggest-search": "Hae",
        "searchsuggest-containing": "sisältää...",
+       "api-error-autoblocked": "Sinun IP-osoitteesi on estetty automaattisesti, koska sitä on käyttänyt estetty käyttäjätunnus.",
        "api-error-badaccess-groups": "Sinulla ei ole oikeutta tallentaa tiedostoja tähän wikiin.",
        "api-error-badtoken": "Sisäinen virhe: virheellinen tarkistussumma.",
+       "api-error-blocked": "Sinut on estetty muokkaamasta.",
        "api-error-copyuploaddisabled": "Tallentaminen URL-osoitteesta ei ole käytössä.",
        "api-error-duplicate": "Samansisältöisiä tiedostoja löytyi {{PLURAL:$1|yksi kappale|useampia kappaleita}}.",
        "api-error-duplicate-archive": "Sivustolla oli aiemmin {{PLURAL:$1|toinen samansisältöinen tiedosto|toisia samansisältöisiä tiedostoja}}, mutta {{PLURAL:$1|se|ne}} poistettiin.",
index 672b071..74c126c 100644 (file)
        "right-override-export-depth": "Exporter les pages en incluant les pages liées jusqu'à une profondeur de 5 niveaux",
        "right-sendemail": "Envoyer un courriel aux autres utilisateurs",
        "right-passwordreset": "Voir les courriels de réinitialisation des mots de passe",
-       "right-managechangetags": "Créer et supprimer des [[Spécial:Balises|balises]] de la base de données",
+       "right-managechangetags": "Créer et (dés)activer des [[Special:Tags|balises]]",
        "right-applychangetags": "Appliquer [[Special:Tags|les balises]] avec ses propres modifications",
        "right-changetags": "Ajouter et supprimer de façon arbitraire [[Special:Tags|des balises]] sur des révisions individuelles et des entrées de journal",
+       "right-deletechangetags": "Supprimer des [[Special:Tags|balises]] de la base de données",
        "grant-generic": "ensemble de droits « $1 »",
        "grant-group-page-interaction": "Interagir avec des pages",
        "grant-group-file-interaction": "Interagir avec des médias",
        "action-viewmyprivateinfo": "voir vos informations personnelles",
        "action-editmyprivateinfo": "modifier vos informations personnelles",
        "action-editcontentmodel": "modifier le modèle de contenu d’une page",
-       "action-managechangetags": "créer et supprimer des balises de la base de données",
+       "action-managechangetags": "créer et (dés)activer des balises",
        "action-applychangetags": "appliquer les balises avec vos modifications",
        "action-changetags": "ajouter et supprimer de façon arbitraire des balises sur des révisions individuelles et des entrées de journal",
+       "action-deletechangetags": "supprimer des balises de la base de données",
        "nchanges": "$1 modification{{PLURAL:$1||s}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|depuis la dernière visite}}",
        "enhancedrc-history": "historique",
        "tags-delete-not-found": "La balise « $1 » n’existe pas.",
        "tags-delete-too-many-uses": "La balise « $1 » est appliquée à plus de $2 {{PLURAL:$2|révision|révisions}}, ce qui signifie qu'elle ne peut pas être supprimée.",
        "tags-delete-warnings-after-delete": "La balise « $1 » a été supprimée, mais {{PLURAL:$2|l’avertissement suivant a|les avertissements suivants ont}} été rencontré{{PLURAL:$2||s}} :",
+       "tags-delete-no-permission": "Vous n’avez pas le droit de supprimer des balises de changement.",
        "tags-activate-title": "Activer la balise",
        "tags-activate-question": "Vous êtes sur le point d'activer la balise « $1 ».",
        "tags-activate-reason": "Motif :",
index ba7e524..839df37 100644 (file)
        "right-override-export-depth": "Exportar páxinas incluíndo as páxinas ligadas ata unha profundidade de 5",
        "right-sendemail": "Enviar correos electrónicos a outros usuarios",
        "right-passwordreset": "Ver os correos electrónicos de restablecemento de contrasinais",
-       "right-managechangetags": "Crear e borrar [[Special:Tags|tags]] da base de datos",
+       "right-managechangetags": "Crear e (des)activar [[Special:Tags|tags]]",
        "right-applychangetags": "Aplicar [[Special:Tags|etiquetas]] xunto cos cambios propios",
        "right-changetags": "Engadir e quitar [[Special:Tags|etiquetas]] arbitrarias a revisións individuais e entradas do rexistro",
+       "right-deletechangetags": "Suprimir as [[Special:Tags|etiquetas]] da base de datos",
        "grant-generic": "conxunto de dereitos \"$1\"",
        "grant-group-page-interaction": "Interactuar con páxinas",
        "grant-group-file-interaction": "Interactuar con ficheiros multimedia",
        "action-viewmyprivateinfo": "ver a súa información privada",
        "action-editmyprivateinfo": "editar a súa información privada",
        "action-editcontentmodel": "editar o modelo de contido dunha páxina",
-       "action-managechangetags": "crear e borrar etiquetas da base de datos",
+       "action-managechangetags": "crear e (des)activar etiquetas",
        "action-applychangetags": "aplicar etiquetas xunto cos cambios",
        "action-changetags": "engadir e quitar etiquetas arbitrarias a revisións individuais e entradas do rexistro",
+       "action-deletechangetags": "borrar etiquetas da base de datos",
        "nchanges": "$1 {{PLURAL:$1|modificación|modificacións}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde a última visita}}",
        "enhancedrc-history": "historial",
        "tags-delete-not-found": "A páxina \"$1\" non existe.",
        "tags-delete-too-many-uses": "A etiqueta \"$1\" aplícase a máis de $2 {{PLURAL:$2|revisión|revisións}}; isto significa que non se pode borrar.",
        "tags-delete-warnings-after-delete": "A etiqueta \"$1\" borrouse; con todo, {{PLURAL:$2|atopouse o seguinte aviso|atopáronse os seguintes avisos}}:",
+       "tags-delete-no-permission": "Non ten permisos para borrar etiquetas de cambio.",
        "tags-activate-title": "Activar unha etiqueta",
        "tags-activate-question": "Está a piques de activar a etiqueta\"$1\".",
        "tags-activate-reason": "Motivo:",
index 2377488..5cce726 100644 (file)
        "category_header": "דפים בקטגוריה \"$1\"",
        "subcategories": "קטגוריות משנה",
        "category-media-header": "קובצי מדיה בקטגוריה \"$1\"",
-       "category-empty": "<em>קטגוריה זו אינה כוללת דפים או קובצי מדיה.</em>",
+       "category-empty": "<strong>קטגוריה זו אינה כוללת דפים או קובצי מדיה.</strong>",
        "hidden-categories": "{{PLURAL:$1|קטגוריה מוסתרת|קטגוריות מוסתרות}}",
        "hidden-category-category": "קטגוריות מוסתרות",
        "category-subcat-count": "{{PLURAL:$2|קטגוריה זו כוללת את קטגוריית המשנה הבאה בלבד.|קטגוריה זו כוללת את {{PLURAL:$1|קטגוריית המשנה המוצגת להלן|$1 קטגוריות המשנה המוצגות להלן}}, וכוללת בסך־הכול $2 קטגוריות משנה.}}",
        "editingsection": "עריכת הדף $1 (פסקה)",
        "editingcomment": "עריכת הדף $1 (פסקה חדשה)",
        "editconflict": "התנגשות עריכה: $1",
-       "explainconflict": "×\9eשת×\9eש ×\90×\97ר ×©×\99× ×\94 ×\90ת ×\94×\93×£ ×\9e×\90×\96 ×©×\94ת×\97×\9cת×\9d ×\9cער×\95×\9a ×\90×\95ת×\95.\n×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f ×\9e×\9b×\99×\9c ×\90ת ×\94×\98קס×\98 ×\91×\93×£ ×\9bפ×\99 ×©×\94×\95×\90 ×¢×ª×\94.\n×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9e×\95צ×\92×\99×\9d ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94ת×\97ת×\95×\9f.\n×¢×\9c×\99×\9b×\9d ×\9c×\9e×\96×\92 ×\90ת ×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9cת×\95×\9a ×\94×\98קס×\98 ×\94ק×\99×\99×\9d.\n'''רק''' הטקסט בחלון העריכה העליון יישמר כשתלחצו על \"{{int:savearticle}}\".",
+       "explainconflict": "×\9eשת×\9eש ×\90×\97ר ×©×\99× ×\94 ×\90ת ×\94×\93×£ ×\9e×\90×\96 ×©×\94ת×\97×\9cת×\9d ×\9cער×\95×\9a ×\90×\95ת×\95.\n×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94×¢×\9c×\99×\95×\9f ×\9eצ×\99×\92 ×\90ת ×\94×\98קס×\98 ×\91×\93×£ ×\9bפ×\99 ×©×\94×\95×\90 ×\9bר×\92×¢.\n×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9e×\95צ×\92×\99×\9d ×\91×\97×\9c×\95×\9f ×\94ער×\99×\9b×\94 ×\94ת×\97ת×\95×\9f.\n×¢×\9c×\99×\9b×\9d ×\9c×\9e×\96×\92 ×\90ת ×\94ש×\99× ×\95×\99×\99×\9d ×©×\9c×\9b×\9d ×\9cת×\95×\9a ×\94×\98קס×\98 ×\94ק×\99×\99×\9d.\n<strong>רק</strong> הטקסט בחלון העריכה העליון יישמר כשתלחצו על \"{{int:savearticle}}\".",
        "yourtext": "הטקסט שלך",
        "storedversion": "גרסה שמורה",
        "nonunicodebrowser": "'''אזהרה: הדפדפן שלך אינו תואם לתקן יוניקוד.'''\nכדי למנוע בעיות הנוצרות כתוצאה מכך ולאפשר לך לערוך דפים בבטחה, תווים שאינם ב־ASCII יוצגו בתיבת העריכה כקודים הקסדצימליים.",
-       "editingold": "'''אזהרה: אתם עורכים גרסה לא עדכנית של דף זה.'''\nאם תשמרו את הדף, כל השינויים שנעשו מאז גרסה זו יאבדו.",
+       "editingold": "<strong>אזהרה: אתם עורכים גרסה לא עדכנית של דף זה.</strong>\nאם תשמרו את הדף, כל השינויים שנעשו מאז גרסה זו יאבדו.",
        "yourdiff": "הבדלים",
        "copyrightwarning": "'''שימו לב:''' תרומתכם ל{{grammar:תחילית|{{SITENAME}}}} תפורסם תחת תנאי הרישיון $2 (ראו $1 לפרטים נוספים). אם אינכם רוצים שעבודתכם תהיה זמינה לעריכה על־ידי אחרים, שתופץ לעיני כול, ושאחרים יוכלו להעתיק ממנה בציון המקור – אל תפרסמו אותה פה. כמו־כן, אתם מבטיחים לנו כי כתבתם את הטקסט הזה בעצמכם, או העתקתם אותו ממקור שאינו מוגן בזכויות יוצרים. '''אל תעשו שימוש בחומר המוגן בזכויות יוצרים ללא רשות!'''",
        "copyrightwarning2": "'''שימו לב:''' תורמים אחרים עשויים לערוך או אף להסיר את תרומתכם ל{{grammar:תחילית|{{SITENAME}}}}. אם אינכם רוצים שעבודתכם תהיה זמינה לעריכה על־ידי אחרים, אל תפרסמו אותה פה. כמו־כן, אתם מבטיחים לנו כי כתבתם את הטקסט הזה בעצמכם, או העתקתם אותו ממקור שאינו מוגן בזכויות יוצרים (ראו $1 לפרטים נוספים). '''אל תעשו שימוש בחומר המוגן בזכויות יוצרים ללא רשות!'''",
        "right-override-export-depth": "ייצוא דפים כולל דפים מקושרים עד עומק של חמישה",
        "right-sendemail": "שליחת דואר אלקטרוני למשתמשים אחרים",
        "right-passwordreset": "צפייה בדואר אלקטרוני של איפוס סיסמה",
-       "right-managechangetags": "×\99צ×\99רת ×\95×\9e×\97×\99קת [[Special:Tags|ת×\92×\99×\95ת]] ×\9e×\91ס×\99ס ×\94נת×\95× ×\99×\9d",
+       "right-managechangetags": "×\99צ×\99ר×\94, ×\94פע×\9c×\94 ×\95×\91×\99×\98×\95×\9c ×©×\9c [[Special:Tags|ת×\92×\99×\95ת]]",
        "right-applychangetags": "החלת [[Special:Tags|תגיות]] יחד עם שינויים",
        "right-changetags": "הוספת והסרה של [[Special:Tags|תגיות]] כלשהן לגרסאות מסוימות ולרשומות יומן",
+       "right-deletechangetags": "מחיקת [[Special:Tags|תגיות]] מבסיס הנתונים",
        "grant-generic": "חבילת ההרשאות \"$1\"",
        "grant-group-page-interaction": "אינטראקציה עם דפים",
        "grant-group-file-interaction": "אינטראקציה עם קבצים",
        "action-viewmyprivateinfo": "לצפות במידע הפרטי שלך",
        "action-editmyprivateinfo": "לערוך את המידע הפרטי שלך",
        "action-editcontentmodel": "לערוך את מודל התוכן של דף",
-       "action-managechangetags": "ליצור ולמחוק תגיות מבסיס הנתונים",
+       "action-managechangetags": "ליצור, להפעיל ולבטל תגיות",
        "action-applychangetags": "להחיל תגיות יחד עם השינויים שלכם",
        "action-changetags": "להוסיף ולהסיר תגיות כלשהן לגרסאות מסוימות ולרשומות יומן",
+       "action-deletechangetags": "למחוק תגיות מבסיס הנתונים",
        "nchanges": "{{PLURAL:$1|שינוי אחד|$1 שינויים}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|מאז ביקורך האחרון}}",
        "enhancedrc-history": "היסטוריה",
        "recentchanges-label-unpatrolled": "עריכה זו טרם נבדקה",
        "recentchanges-label-plusminus": "גודל הדף השתנה במספר זה של בתים",
        "recentchanges-legend-heading": "<strong>מקרא:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ראו גם [[Special:NewPages|רשימת דפים חדשים]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ראו גם את [[Special:NewPages|רשימת הדפים החדשים]])",
        "recentchanges-legend-plusminus": "(''±123'')",
        "recentchanges-submit": "הצגה",
        "rcnotefrom": "להלן {{PLURAL:$5|השינוי שבוצע|השינויים שבוצעו}} מאז <strong>$3, $4</strong> (מוצגים עד <strong>$1</strong>).",
        "tags-delete-not-found": "התגית \"$1\" אינה קיימת.",
        "tags-delete-too-many-uses": "התגית \"$1\" מוחלת על יותר {{PLURAL:$2|מגרסה אחת|מ־$2 גרסאות}}, ולכן לא ניתן למחוק אותה.",
        "tags-delete-warnings-after-delete": "התגית \"$1\" נמחקה, אבל {{PLURAL:$2|התקבלה האזהרה הבאה|התקבלו האזהרות הבאות}}:",
+       "tags-delete-no-permission": "אין לך הרשאה למחוק תגיות שינויים.",
        "tags-activate-title": "הפעלת תגית",
        "tags-activate-question": "אתם עומדים להפעיל את התגית \"$1\".",
        "tags-activate-reason": "הסבר:",
index 140f56a..3d9cd3b 100644 (file)
        "otherlanguages": "Nan lòt lang yo",
        "redirectedfrom": "(Redirije depi $1)",
        "redirectpagesub": "Paj pou redireksyon",
-       "redirectto": "Voye sou:",
+       "redirectto": "Redireksyon sou&nbsp;:",
        "lastmodifiedat": "Paj sa te modifye pou dènye fwa $1 a $2.<br />",
        "viewcount": "Paj sa te konsilte {{PLURAL:$1|yon fwa|$1 fwa}}.",
        "protectedpage": "Paj pwoteje",
        "whatlinkshere-page": "Paj :",
        "linkshere": "Paj yo ki anba ap mene nan <b>[[:$1]]</b> :",
        "nolinkshere": "Pyès paj genyen lyen pou paj sa a <b>[[:$1]]</b>.",
-       "isredirect": "Paj redireksyon",
+       "isredirect": "paj redireksyon",
        "istemplate": "anndan",
        "isimage": "lyen fichye a",
        "whatlinkshere-prev": "{{PLURAL:$1|presedan|$1 presedan yo}}",
index a58b482..13d38a0 100644 (file)
        "feedback-bugornote": "Jika Anda sudah siap untuk mendeskripsikan masalah teknis secara rinci silakan [$1 melaporkan bug].\nJika tidak, Anda dapat menggunakan formulir mudah di bawah ini. Komentar Anda akan ditambahkan ke halaman \"[$3 $2]\", bersama dengan nama pengguna Anda dan apa browser yang Anda gunakan.",
        "feedback-cancel": "Batal",
        "feedback-close": "Selesai",
+       "feedback-dialog-title": "Kirimkan saran dan tanggapan",
+       "feedback-dialog-intro": "Anda bisa menggunakan formulir sederhana di bawah untuk mengirimkan saran dan masukan. Komentar Anda akan ditambahkan pada laman \"$1\" bersama nama pengguna Anda.",
        "feedback-error-title": "Kesalahan",
        "feedback-error1": "Galat: Hasil tidak dikenal dari API",
        "feedback-error2": "Galat: Penyuntingan gagal",
index 7cac47b..71f985c 100644 (file)
        "watchlistfor2": "$1 $2 царна",
        "addedwatchtext": "\"[[:$1]]\" оагIув, шун [[Special:Watchlist|теркама дагаршкахь]] чуяккха я. \nТехьара мел йола укх оагIувни хувцамаш цу дагаршкахь хоам беш хургья. Вешта [[Special:RecentChanges|керда хувцама дагаршкаехь]] сома къоалмаца хьакъоастлуш хургья.",
        "removedwatchtext": "\"[[:$1]]\" оагIув, шун [[Special:Watchlist|теркама дарагчера]] дIаяккха хиннай.",
-       "watch": "Зе",
+       "watch": "Зем бе",
        "watchthispage": "Укх оагIува теркам бе",
        "unwatch": "Лора ма де",
        "watchlist-details": "Шун теркама дагарченгахь йола  $1 {{PLURAL:$1|1=оагIув|оагIувнаш}}, дувцама оагIувнаш ца лоархIаш.",
        "tooltip-ca-protect": "Eр оагIув лорае",
        "tooltip-ca-delete": "Ер оагIув дIаяькха",
        "tooltip-ca-move": "Укх оагIон цIи хувца",
-       "tooltip-ca-watch": "Ер оагIув хьай теркам беча каьхата тIа тIаяьккха",
+       "tooltip-ca-watch": "Ер оагIув Iайха зувш йолча оагIонашта юкъеяккха",
        "tooltip-ca-unwatch": "Ер оагIув теркам беча каьхата тIара дIаяькха",
        "tooltip-search": "Хьалáха {{grammar:prepositional|{{SITENAME}}}} чу",
        "tooltip-search-go": "Изза мо цӀи йолаш оагӀув тӀa дехьавала",
        "tooltip-ca-nstab-special": "Ер гIулакха оагIув я, из хувца бокъо яц",
        "tooltip-ca-nstab-project": "Проектан оагIув",
        "tooltip-ca-nstab-image": "Файлан оагӀув",
-       "tooltip-ca-nstab-template": "Ð\9bон оагIув",
+       "tooltip-ca-nstab-template": "Ð\9bеÑ\80а оагIув",
        "tooltip-ca-nstab-help": "ГӀон оагIув",
        "tooltip-ca-nstab-category": "Категорий оагӀув",
        "tooltip-minoredit": "Ер хувцар башха доаца санна белгалде",
        "tooltip-compareselectedversions": "Укх оагIувни шин доржамаш тIа юкъера хувцамаш зе.",
        "tooltip-watch": "Ер оагIув теркам беча каьхата тIа яькха",
        "tooltip-rollback": "Цкъа пIелг тоIабе дIадаккха тIехьара редакторас даь хувцамаш",
-       "tooltip-undo": "Даь хувцар дIадаьккха, хьалххе хьажар хьахьокха, дIадаккхара бахьан Iочуязаде аьттув болаш.",
+       "tooltip-undo": "Даь хувцар дIадаьккха, хьалххе бIаргтохар хьахьокха, дIадаккхара бахьан Iочуязде аьттув болаш.",
        "tooltip-summary": "Лоаца йоазонца сурт оттадар Iочуязде",
        "pageinfo-hidden-categories": "{{PLURAL:$1|1=Къайла категори|Къайла категореш}} ($1)",
        "pageinfo-toolboxlink": "ОагIонах бола хоам",
index 14da229..9f6beb9 100644 (file)
        "right-override-export-depth": "Esporta le pagine includendo le pagine collegate fino ad una profondità di 5",
        "right-sendemail": "Invia email ad altri utenti",
        "right-passwordreset": "Vede i messaggi di reimpostazione della password",
-       "right-managechangetags": "Crea ed elimina dal database i [[Special:Tags|tag]]",
+       "right-managechangetags": "Crea e attiva/disattiva le [[Special:Tags|etichette]]",
        "right-applychangetags": "Applica delle [[Special:Tags|etichette]] alle proprie modifiche",
        "right-changetags": "Aggiunge e rimuove specifiche [[Special:Tags|etichette]] su singole versioni o voci di registro",
+       "right-deletechangetags": "Cancella le [[Special:Tags|etichette]] dal database",
        "grant-generic": "Pacchetto diritti \"$1\"",
        "grant-group-page-interaction": "Interagisce con le pagine",
        "grant-group-file-interaction": "Interagisce con i file multimediali",
        "action-viewmyprivateinfo": "vedere i propri dati personali",
        "action-editmyprivateinfo": "modificare i propri dati personali",
        "action-editcontentmodel": "modificare il modello di contenuto di una pagina",
-       "action-managechangetags": "crea ed elimina i tag dal database",
+       "action-managechangetags": "creare e attivare/disattivare le etichette",
        "action-applychangetags": "applicare delle etichette alle tue modifiche",
        "action-changetags": "aggiungere o rimuovere specifiche etichette su singole versioni o voci di registro",
+       "action-deletechangetags": "cancellare le etichette dal database",
        "nchanges": "$1 {{PLURAL:$1|modifica|modifiche}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|dall'ultima visita}}",
        "enhancedrc-history": "cronologia",
        "tags-activate": "attiva",
        "tags-deactivate": "disattiva",
        "tags-hitcount": "$1 {{PLURAL:$1|modifica|modifiche}}",
-       "tags-manage-no-permission": "Non hai il permesso di gestire il cambiamento tag.",
+       "tags-manage-no-permission": "Non si dispone dei permessi necessari per gestire le etichette di modifica.",
        "tags-manage-blocked": "Non puoi gestire le etichette alle modifiche mentre sei bloccato.",
        "tags-create-heading": "Crea un nuovo tag",
        "tags-create-explanation": "Per impostazione predefinita, i tag appena creati saranno disponibili per l'utilizzo di utenti e bot.",
        "tags-delete-not-found": "Il tag \"$1\" non esiste.",
        "tags-delete-too-many-uses": "Il tag \"$1\" è applicato a più di $2 {{PLURAL:$2|revisione|revisioni}}, il che significa che non può essere eliminato.",
        "tags-delete-warnings-after-delete": "L'etichetta \"$1\" è stata cancellata, ma fai attenzione {{PLURAL:$2|al seguente avviso|ai seguenti avvisi}}:",
+       "tags-delete-no-permission": "Non si dispone dei permessi necessari per cancellare le etichette di modifica.",
        "tags-activate-title": "Attiva tag",
        "tags-activate-question": "Stai per attivare il tag \"$1\".",
        "tags-activate-reason": "Motivo:",
        "tags-apply-blocked": "Non puoi applicare le etichette alle modifiche mentre sei bloccato.",
        "tags-apply-not-allowed-one": "L'etichetta \"$1\" non può essere applicata manualmente.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|La seguente etichetta non può essere applicata|Le seguenti etichette non possono essere applicate}}  manualmente: $1",
-       "tags-update-no-permission": "Non hai il permesso di aggiungere o rimuovere modifiche di tag dalle singole revisioni o voci di registro.",
+       "tags-update-no-permission": "Non si dispone dei permessi necessari per aggiungere o rimuovere le etichette di modifica dalle singole versioni o voci di registro.",
        "tags-update-blocked": "Non puoi aggiungere o rimuovere le etichette alle modifiche mentre sei bloccato.",
        "tags-update-add-not-allowed-one": "Il tag \"$1\" non può essere aggiunto manualmente.",
        "tags-update-add-not-allowed-multi": "{{PLURAL:$2|Il seguente tag non può essere aggiunto|I seguenti tag non possono essere aggiunti}} manualmente: $1",
        "api-error-nomodule": "Errore interno: non è stato impostato il modulo di caricamento.",
        "api-error-ok-but-empty": "Errore interno: nessuna risposta dal server.",
        "api-error-overwrite": "Sovrascrivere un file esistente non è consentito.",
+       "api-error-ratelimited": "Stai cercando di caricare più file in meno tempo di quanto questo wiki permette.\nRiprova tra pochi minuti.",
        "api-error-stashfailed": "Errore interno: il server non è riuscito a memorizzare il documento temporaneo.",
        "api-error-publishfailed": "Errore interno: il server non è riuscito a pubblicare il documento temporaneo.",
        "api-error-stasherror": "Si è verificato un errore durante il caricamento del file in stash.",
index 1b07ee9..9c32573 100644 (file)
@@ -69,7 +69,8 @@
                        "Sujiniku",
                        "Azeha",
                        "Kana Higashikawa",
-                       "Shield-9"
+                       "Shield-9",
+                       "Waiesu"
                ]
        },
        "tog-underline": "リンクの下線:",
        "whatlinkshere-prev": "前の$1件",
        "whatlinkshere-next": "次の$1件",
        "whatlinkshere-links": "← リンク",
-       "whatlinkshere-hideredirs": "転送ページを$1",
-       "whatlinkshere-hidetrans": "参照読み込みを$1",
-       "whatlinkshere-hidelinks": "リンクを$1",
-       "whatlinkshere-hideimages": "ファイルへのリンクを$1",
+       "whatlinkshere-hideredirs": "転送ページを非表示",
+       "whatlinkshere-hidetrans": "参照読み込みを非表示",
+       "whatlinkshere-hidelinks": "リンクを非表示",
+       "whatlinkshere-hideimages": "ファイルへのリンクを非表示",
        "whatlinkshere-filters": "絞り込み",
        "whatlinkshere-submit": "実行",
        "autoblockid": "自動ブロック #$1",
        "tags-delete-not-found": "タグ「$1」は存在しません。",
        "tags-delete-too-many-uses": "タグ「$1」は少なくとも$2版に付与されており、削除できません。",
        "tags-delete-warnings-after-delete": "タグ「$1」の削除しましたが、以下の{{PLURAL:$2|警告}}が発生しました:",
+       "tags-delete-no-permission": "変更タグを削除する権限がありません。",
        "tags-activate-title": "タグの有効化",
        "tags-activate-question": "タグ「$1」を有効化しようとしています。",
        "tags-activate-reason": "理由:",
index 8277a44..4114ce8 100644 (file)
        "actionthrottled": "Hejmara guherandinên hatine hesibandin",
        "actionthrottledtext": "Te ev tişt di demeke gelekî kin de kir. Ji kerema xwe çend xulekan bisekine û carekî din biceribîne.",
        "protectedpagetext": "Ev rûpel ji bo guhertin û karên din ne kirin hatiye parastin.",
-       "viewsourcetext": "Tu dikarî li çavkaniya vê rûpelê binêrî û wê kopî bikî:",
+       "viewsourcetext": "Tu dikarî li çavkaniya vê rûpelê binêrî û wê kopî bikî.",
        "viewyourtext": "Hûn çavkaniyê <strong>guhertinê xwe</strong> yê di vê rûpelê de dikarin bibînin û kopî bikin:",
        "protectedinterface": "Di vê rûpelê de nivîsandin ji bo navrû(interface)yî zimanan yê vê nivîsbariyê ye û ew tê parastin ku vandalîzm li vê derê çênebe.\nBo lêzêdekirin an jî guherandina wergerên bo hemû wîkiyan ji kerema xwe re mehelîkirina Mediawîkiyê [//translatewiki.net/ translatewiki.net]'ê bi kar bîne.",
        "editinginterface": "'''Hişyarî:''' Tu rûpelekê a ku di Wîkîpediya de ji bo sîstemê girîng e,  diguherînî. Guherandinên di vê rûpelê de wê ji aliyê hemû bikarhêneran ve werin dîtin. Ji bo wergerê ji kerema xwe di [//translatewiki.net/wiki/Main_Page?setlang=ku-latn translatewiki.net] de bixebite, projeya MediaWiki.",
        "newpassword": "Şîfreya nû",
        "retypenew": "Şîfreya nû careke din binîvîse",
        "resetpass_submit": "Şîfreyê pêkbîne û têkeve",
-       "changepassword-success": "Guhertine şîfreya te serkeftî bû!",
+       "changepassword-success": "Şîfreya te hate guhertandin!",
        "botpasswords-label-appid": "Navê bot:",
        "botpasswords-label-create": "Çêke",
        "botpasswords-label-update": "Rojane bike",
        "right-sendemail": "Ji bikarhênerên di re ename bişîne",
        "grant-editpage": "Rûpelên ku hene biguherîne",
        "grant-editprotected": "Rûpelên parastî bigûherîne",
+       "grant-basic": "Mafên bingehîn",
        "newuserlogpage": "Çêkirina hesabê nû",
        "newuserlogpagetext": "Ev têketina hesabên bikarhêneriyê ye ên ku nû hatine afirandin.",
        "rightslog": "Guhertina mafê bikarhêneriyê",
        "listusers-noresult": "Bikarhêner nehate dîtin.",
        "listusers-blocked": "(hate astengkirin)",
        "activeusers": "Lîsteya bikarhênerên çalak",
+       "activeusers-from": "Li bikarhênerên bi vê dest pê dikin bigere:",
        "activeusers-hidebots": "Bot'an veşêre",
        "activeusers-hidesysops": "Rêveberan veşêre",
        "activeusers-noresult": "Tu bikarhêner nehate dîtin.",
        "whatlinkshere-prev": "{{PLURAL:$1|yê|$1 yên}} berê",
        "whatlinkshere-next": "{{PLURAL:$1|yê|$1 yên}} din",
        "whatlinkshere-links": "← girêdan",
-       "whatlinkshere-hideredirs": "Beralîkirinan $1",
+       "whatlinkshere-hideredirs": "Beralîkirinan veşêre",
        "whatlinkshere-hidetrans": "Naverokan $1",
-       "whatlinkshere-hidelinks": "Girêdanan $1",
-       "whatlinkshere-hideimages": "Girêdanên wêneyan $1",
+       "whatlinkshere-hidelinks": "Girêdanan veşêre",
+       "whatlinkshere-hideimages": "Girêdanên wêneyan veşêre",
        "whatlinkshere-filters": "Parzûn",
        "block": "Bikarhêner asteng bike",
        "unblock": "Astengkirinê rake",
        "tooltip-feed-rss": "RSS feed'ên ji bo rûpelê",
        "tooltip-feed-atom": "Atom feed'ên ji bo vê rûpelê",
        "tooltip-t-contributions": "Lîsteyekî beşdariyên {{GENDER:$1|vê bikarhênerê}} bibîne",
-       "tooltip-t-emailuser": "Jê re name bişîne",
+       "tooltip-t-emailuser": "Jê {{GENDER:$1|vî bikarhênerî}}re peyamê bişîne",
        "tooltip-t-info": "Bêhtir agahî di derbarê vê rûpelê de",
        "tooltip-t-upload": "Dosyeyan bar bike",
        "tooltip-t-specialpages": "Lîsteya hemû rûpelên taybetî",
index 43656ec..d309c3c 100644 (file)
        "right-override-export-depth": "Eksport stron wraz z linkowanymi do głębokości 5 linków",
        "right-sendemail": "Wysyłanie e‐maili do innych użytkowników",
        "right-passwordreset": "Sprawdzanie treści e‐maila o resetowaniu hasła",
-       "right-managechangetags": "Tworzenie i usuwanie [[Special:Tags|znaczników]] z bazy danych",
+       "right-managechangetags": "Tworzenie i de(aktywowanie) [[Special:Tags|znaczników]]",
        "right-applychangetags": "Wprowadzanie [[Special:Tags|znaczników]] wraz z własnymi zmianami",
        "right-changetags": "Dodawanie i usuwanie dowolnych [[Special:Tags|znaczników]] z poszczególnych wersji i wpisów w rejestrze",
        "grant-group-page-interaction": "Interakcja ze stronami",
        "action-viewmyprivateinfo": "zobaczenia swoich prywatnych danych",
        "action-editmyprivateinfo": "edycji swoich prywatnych danych",
        "action-editcontentmodel": "edycji modelu zawartości strony",
-       "action-managechangetags": "utwórz lub usuń znaczniki z bazy danych",
+       "action-managechangetags": "tworzenia i de(aktywowania) znaczników",
        "action-applychangetags": "wprowadzania znaczników wraz z własnymi zmianami",
        "action-changetags": "dodawania i usuwania dowolnych znaczników z poszczególnych wersji i wpisów w rejestrze",
+       "action-deletechangetags": "usuwania znaczników z bazy danych",
        "nchanges": "$1 {{PLURAL:$1|zmiana|zmiany|zmian}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od ostatniej wizyty}}",
        "enhancedrc-history": "historia",
index 751bb82..44336ef 100644 (file)
                        "Robin van der Vliet",
                        "Conquistador",
                        "Frigory",
-                       "Psychoslave"
+                       "Psychoslave",
+                       "Guycn2"
                ]
        },
        "sidebar": "{{notranslate}}",
        "image_tip": "This is the text that appears when you hover the mouse over the sixth (middle) button on the edit toolbar.\n\n{{Identical|Embedded file}}",
        "media_sample": "{{optional}}\n{{Identical|Example}}",
        "media_tip": "This is the text that appears when you hover the mouse over the fifth button from the right in the edit toolbar.\n{{Identical|File link}}",
+       "sig-text": "{{notranslate}} This is the text that appears when you click on the signature button (second button from the right) on the edit toolbar. $1 will be replaced with four tildes (which cannot be included directly in the message for technical reasons).",
        "sig_tip": "This is the text that appears when you hover the mouse over the second key from the right on the edit toolbar.\n{{Identical|Signature with timestamp}}",
        "hr_tip": "This is the text that appears when you hover the mouse over the first button on the right on the edit toolbar.",
        "summary": "The Summary text beside the edit summary field\n\nSee also:\n* {{msg-mw|Subject}}\nSee also:\n* {{msg-mw|Accesskey-summary}}\n* {{msg-mw|Tooltip-summary}}\n{{Identical|Summary}}",
        "yourtext": "Used in Diff Preview page. The diff is between {{msg-mw|currentrev}} and {{msg-mw|yourtext}}.\n\nAlso used in Edit Conflict page; the diff between {{msg-mw|yourtext}} and {{msg-mw|storedversion}}.",
        "storedversion": "This is used in an edit conflict as the label for the top revision that has been stored, as opposed to your version {{msg-mw|yourtext}} that has not been stored which is shown at the bottom of the page.",
        "nonunicodebrowser": "Used as warning when editing page.",
-       "editingold": "Used as warning when editing page.",
+       "editingold": "Used as warning when editing an old revision of a page.",
        "yourdiff": "Used as h2 header for the diff of the current version of a page with the user's version in case there is an edit conflict or a spam filter hit.",
        "copyrightwarning": "Copyright warning displayed under the edit box in editor. Parameters:\n* $1 - link\n* $2 - license name",
        "copyrightwarning2": "Copyright warning displayed under the edit box in editor\n*$1 - license name",
        "right-managechangetags": "{{doc-right|managechangetags}}",
        "right-applychangetags": "{{doc-right|applychangetags}}",
        "right-changetags": "{{doc-right|changetags}}",
+       "right-deletechangetags": "{{doc-right|deletechangetags}}",
        "grant-generic": "Used if the grant name is not defined. Parameters:\n* $1 - grant name\n\nDefined grants (grant name refers: blockusers, createeditmovepage, ...):\n* {{msg-mw|grant-checkuser}}\n* {{msg-mw|grant-blockusers}}\n* {{msg-mw|grant-createaccount}}\n* {{msg-mw|grant-createeditmovepage}}\n* {{msg-mw|grant-delete}}\n* {{msg-mw|grant-editinterface}}\n* {{msg-mw|grant-editmycssjs}}\n* {{msg-mw|grant-editmyoptions}}\n* {{msg-mw|grant-editmywatchlist}}\n* {{msg-mw|grant-editpage}}\n* {{msg-mw|grant-editprotected}}\n* {{msg-mw|grant-highvolume}}\n* {{msg-mw|grant-oversight}}\n* {{msg-mw|grant-patrol}}\n* {{msg-mw|grant-protect}}\n* {{msg-mw|grant-rollback}}\n* {{msg-mw|grant-sendemail}}\n* {{msg-mw|grant-uploadeditmovefile}}\n* {{msg-mw|grant-uploadfile}}\n* {{msg-mw|grant-basic}}\n* {{msg-mw|grant-viewdeleted}}\n* {{msg-mw|grant-viewmywatchlist}}",
        "grant-group-page-interaction": "{{Related|grant-group}}",
        "grant-group-file-interaction": "{{Related|grant-group}}",
        "action-managechangetags": "{{doc-action|managechangetags}}",
        "action-applychangetags": "{{doc-action|applychangetags}}",
        "action-changetags": "{{doc-action|changetags}}",
+       "action-deletechangetags": "{{doc-action|deletechangetags}}",
        "nchanges": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to a diff of the changes.\n\nParameters:\n* $1 - the number of changes on that day (2 or more)\nThree messages are shown side-by-side: ({{msg-mw|Nchanges}} | {{msg-mw|Enhancedrc-since-last-visit}} | {{msg-mw|Enhancedrc-history}}).",
        "enhancedrc-since-last-visit": "Appears on enhanced watchlist and recent changes when page has more than one change on given date and at least one that the user hasn't seen yet, linking to a diff of the unviewed changes.\n\nParameters:\n* $1 - the number of unviewed changes (1 or more)\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).",
        "enhancedrc-history": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to its history.\n\nThis is the same as {{msg-mw|hist}}, but not abbreviated.\n\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).\n{{Identical|History}}",
        "tags-delete-not-found": "Error message on [[Special:Tags]]",
        "tags-delete-too-many-uses": "Error message on [[Special:Tags]]",
        "tags-delete-warnings-after-delete": "Warning shown after deleting a tag.\n\nParameters:\n* $1 - the code name of the tag that was deleted\n* $2 - the number of warnings",
+       "tags-delete-no-permission": "Error message on [[Special:Tags]]",
        "tags-activate-title": "The title of a page used to activate a tag. For more information on tags see [[mw:Manual:Tags|MediaWiki]].",
        "tags-activate-question": "An explanation to tell users what they are about to do.\n\nParameters:\n* $1 - the code name of the tag that is about to be activated",
        "tags-activate-reason": "{{Identical|Reason}}",
index bfd77ff..e5cf8c9 100644 (file)
        "right-override-export-depth": "экспортирование страниц, включая связанные страницы с глубиной до 5",
        "right-sendemail": "отправка электронной почты другим участникам",
        "right-passwordreset": "просмотр электронных писем с изменением пароля",
-       "right-managechangetags": "создание и удаление [[Special:Tags|меток]] из базы данных",
+       "right-managechangetags": "Создание и (де)активация [[Special:Tags|меток]]",
        "right-applychangetags": "применение [[Special:Tags|меток]] вместе со своими правками",
        "right-changetags": "добавление и удаление произвольных [[Special:Tags|меток]] на отдельных правках и записях в журнале",
+       "right-deletechangetags": "Удаление [[Special:Tags|меток]] из базы данных",
        "grant-generic": "Набор прав «$1»",
        "grant-group-page-interaction": "Взаимодействие со страницами",
        "grant-group-file-interaction": "Взаимодействие с медиафайлами",
        "action-viewmyprivateinfo": "просмотр вашей частной информации",
        "action-editmyprivateinfo": "редактирование вашей частной информации",
        "action-editcontentmodel": "редактирование контентной модели страницы",
-       "action-managechangetags": "создание и удаление меток из базы данных",
+       "action-managechangetags": "создание и (де)активацию меток",
        "action-applychangetags": " применять теги наряду с Вашими изменениями",
        "action-changetags": "Добавлять и удалять произвольные теги на отдельных изменениях и записях в журнале",
+       "action-deletechangetags": "удаление меток из базы данных",
        "nchanges": "$1 {{PLURAL:$1|изменение|изменения|изменений}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|с последнего посещения}}",
        "enhancedrc-history": "история",
        "whatlinkshere-prev": "{{PLURAL:$1|1=предыдущая|предыдущие}} $1",
        "whatlinkshere-next": "{{PLURAL:$1|1=следующая|следующие}} $1",
        "whatlinkshere-links": "← ссылки",
-       "whatlinkshere-hideredirs": "$1 перенаправления",
-       "whatlinkshere-hidetrans": "$1 включения",
-       "whatlinkshere-hidelinks": "$1 ссылки",
-       "whatlinkshere-hideimages": "$1 файловые ссылки",
+       "whatlinkshere-hideredirs": "Скрыть перенаправления",
+       "whatlinkshere-hidetrans": "Скрыть включения",
+       "whatlinkshere-hidelinks": "Скрыть ссылки",
+       "whatlinkshere-hideimages": "Скрыть файловые ссылки",
        "whatlinkshere-filters": "Фильтры",
        "whatlinkshere-submit": "Выполнить",
        "autoblockid": "Автоблокировка #$1",
        "tags-delete-not-found": "Метка «$1» не существует.",
        "tags-delete-too-many-uses": "Метка «$1» применяется в более чем $2 {{PLURAL:$2|версии|версиям}}, что означает, что она не может быть удалена.",
        "tags-delete-warnings-after-delete": "Метка «$1» была удалена, но {{PLURAL:$2|было обнаружено следующее предупреждение|были обнаружены следующие предупреждения}}:",
+       "tags-delete-no-permission": "У вас нет прав на удаление изменений меток.",
        "tags-activate-title": "Активировать метку",
        "tags-activate-question": "Вы собираетесь активировать метку «$1».",
        "tags-activate-reason": "Причина:",
index bc7a6fc..cc8a3c8 100644 (file)
        "right-override-export-depth": "Izvoz strani, vključno s povezaimi straneh do globine 5",
        "right-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
        "right-passwordreset": "Ogled e-pošt ponastavitve gesel",
-       "right-managechangetags": "Ustvarjanje in brisanje [[Special:Tags|oznak]] iz zbirke podatkov",
+       "right-managechangetags": "Ustvarjanje in (dez)aktivacijo [[Special:Tags|oznak]]",
        "right-applychangetags": "Uveljavitev [[Special:Tags|oznak]] skupaj s spremembami",
        "right-changetags": "Dodajanje in odstranjevanje poljubnih [[Special:Tags|oznak]] na posameznih redakcijah in dnevniških vnosih",
+       "right-deletechangetags": "Izbris [[Special:Tags|oznak]] iz zbirke podatkov",
        "grant-generic": "Snov pravic »$1«",
        "grant-group-page-interaction": "Interakcija s stranmi",
        "grant-group-file-interaction": "Interakcija s predstavnostjo",
        "action-viewmyprivateinfo": "ogled svojih zasebnih informacij",
        "action-editmyprivateinfo": "urejanje svojih zasebnih informacij",
        "action-editcontentmodel": "urejanje vsebinskega modela strani",
-       "action-managechangetags": "ustvarjanje in brisanje oznak iz zbirke podatkov",
+       "action-managechangetags": "ustvarjanje in (dez)aktivacijo oznak",
        "action-applychangetags": "uveljavitev oznak skupaj z vašimi spremembami",
        "action-changetags": "dodajanje in odstranjevanje poljubnih oznak na posameznih redakcijah in dnevniških vnosih",
+       "action-deletechangetags": "izbris oznak iz zbirke podatkov",
        "nchanges": "$1 {{PLURAL:$1|sprememba|spremembi|spremembe|sprememb|sprememb}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|od zadnjega obiska}}",
        "enhancedrc-history": "zgodovina",
        "tags-delete-not-found": "Oznaka »$1« ne obstaja.",
        "tags-delete-too-many-uses": "Oznaka »$1« je uporabljena pri več kot $2 {{PLURAL:$2|redakciji|redakcijah}}, kar pomeni, da je ni mogoče izbrisati.",
        "tags-delete-warnings-after-delete": "Oznako »$1« smo izbrisali, vendar smo naleteli na {{PLURAL:$2|naslednjo težavo|naslednji težavi|naslednje težave}}:",
+       "tags-delete-no-permission": "Nimate dovoljenja za izbris oznak sprememb.",
        "tags-activate-title": "Aktiviraj oznako",
        "tags-activate-question": "Aktivirali boste oznako »$1«.",
        "tags-activate-reason": "Razlog:",
index 1d64e07..7f197c3 100644 (file)
        "right-override-export-depth": "Exportera sidor inklusive länkade sidor till ett djup på 5",
        "right-sendemail": "Skicka e-post till andra användare",
        "right-passwordreset": "Visa e-postmeddelanden med lösenordsåterställning",
-       "right-managechangetags": "Skapa och radera [[Special:Tags|märken]] från databasen",
+       "right-managechangetags": "Skapa och (in)aktivera [[Special:Tags|märken]]",
        "right-applychangetags": "Tillämpa [[Special:Tags|märken]] tillsammans med ens ändringar",
        "right-changetags": "Lägg till och ta bort godtyckliga [[Special:Tags|märken]] på individuella sidversioner och loggposter.",
+       "right-deletechangetags": "Radera [[Special:Tags|märken]] från databasen",
        "grant-generic": "Rättighetsgrupp \"$1\"",
        "grant-group-page-interaction": "Interagera med sidor",
        "grant-group-file-interaction": "Interagera med media",
        "action-viewmyprivateinfo": "visa din privata information",
        "action-editmyprivateinfo": "redigera din privata information",
        "action-editcontentmodel": "ändra innehållsmodellen för en sida",
-       "action-managechangetags": "skapa och radera märken från databasen",
+       "action-managechangetags": "skapa och (in)aktivera märken",
        "action-applychangetags": "tillämpa märken tillsammans med dina ändringar",
        "action-changetags": "lägg till och ta bort godtyckliga märken på individuella sidversioner och loggposter",
+       "action-deletechangetags": "radera märken från databasen",
        "nchanges": "$1 {{PLURAL:$1|ändring|ändringar}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|sedan senaste besöket}}",
        "enhancedrc-history": "historik",
        "tags-delete-not-found": "Märket \"$1\" finns inte.",
        "tags-delete-too-many-uses": "Märket \"$1\" appliceras på fler än $2 {{PLURAL:$2|version|versioner}}, vilket innebär att det inte kan raderas.",
        "tags-delete-warnings-after-delete": "Märket \"$1\" raderades, men följande {{PLURAL:$2|varning|varningar}} uppstod:",
+       "tags-delete-no-permission": "Du har inte behörighet att radera ändringsmärken.",
        "tags-activate-title": "Aktivera märke",
        "tags-activate-question": "Du är på väg att aktivera märket \"$1\".",
        "tags-activate-reason": "Anledning:",
index f29edf3..e7d3e3a 100644 (file)
        "allmessagesdefault": "طے شدہ متن",
        "allmessagescurrent": "موجودہ متن",
        "allmessagestext": "یہ میڈیاویکی: جاۓ نام میں دستیاب نظامی پیغامات کی فہرست ہے۔",
+       "allmessages-filter": "تلاش بلحاظ:",
        "allmessages-filter-all": "تمام",
        "allmessages-filter-modified": "تبدیل شدہ",
+       "allmessages-prefix": "تلاش بلحاظ سابقہ:",
        "allmessages-language": "زبان:",
        "allmessages-filter-submit": "ٹھیک",
        "allmessages-filter-translate": "ترجمہ",
        "logentry-delete-delete": "$1 {{GENDER:$2|حذف کیا گیا}} صفحہ $3",
        "logentry-move-move": "$1 نے صفحہ $3 کو بجانب $4 منتقل کیا",
        "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
+       "logentry-protect-modify": "$1 نے $3 کا درجۂ حفاظت {{GENDER:$2|تبدیل کیا}} $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|اپلوڈ}} $3",
        "rightsnone": "(کچھ نہیں)",
        "revdelete-summary": "خلاصۂ تدوین",
index f261a47..cd62c12 100644 (file)
        "right-override-export-depth": "导出页面,包括最多5层链接",
        "right-sendemail": "发送电子邮件给其他用户",
        "right-passwordreset": "查看密码重置电子邮件",
-       "right-managechangetags": "从数据库创建和删除[[Special:Tags|标签]]",
+       "right-managechangetags": "创建和(取消)激活[[Special:Tags|标签]]",
        "right-applychangetags": "连同某人的更改一起应用[[Special:Tags|标签]]",
        "right-changetags": "在个别修订和日志记录中添加和移除任意[[Special:Tags|标签]]",
+       "right-deletechangetags": "从数据库删除[[Special:Tags|标签]]",
        "grant-generic": "“$1”权限束",
        "grant-group-page-interaction": "与页面交互",
        "grant-group-file-interaction": "与媒体交互",
        "action-viewmyprivateinfo": "查看您的私人信息",
        "action-editmyprivateinfo": "编辑你的私人信息",
        "action-editcontentmodel": "编辑页面的内容模型",
-       "action-managechangetags": "创建和从数据库中删除标签",
+       "action-managechangetags": "创建和(取消)激活标签",
        "action-applychangetags": "连同您的更改应用标签",
        "action-changetags": "在个别修订和日志记录中添加和移除任意标签",
+       "action-deletechangetags": "从数据库删除标签",
        "nchanges": "$1次更改",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|上次访问后}}$1个",
        "enhancedrc-history": "历史",
        "tags-delete-not-found": "标签“$1”不存在。",
        "tags-delete-too-many-uses": "“$1”标签已应用到超过$2个修订版本,这意味着它不能被删除。",
        "tags-delete-warnings-after-delete": "标签“$1”已删除,但遇到了以下{{PLURAL:$2|警告}}:",
+       "tags-delete-no-permission": "您没有权限删除更改标签。",
        "tags-activate-title": "激活标签",
        "tags-activate-question": "您将要激活标签“$1”。",
        "tags-activate-reason": "原因:",
index 260fd37..8f0ad6b 100644 (file)
@@ -4,16 +4,6 @@
        margin: 1em 0;
 }
 
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty,
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty .oo-ui-fieldLayout-body {
-       display: none;
-}
-
-.oo-ui-fieldLayout.mw-htmlform-ooui-header-errors {
-       /* Override 'display: none' from above */
-       display: block;
-}
-
 .mw-htmlform-ooui .mw-htmlform-submit-buttons {
        margin-top: 1em;
 }
index e905f69..b02fa36 100644 (file)
                                return result === null ? null : result.join( '' );
                        }
 
-                       asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ );
+                       asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
                        htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
                        htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
 
index ccb86d0..1be2d62 100644 (file)
@@ -211,6 +211,7 @@ class ParserTest {
                # add a namespace shadowing a interwiki link, to test
                # proper precedence when resolving links. (bug 51680)
                $wgExtraNamespaces[100] = 'MemoryAlpha';
+               $wgExtraNamespaces[101] = 'MemoryAlpha talk';
 
                // XXX: tests won't run without this (for CACHE_DB)
                if ( $wgMainCacheType === CACHE_DB ) {
@@ -913,7 +914,6 @@ class ParserTest {
                        'wgExperimentalHtmlIds' => false,
                        'wgExternalLinkTarget' => false,
                        'wgHtml5' => true,
-                       'wgWellFormedXml' => true,
                        'wgAdaptiveMessageCache' => true,
                        'wgDisableLangConversion' => false,
                        'wgDisableTitleConversion' => false,
index 23bdbde..7051b4f 100644 (file)
@@ -5632,7 +5632,7 @@ Parenthesis in external links, w/ transclusion or comment
 </p><p>(<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
 </p>
 !! html/parsoid
-<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[20,31,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;hi&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">hi&lt;/span>"}]]}'>http://example.com/hi</a>)</p>
+<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}&#39;>hi&lt;/span>"}]]}'>http://example.com/hi</a>)</p>
 
 <p>(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com&lt;!-- hi -->"}}'>http://example.com</a>)</p>
 !! end
@@ -5650,7 +5650,7 @@ parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 text
 <nowiki>*</nowiki>text
 <nowiki>[[foo]]</nowiki>
-<nowiki>*[[foo]]</nowiki>
+<nowiki>*</nowiki>a <nowiki>[[foo]]</nowiki>
 !! end
 
 !! test
@@ -5658,7 +5658,7 @@ mw:ExtLink -vs- mw:WikiLink (T94723)
 !! options
 parsoid=html2wt
 !! html/parsoid
-<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"piped","a":{"href":"./Foo"},"sa":{"href":"Foo"},"dsr":[0,11,6,2]}'>Bar</a>
+<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"piped","a":{"href":"./Foo"},"sa":{"href":"Foo"}}'>Bar</a>
 <a rel="mw:WikiLink" href="./Foo" title="Foo">Bar</a>
 <a rel="mw:WikiLink" href="http://en.wikipedia.org/wiki/Foo" title="Foo">Bar</a>
 <a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" title="Foo">Bar</a>
@@ -7110,6 +7110,25 @@ parsoid=html2wt
 |}
 !! end
 
+!! test
+Testing serialization after deletion in references
+!! options
+parsoid={
+  "modes": ["wt2wt"],
+  "changes": [
+    ["#x", "remove"]
+  ]
+}
+!! wikitext
+hi <ref><div id="x">ho</div></ref>
+
+<references />
+!! wikitext/edited
+hi <ref></ref>
+
+<references />
+!! end
+
 !!test
 Testing serialization after deletion of table cells
 !!options
@@ -7572,10 +7591,10 @@ Broken image links with HTML captions (bug 39700)
 <a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">abc</a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span>
-<span typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100"/></a></span>
-<span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;lt;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;&lt;&amp;quot;,&amp;quot;dsr&amp;quot;:[107,111,null,null]}\">&amp;lt;&lt;/span>"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span>
-<span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"a&lt;i data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[134,142,3,4]}\">b&lt;/i>c"}'><a href="./File:Nonexistent"><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;script>&lt;/script>"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script>&amp;lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"100x100px"},{"ck":"caption","ak":"&lt;script>&lt;/script>"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&amp;lt;script>&amp;lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"100","width":"100"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp;lt;"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=&#39;{\"src\":\"&amp;amp;lt;\",\"srcContent\":\"&amp;lt;\",\"dsr\":[107,111,null,null]}&#39;>&amp;lt;&lt;/span>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span>
+<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"a&lt;i>b&lt;/i>c"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"a&lt;i data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[134,142,3,4]}&#39;>b&lt;/i>c"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span></p>
 !! end
 
 !! test
@@ -8432,7 +8451,7 @@ Blah blah blah
 !! wikitext
 #REDIRECT [[{{echo|Foo}}bar]]
 !! html/parsoid
-<link typeof="mw:ExpandedAttrs" rel="mw:PageProp/redirect" href="./Foobar" data-mw='{"attribs":[[{"txt":"href"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[12,24,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;Foo&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">Foo&lt;/span>bar"}]]}'/>
+<link about="#mwt2" typeof="mw:ExpandedAttrs" rel="mw:PageProp/redirect" href="./Foobar" data-parsoid='{"a":{"href":"./Foobar"},"sa":{"href":"{{echo|Foo}}bar"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[12,24,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"Foo\"}},\"i\":0}}]}&#39;>Foo&lt;/span>bar"}]]}'/>
 !! end
 
 !! test
@@ -9912,7 +9931,7 @@ Parsoid: Page property magic word with magic word contents
 !! wikitext
 {{DISPLAYTITLE:''{{PAGENAME}}''}}
 !! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=\"{&amp;quot;dsr&amp;quot;:[15,31,2,2]}\">&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[]],&amp;quot;dsr&amp;quot;:[17,29,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;PAGENAME&amp;quot;,&amp;quot;function&amp;quot;:&amp;quot;pagename&amp;quot;},&amp;quot;params&amp;quot;:{},&amp;quot;i&amp;quot;:0}}]}\">Main Page&lt;/span>&lt;/i>"}]]}'/>
+<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:&#39;&#39;{{PAGENAME}}&#39;&#39;}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"&lt;i data-parsoid=&#39;{\"dsr\":[15,31,2,2]}&#39;>&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[17,29,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}&#39;>Main Page&lt;/span>&lt;/i>"}]]}'/>
 !! end
 
 !! test
@@ -9920,7 +9939,7 @@ Parsoid: Template-generated DISPLAYTITLE
 !! wikitext
 {{{{echo|DISPLAYTITLE}}:Foo}}
 !! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,29,null,null],"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DISPLAYTITLE}}:Foo"},"params":{},"i":0}}]}'/>
+<meta property="mw:PageProp/displaytitle" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DISPLAYTITLE}}:Foo"},"params":{},"i":0}}]}'/>
 !! end
 
 !! test
@@ -11211,10 +11230,9 @@ parsoid=wt2html
 |c
 |}
 !!html/parsoid
-<meta typeof="mw:Includes/IncludeOnly"/><meta typeof="mw:Includes/IncludeOnly/End"/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[31,38,null,null],&amp;quot;src&amp;quot;:&amp;quot;{{{b}}}&amp;quot;}\">{{{b}}}&lt;/span>"},{"html":""}]]}' data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}'>
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}' data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=&#39;{\"dsr\":[31,38,null,null],\"src\":\"{{{b}}}\"}&#39;>{{{b}}}&lt;/span>"},{"html":""}]]}'>
 <tbody><tr><td>c</td></tr>
 </tbody></table>
-
 !!end
 
 ###
@@ -11572,7 +11590,7 @@ Templates: Support for templates generating attributes and content
 <div style="background:#f9f9f9;">foo</div>
 
 !! html/parsoid
-<div style="background:#f9f9f9;" about="#mwt3" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html"}' data-mw='{"attribs":[[{"txt":"style","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[5,49,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;style{{=}}\\&amp;quot;background:&amp;amp;#35;f9f9f9;\\&amp;quot;&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">style&lt;/span>&lt;span typeof=\"mw:Nowiki\" about=\"#mwt1\" data-parsoid=\"{}\">=&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">\"background:&lt;/span>&lt;span typeof=\"mw:Entity\" about=\"#mwt1\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;#35;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;#&amp;quot;}\">#&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">f9f9f9;\"&lt;/span>"},{"html":""}]]}'>foo</div>
+<div style="background:#f9f9f9;" about="#mwt3" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html"}' data-mw='{"attribs":[[{"txt":"style","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[5,49,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"style{{=}}\\\"background:&amp;amp;#35;f9f9f9;\\\"\"}},\"i\":0}}]}&#39;>style&lt;/span>&lt;span typeof=\"mw:Nowiki\" about=\"#mwt1\" data-parsoid=\"{}\">=&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">\"background:&lt;/span>&lt;span typeof=\"mw:Entity\" about=\"#mwt1\" data-parsoid=&#39;{\"src\":\"&amp;amp;#35;\",\"srcContent\":\"#\"}&#39;>#&lt;/span>&lt;span about=\"#mwt1\" data-parsoid=\"{}\">f9f9f9;\"&lt;/span>"},{"html":""}]]}'>foo</div>
 !! end
 
 !! test
@@ -12931,7 +12949,7 @@ parsoid=wt2html,wt2wt,html2html
 <div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
 
 !! html/parsoid
-<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[24,38,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"width","ak":"{{echo|137px}}"},{"ck":"caption","ak":"This is a caption"}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[24,38,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"137px\"}},\"i\":0}}]}&#39;>137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"16","width":"137"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>This is a caption</figcaption></figure>
 !! end
 
 !! test
@@ -12942,7 +12960,7 @@ parsoid=wt2html,wt2wt,html2html
 <div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
 
 !! html/parsoid
-<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt3" data-mw='{"attribs":[["thumbnail",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[18,32,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;thumb&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">thumb&lt;/span>"}],["width",{"html":"&lt;span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[33,47,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt3" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"{{echo|thumb}}"},{"ck":"width","ak":"{{echo|137px}}"},{"ck":"caption","ak":"This is a caption"}]}' data-mw='{"attribs":[["thumbnail",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,32,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"thumb\"}},\"i\":0}}]}&#39;>thumb&lt;/span>"}],["width",{"html":"&lt;span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[33,47,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"137px\"}},\"i\":0}}]}&#39;>137px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="16" width="137" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"16","width":"137"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>This is a caption</figcaption></figure>
 !! end
 
 !! test
@@ -12953,7 +12971,7 @@ parsoid=wt2html,wt2wt,html2html
 <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a>
 </p>
 !! html/parsoid
-<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;dsr&amp;quot;:[18,31,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;50px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">50px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p>
+<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,31,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"50px\"}},\"i\":0}}]}&#39;>50px&lt;/span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 ## Parsoid does not provide editing support for images where templates produce multiple image attributes.
@@ -13323,8 +13341,9 @@ Image with wiki markup in implicit alt
 </p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"testing &lt;b data-parsoid=\"{&amp;quot;dsr&amp;quot;:[27,37,3,3]}\">bold&lt;/b> in alt"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></span></p>
-<p><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt","resource":"Image:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt"}]}' data-mw='{"caption":"testing &lt;b data-parsoid=&#39;{\"dsr\":[27,37,3,3]}&#39;>bold&lt;/b> in alt"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></span></p>
+
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"alt","ak":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing &#39;&#39;&#39;bold&#39;&#39;&#39; in alt","resource":"Image:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13335,7 +13354,7 @@ Alt image option should handle most kinds of wikitext without barfing
 <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="This is a link and a bold template." src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is the image caption</div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[65,73,2,2]}\">link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[80,106,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&#39;&#39;bold template&#39;&#39;&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}&#39;>link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;#39;&amp;#39;bold template&amp;#39;&amp;#39;\"}},\"i\":0}}]}&#39;>bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
 !! end
 
 ###################
@@ -13562,7 +13581,7 @@ Frameless image caption with a free URL
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;url&amp;quot;,&amp;quot;dsr&amp;quot;:[18,36,0,0]}\">http://example.com&lt;/a>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"http://example.com"}]}' data-mw='{"caption":"&lt;a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid=&#39;{\"stx\":\"url\",\"dsr\":[18,36,0,0]}&#39;>http://example.com&lt;/a>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13672,7 +13691,7 @@ BUG 648: Frameless image caption with a link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[30,38,2,2]}\">link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,38,2,2]}&#39;>link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13683,7 +13702,7 @@ BUG 648: Frameless image caption with a link (suffix)
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;link&amp;quot;},&amp;quot;dsr&amp;quot;:[30,41,2,5],&amp;quot;tail&amp;quot;:&amp;quot;foo&amp;quot;}\">linkfoo&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]]foo in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,41,2,5],\"tail\":\"foo\"}&#39;>linkfoo&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13694,7 +13713,7 @@ BUG 648: Frameless image caption with an interwiki link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;http://www.usemod.com/cgi-bin/mb.pl?Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;MeatBall:Link&amp;quot;},&amp;quot;isIW&amp;quot;:true,&amp;quot;dsr&amp;quot;:[30,47,2,2]}\">MeatBall:Link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,47,2,2]}&#39;>MeatBall:Link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13705,7 +13724,7 @@ BUG 648: Frameless image caption with a piped interwiki link
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;piped&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;http://www.usemod.com/cgi-bin/mb.pl?Link&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;MeatBall:Link&amp;quot;},&amp;quot;isIW&amp;quot;:true,&amp;quot;dsr&amp;quot;:[30,52,16,2]}\">link&lt;/a> in it"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link|link]] in it"}]}' data-mw='{"caption":"text with a &lt;a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid=&#39;{\"stx\":\"piped\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,52,16,2]}&#39;>link&lt;/a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13713,7 +13732,7 @@ T107474: Frameless image caption with <nowiki>
 !! wikitext
 [[File:Foobar.jpg|<nowiki>text with a [[MeatBall:Link|link]] in it</nowiki>]]
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;span typeof=\"mw:Nowiki\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[18,75,8,9]}\">text with a [[MeatBall:Link|link]] in it&lt;/span>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;nowiki>text with a [[MeatBall:Link|link]] in it&lt;/nowiki>"}]}' data-mw='{"caption":"&lt;span typeof=\"mw:Nowiki\" data-parsoid=&#39;{\"dsr\":[18,75,8,9]}&#39;>text with a [[MeatBall:Link|link]] in it&lt;/span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13724,7 +13743,7 @@ Escape HTML special chars in image alt text
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="&amp; &lt; &gt; &quot;"><img alt="&amp; &lt; &gt; &quot;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&amp;amp; &amp;lt; &amp;gt; \""}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp; &lt; > \""}]}' data-mw='{"caption":"&amp;amp; &amp;lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -13735,7 +13754,7 @@ BUG 499: Alt text should have &#1234;, not &amp;1234;
 <p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=\"{&amp;quot;src&amp;quot;:&amp;quot;&amp;amp;#9792;&amp;quot;,&amp;quot;srcContent&amp;quot;:&amp;quot;♀&amp;quot;,&amp;quot;dsr&amp;quot;:[18,25,null,null]}\">♀&lt;/span>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&amp;#9792;"}]}' data-mw='{"caption":"&lt;span typeof=\"mw:Entity\" data-parsoid=&#39;{\"src\":\"&amp;amp;#9792;\",\"srcContent\":\"♀\",\"dsr\":[18,25,null,null]}&#39;>♀&lt;/span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -14073,7 +14092,7 @@ Parsoid-specific image handling - simple image with a formatted caption
 !! wikitext
 [[File:Foobar.jpg|<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>]]
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;table data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[18,81,7,8]}\">&lt;tbody data-parsoid=\"{&amp;quot;dsr&amp;quot;:[25,73,0,0]}\">&lt;tr data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[25,54,4,5]}\">&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[29,39,4,5]}\">a&lt;/td>&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[39,49,4,5]}\">b&lt;/td>&lt;/tr>&lt;tr data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[54,73,4,5]}\">&lt;td data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;html&amp;quot;,&amp;quot;dsr&amp;quot;:[58,68,4,5]}\">c&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;table>&lt;tr>&lt;td>a&lt;/td>&lt;td>b&lt;/td>&lt;/tr>&lt;tr>&lt;td>c&lt;/td>&lt;/tr>&lt;/table>"}]}' data-mw='{"caption":"&lt;table data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[18,81,7,8]}&#39;>&lt;tbody data-parsoid=&#39;{\"dsr\":[25,73,0,0]}&#39;>&lt;tr data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[25,54,4,5]}&#39;>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[29,39,4,5]}&#39;>a&lt;/td>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[39,49,4,5]}&#39;>b&lt;/td>&lt;/tr>&lt;tr data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[54,73,4,5]}&#39;>&lt;td data-parsoid=&#39;{\"stx\":\"html\",\"dsr\":[58,68,4,5]}&#39;>c&lt;/td>&lt;/tr>&lt;/tbody>&lt;/table>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 !! end
 
 !! test
@@ -14167,7 +14186,7 @@ T93580: 2. <ref> inside inline images
 
 <references />
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: &lt;ref>foo&lt;/ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[64,78,5,6]}\" data-mw=\"{&amp;quot;name&amp;quot;:&amp;quot;ref&amp;quot;,&amp;quot;body&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;mw-reference-text-cite_note-1&amp;quot;},&amp;quot;attrs&amp;quot;:{}}\">&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=\"{&amp;quot;group&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;foo&amp;quot;,&amp;quot;hasRefInRef&amp;quot;:false,&amp;quot;dsr&amp;quot;:[64,78,5,6],&amp;quot;tmp&amp;quot;:{}}\" data-mw=\"{}\">"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: &lt;ref>foo&lt;/ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[64,78,5,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-1\"},\"attrs\":{}}&#39;>&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=&#39;{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,78,5,6]}&#39;/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol>
 !! end
@@ -14179,7 +14198,7 @@ T93580: 3. Templated <ref> inside inline images
 
 <references />
 !! html/parsoid
-<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|&lt;ref>{{echo|foo}}&lt;/ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion  mw:Extension/ref\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[64,96,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&lt;ref>{{echo|foo}}&lt;/ref>&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Transclusion mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=\"{&amp;quot;group&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;foo&amp;quot;,&amp;quot;hasRefInRef&amp;quot;:false,&amp;quot;dsr&amp;quot;:[64,96,null,null],&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;}]],&amp;quot;tmp&amp;quot;:{}}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;&lt;ref>{{echo|foo}}&lt;/ref>&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\">"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|&lt;ref>{{echo|foo}}&lt;/ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: &lt;span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion  mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;lt;ref>{{echo|foo}}&amp;lt;/ref>\"}},\"i\":0}}]}&#39;>&lt;a href=\"#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>&lt;meta typeof=\"mw:Transclusion mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid=&#39;{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;lt;ref>{{echo|foo}}&amp;lt;/ref>\"}},\"i\":0}}]}&#39;/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
 
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol>
 !! end
@@ -14781,7 +14800,7 @@ Parsoid: Defaultsort (template-generated)
 !! wikitext
 {{{{echo|DEFAULTSORT}}:Foo}}
 !! html/parsoid
-<meta property="mw:PageProp/categorydefaultsort" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,28,null,null],"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DEFAULTSORT}}:Foo"},"params":{},"i":0}}]}'/>
+<meta property="mw:PageProp/categorydefaultsort" content="Foo" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|DEFAULTSORT}}:Foo"},"params":{},"i":0}}]}'/>
 !! end
 
 ###
@@ -16010,7 +16029,7 @@ Bug 2304: HTML attribute safety (dangerous template; 2309)
 <div title=""></div>
 
 !! html/parsoid
-<div title='" onmouseover="alert(document.cookie)' about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"title":"\" onmouseover=\"alert(document.cookie)"},"sa":{"title":"{{dangerous attribute}}"}}' data-mw='{"attribs":[[{"txt":"title"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[]],&amp;quot;dsr&amp;quot;:[12,35,null,null]}\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;dangerous attribute&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Dangerous_attribute&amp;quot;},&amp;quot;params&amp;quot;:{},&amp;quot;i&amp;quot;:0}}]}\">\" onmouseover=\"alert(document.cookie)&lt;/span>"}]]}'></div>
+<div title='" onmouseover="alert(document.cookie)' about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"title":"\" onmouseover=\"alert(document.cookie)"},"sa":{"title":"{{dangerous attribute}}"}}' data-mw='{"attribs":[[{"txt":"title"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"pi\":[[]],\"dsr\":[12,35,null,null]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"dangerous attribute\",\"href\":\"./Template:Dangerous_attribute\"},\"params\":{},\"i\":0}}]}&#39;>\" onmouseover=\"alert(document.cookie)&lt;/span>"}]]}'></div>
 !! end
 
 !! test
@@ -16549,9 +16568,12 @@ array (
 <pre typeof="mw:Extension/tag" data-mw='{"name":"tag","attrs":{"foo":"bar"},"body":null}' data-parsoid='{}' about="#mwt2"></pre>text
 !! end
 
-# </tag> should be output literally since there is no matching tag that begins it
+## </tag> should be output literally since there is no matching tag that begins it
+## Don't expect parsoid to rt this form.
 !! test
 Parser hook: basic arguments using terminated empty elements (bug 2374)
+!! options
+parsoid=wt2html
 !! wikitext
 <tag width=200 height = "100" depth = '50' square/>
 other stuff
@@ -16569,6 +16591,28 @@ array (
 <p>other stuff
 &lt;/tag&gt;
 </p>
+!! html/parsoid
+<pre typeof="mw:Extension/tag" data-mw='{"name":"tag","attrs":{"width":"200","height":"100","depth":"50","square":""},"body":null}' about="#mwt2"></pre><p>other stuff
+&lt;/tag></p>
+!! end
+
+## Don't expect parsoid to rt this form.
+!! test
+Parser hook: Don't allow unclosed extension tags
+!! options
+parsoid=wt2html
+!! wikitext
+test <tag>123
+
+this is a '''test'''
+!! html/php
+<p>test &lt;tag&gt;123
+</p><p>this is a <b>test</b>
+</p>
+!! html/parsoid
+<p>test &lt;tag>123</p>
+
+<p>this is a <b>test</b></p>
 !! end
 
 ###
@@ -16891,7 +16935,7 @@ HTML nested bullet list, closed tags (bug 5497)
 </ul>
 </li>
 </ul>
-!! html
+!! html/php
 <ul>
 <li>One</li>
 <li>Two:
@@ -16902,6 +16946,16 @@ HTML nested bullet list, closed tags (bug 5497)
 </li>
 </ul>
 
+!! html/parsoid
+<ul data-parsoid='{"stx":"html"}'>
+<li data-parsoid='{"stx":"html"}'>One</li>
+<li data-parsoid='{"stx":"html"}'>Two:
+<ul data-parsoid='{"stx":"html"}'>
+<li data-parsoid='{"stx":"html"}'>Sub-one</li>
+<li data-parsoid='{"stx":"html"}'>Sub-two</li>
+</ul>
+</li>
+</ul>
 !! end
 
 !! test
@@ -17271,7 +17325,7 @@ Fuzz testing: image with bogus manual thumbnail
 <div class="thumb tright"><div class="thumbinner" style="width:182px;">Error creating thumbnail:   <div class="thumbcaption"></div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"manualthumb","ak":"thumbnail= "}],"dsr":[0,32,2,2]}' data-mw='{"errors":[{"key":"missing-thumbnail","message":"This thumbnail does not exist.","params":{"name":""}}],"thumb":""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{},"dsr":[2,30,null,null]}'><img resource="./File:Foobar.jpg" src="./Special:FilePath/" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"220"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure>
+<figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"manualthumb","ak":"thumbnail= "}]}' data-mw='{"errors":[{"key":"missing-thumbnail","message":"This thumbnail does not exist.","params":{"name":""}}],"thumb":""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="./Special:FilePath/" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"220"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure>
 !!end
 
 !! test
@@ -20592,6 +20646,7 @@ this is not the the title
 !! html/php
 Screen
 <p>this is not the the title
+<span class="error"><strong>Warning:</strong> Display title "whatever" was ignored since it is not equivalent to the page's actual title.</span>
 </p>
 !! end
 
@@ -20855,9 +20910,9 @@ percent-encoding and + signs in internal links (Bug 26410)
 <a href="/index.php?title=3E&amp;action=edit&amp;redlink=1" class="new" title="3E (page does not exist)">3E</a> <a href="/index.php?title=3E%2B&amp;action=edit&amp;redlink=1" class="new" title="3E+ (page does not exist)">3E+</a>
 </p>
 !! html/parsoid
-<p><a rel="mw:WikiLink" href="./User:+%25" title="User:+%">User:+%</a> <a rel="mw:WikiLink" href="Page+title%25" title="Page+title%">Page+title%</a>
-<a rel="mw:WikiLink" href="%25+" title="%+">%+</a> <a rel="mw:WikiLink" href="%25+" title="%+">%20</a> <a rel="mw:WikiLink" href="%25+" title="%+">%+ </a> <a rel="mw:WikiLink" href="%25+r" title="%+r">%+r</a>
-<a rel="mw:WikiLink" href="%25" title="%">%</a> <a rel="mw:WikiLink" href="+" title="+">+</a> <span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid=\"{&amp;quot;stx&amp;quot;:&amp;quot;simple&amp;quot;,&amp;quot;a&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;./Bar&amp;quot;},&amp;quot;sa&amp;quot;:{&amp;quot;href&amp;quot;:&amp;quot;bar&amp;quot;},&amp;quot;dsr&amp;quot;:[94,101,2,2]}\">bar&lt;/a>"}'><a href="./File:%25+abc9"><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></span>
+<p><a rel="mw:WikiLink" href="./User:+%25" title="User:+%" data-parsoid='{"stx":"simple","a":{"href":"./User:+%25"},"sa":{"href":"User:+%"}}'>User:+%</a> <a rel="mw:WikiLink" href="./Page+title%25" title="Page+title%" data-parsoid='{"stx":"simple","a":{"href":"./Page+title%25"},"sa":{"href":"Page+title%"}}'>Page+title%</a>
+<a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%+</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"piped","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%20</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+ "}}'>%+ </a> <a rel="mw:WikiLink" href="./%25+r" title="%+r" data-parsoid='{"stx":"simple","a":{"href":"./%25+r"},"sa":{"href":"%+r"}}'>%+r</a>
+<a rel="mw:WikiLink" href="./%25" title="%" data-parsoid='{"stx":"simple","a":{"href":"./%25"},"sa":{"href":"%"}}'>%</a> <a rel="mw:WikiLink" href="./+" title="+" data-parsoid='{"stx":"simple","a":{"href":"./+"},"sa":{"href":"+"}}'>+</a> <span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"&lt;a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Bar\"},\"sa\":{\"href\":\"bar\"},\"dsr\":[94,101,2,2]}&#39;>bar&lt;/a>"}'><a href="./File:%25+abc9" data-parsoid='{"a":{"href":"./File:%25+abc9"},"sa":{}}'><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></span>
 <a rel="mw:WikiLink" href="./3E" title="3E" data-parsoid='{"stx":"simple","a":{"href":"./3E"},"sa":{"href":"%33%45"}}'>3E</a> <a rel="mw:WikiLink" href="./3E+" title="3E+" data-parsoid='{"stx":"simple","a":{"href":"./3E+"},"sa":{"href":"%33%45+"}}'>3E+</a></p>
 !! end
 
@@ -22108,7 +22163,7 @@ This should just get lost.
 B <span about="#mwt4" class="mw-ref" id="cite_ref-b_2-0" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-b-2"},"attrs":{"name":"b"}}'><a href="#cite_note-b-2"><span class="mw-reflink-text">[2]</span></a></span></p>
 
 
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{},"body":{"html":"\n&lt;span about=\"#mwt8\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[59,82,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-a-1\"},\"attrs\":{\"name\":\"a\"}}&#39;>&lt;a href=\"#cite_note-a-1\" style=\"counter-reset: mw-Ref 1;\">&lt;span class=\"mw-reflink-text\">[1]&lt;/span>&lt;/a>&lt;/span>\n"}}'><li about="#cite_note-a-1" id="cite_note-a-1"><a href="#cite_ref-a_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-a-1" class="mw-reference-text">foo</span></li><li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">bar</span></li>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{},"body":{"html":"\n&lt;span about=\"#mwt8\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[59,82,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-a-1\"},\"attrs\":{\"name\":\"a\"}}&#39;>&lt;a href=\"#cite_note-a-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]&lt;/span>&lt;/a>&lt;/span>\n"}}'><li about="#cite_note-a-1" id="cite_note-a-1"><a href="#cite_ref-a_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-a-1" class="mw-reference-text">foo</span></li><li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">bar</span></li>
 </ol>
 !! end
 
@@ -22141,7 +22196,7 @@ B <span about="#mwt4" class="mw-ref" id="cite_ref-b_2-0" rel="dc:references" typ
 <li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo bar for a</span></li>
 </ol>
 
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt8" data-mw-group="X" data-mw='{"name":"references","attrs":{"group":"X"},"body":{"html":"\n&lt;span about=\"#mwt10\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[96,119,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-b-2\"},\"attrs\":{\"name\":\"b\"}}&#39;>&lt;a href=\"#cite_note-b-2\" style=\"counter-reset: mw-Ref 1;\" data-mw-group=\"X\">&lt;span class=\"mw-reflink-text\">[X 1]&lt;/span>&lt;/a>&lt;/span>\n"}}'>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt8" data-mw-group="X" data-mw='{"name":"references","attrs":{"group":"X"},"body":{"html":"\n&lt;span about=\"#mwt10\" class=\"mw-ref\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid=&#39;{\"dsr\":[96,119,14,6]}&#39; data-mw=&#39;{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-b-2\"},\"attrs\":{\"name\":\"b\"}}&#39;>&lt;a href=\"#cite_note-b-2\" style=\"counter-reset: mw-Ref 1;\" data-mw-group=\"X\" data-parsoid=\"{}\">&lt;span class=\"mw-reflink-text\" data-parsoid=\"{}\">[X 1]&lt;/span>&lt;/a>&lt;/span>\n"}}'>
 <li about="#cite_note-b-2" id="cite_note-b-2"><a href="#cite_ref-b_2-0" data-mw-group="X" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-b-2" class="mw-reference-text">foo</span></li>
 </ol>
 !! end
@@ -22204,8 +22259,11 @@ Entities in ref name
 </ol>
 !! end
 
-# This test is wt2html only because we're permitting the serializer to produce
-# dirty diffs, normalizing the unclosed references to the self-closed version.
+## The output here may look funny, but it's what the php parser will do.  The
+## unclosed references tag becomes escaped text, and then a new references
+## tag is auto-generated.  The test is wt2html only because it roundtrips with
+## nowiki tags, and the auto-generated references tag is only dropped in
+## rtTestMode.
 !! test
 Generate references for unclosed references tag
 !! options
@@ -22215,9 +22273,10 @@ a<ref>foo</ref>
 
 <references>
 !! html/parsoid
-<p>a<span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1"><span class="mw-reflink-text">[1]</span></a></span></p>
-<ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'>
-<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol>
+<p>a<span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></p>
+
+<p>&lt;references></p>
+<ol class="mw-references" typeof="mw:Extension/references" about="#mwt3" data-mw='{"name":"references","attrs":{},"autoGenerated":true}'><li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol>
 !! end
 
 !! test
 !! test
 2. Leading whitespace in non-indent-pre contexts should not be escaped
 !! options
-parsoid=htm2wt
+parsoid=html2wt
 !! html/parsoid
 <p>foo <span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="#cite_note-1"><span class="mw-reflink-text">[1]</span></a></span></p>
 <ol class="mw-references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'>
-<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><i data-parsoid='{"dsr":[9,14,2,2]}'>a</i>
+<li about="#cite_note-1" id="cite_note-1"><a href="#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><i>a</i>
  b</span></li>
 </ol>
 !! wikitext
@@ -24663,7 +24722,7 @@ T115289: Don't migrate newlines out of tables with fostered content
 !! wikitext
 <table><td></td>{{echo|<tr>[[Category:One]]}}<!--c-->[[Category:Two]]
 !! html/parsoid
-<link rel="mw:PageProp/Category" href="./Category:One" about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"stx":"simple","a":{"href":"./Category:One"},"sa":{"href":"Category:One"},"fostered":true,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["&lt;table>&lt;td>&lt;/td>",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;tr>[[Category:One]]"}},"i":0}},"&lt;!--c-->[[Category:Two]]"]}'/><link rel="mw:PageProp/Category" href="./Category:Two" about="#mwt2"/><table about="#mwt2" data-parsoid='{"stx":"html","autoInsertedEnd":true,"dsr":[0,53,7,0]}'><tbody><tr><td></td></tr><tr><!--c--></tr></tbody></table>
+<link rel="mw:PageProp/Category" href="./Category:One" about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"stx":"simple","a":{"href":"./Category:One"},"sa":{"href":"Category:One"},"fostered":true,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["&lt;table>&lt;td>&lt;/td>",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;tr>[[Category:One]]"}},"i":0}},"&lt;!--c-->[[Category:Two]]"]}'/><link rel="mw:PageProp/Category" href="./Category:Two" about="#mwt2"/><table about="#mwt2" data-parsoid='{"stx":"html","autoInsertedEnd":true}'><tbody><tr><td></td></tr><tr><!--c--></tr></tbody></table>
 !! end
 
 !! test
@@ -26689,8 +26748,8 @@ parsoid=html2wt
 !! html/parsoid
 <p>x<meta typeof="mw:Placeholder" data-parsoid='{"src":"&lt;nowiki/>"}'/><meta typeof="mw:Placeholder" data-parsoid='{"src":"&lt;nowiki/>"}'/>
 y</p>
-<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,23,null,null],"pi":[[{"k":"1","named":true,"spc":["\n"," "," ",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
-<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[0,24,null,null],"pi":[[{"k":"1","named":true,"spc":["\n"," "," ","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
+<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1","named":true,"spc":["\n"," "," ",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
+<p><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1","named":true,"spc":["\n"," "," ","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;nowiki/>"}},"i":0}}]}'></span></p>
 !! wikitext
 x
 y
index 6c92b8c..a01f2f5 100644 (file)
@@ -7,7 +7,6 @@ class HtmlTest extends MediaWikiTestCase {
                parent::setUp();
 
                $this->setMwGlobals( [
-                       'wgWellFormedXml' => false,
                        'wgUseMediaWikiUIEverywhere' => false,
                ] );
 
@@ -45,7 +44,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testElementBasics() {
                $this->assertEquals(
-                       '<img>',
+                       '<img/>',
                        Html::element( 'img', null, '' ),
                        'No close tag for short-tag elements'
                );
@@ -62,12 +61,10 @@ class HtmlTest extends MediaWikiTestCase {
                        'Close tag for empty element (array, string)'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
                $this->assertEquals(
                        '<img/>',
                        Html::element( 'img', null, '' ),
-                       'Self-closing tag for short-tag elements (wgWellFormedXml = true)'
+                       'Self-closing tag for short-tag elements'
                );
        }
 
@@ -134,22 +131,20 @@ class HtmlTest extends MediaWikiTestCase {
                );
 
                $this->assertEquals(
-                       ' selected',
+                       ' selected=""',
                        Html::expandAttributes( [ 'selected' => true ] ),
                        'Boolean attributes have no value when value is true'
                );
                $this->assertEquals(
-                       ' selected',
+                       ' selected=""',
                        Html::expandAttributes( [ 'selected' ] ),
                        'Boolean attributes have no value when value is true (passed as numerical array)'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
                $this->assertEquals(
                        ' selected=""',
                        Html::expandAttributes( [ 'selected' => true ] ),
-                       'Boolean attributes have empty string value when value is true (wgWellFormedXml)'
+                       'Boolean attributes have empty string value when value is true'
                );
        }
 
@@ -158,12 +153,12 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testExpandAttributesForNumbers() {
                $this->assertEquals(
-                       ' value=1',
+                       ' value="1"',
                        Html::expandAttributes( [ 'value' => 1 ] ),
                        'Integer value is cast to a string'
                );
                $this->assertEquals(
-                       ' value=1.1',
+                       ' value="1.1"',
                        Html::expandAttributes( [ 'value' => 1.1 ] ),
                        'Float value is cast to a string'
                );
@@ -174,7 +169,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testExpandAttributesForObjects() {
                $this->assertEquals(
-                       ' value=stringValue',
+                       ' value="stringValue"',
                        Html::expandAttributes( [ 'value' => new HtmlTestValue() ] ),
                        'Object value is converted to a string'
                );
@@ -193,43 +188,21 @@ class HtmlTest extends MediaWikiTestCase {
                        'Empty string is always quoted'
                );
                $this->assertEquals(
-                       ' key=value',
+                       ' key="value"',
                        Html::expandAttributes( [ 'key' => 'value' ] ),
                        'Simple string value needs no quotes'
                );
                $this->assertEquals(
-                       ' one=1',
+                       ' one="1"',
                        Html::expandAttributes( [ 'one' => 1 ] ),
                        'Number 1 value needs no quotes'
                );
                $this->assertEquals(
-                       ' zero=0',
+                       ' zero="0"',
                        Html::expandAttributes( [ 'zero' => 0 ] ),
                        'Number 0 value needs no quotes'
                );
 
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-
-               $this->assertEquals(
-                       ' empty_string=""',
-                       Html::expandAttributes( [ 'empty_string' => '' ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Empty string'
-               );
-               $this->assertEquals(
-                       ' key="value"',
-                       Html::expandAttributes( [ 'key' => 'value' ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Simple string'
-               );
-               $this->assertEquals(
-                       ' one="1"',
-                       Html::expandAttributes( [ 'one' => 1 ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Number 1'
-               );
-               $this->assertEquals(
-                       ' zero="0"',
-                       Html::expandAttributes( [ 'zero' => 0 ] ),
-                       'Attribute values are always quoted (wgWellFormedXml): Number 0'
-               );
        }
 
        /**
@@ -346,48 +319,48 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testNamespaceSelector() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(),
                        'Basic namespace selector without custom options'
                );
 
                $this->assertEquals(
-                       '<label for=mw-test-namespace>Select a namespace:</label>&#160;' .
-                               '<select id=mw-test-namespace name=wpNamespace>' . "\n" .
-                               '<option value=all>all</option>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2 selected>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<label for="mw-test-namespace">Select a namespace:</label>&#160;' .
+                               '<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
+                               '<option value="all">all</option>' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2" selected="">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ],
@@ -397,24 +370,24 @@ class HtmlTest extends MediaWikiTestCase {
                );
 
                $this->assertEquals(
-                       '<label for=namespace>Select a namespace:</label>&#160;' .
-                               '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=0>(Main)</option>' . "\n" .
-                               '<option value=1>Talk</option>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=3>User talk</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<label for="namespace">Select a namespace:</label>&#160;' .
+                               '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="0">(Main)</option>' . "\n" .
+                               '<option value="1">Talk</option>' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="3">User talk</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'label' => 'Select a namespace:' ]
@@ -425,18 +398,18 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testCanFilterOutNamespaces() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option value=2>User</option>' . "\n" .
-                               '<option value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option value="2">User</option>' . "\n" .
+                               '<option value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector(
                                [ 'exclude' => [ 0, 1, 3, 100, 101 ] ]
@@ -447,23 +420,23 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testCanDisableANamespaces() {
                $this->assertEquals(
-                       '<select id=namespace name=namespace>' . "\n" .
-                               '<option disabled value=0>(Main)</option>' . "\n" .
-                               '<option disabled value=1>Talk</option>' . "\n" .
-                               '<option disabled value=2>User</option>' . "\n" .
-                               '<option disabled value=3>User talk</option>' . "\n" .
-                               '<option disabled value=4>MyWiki</option>' . "\n" .
-                               '<option value=5>MyWiki Talk</option>' . "\n" .
-                               '<option value=6>File</option>' . "\n" .
-                               '<option value=7>File talk</option>' . "\n" .
-                               '<option value=8>MediaWiki</option>' . "\n" .
-                               '<option value=9>MediaWiki talk</option>' . "\n" .
-                               '<option value=10>Template</option>' . "\n" .
-                               '<option value=11>Template talk</option>' . "\n" .
-                               '<option value=14>Category</option>' . "\n" .
-                               '<option value=15>Category talk</option>' . "\n" .
-                               '<option value=100>Custom</option>' . "\n" .
-                               '<option value=101>Custom talk</option>' . "\n" .
+                       '<select id="namespace" name="namespace">' . "\n" .
+                               '<option disabled="" value="0">(Main)</option>' . "\n" .
+                               '<option disabled="" value="1">Talk</option>' . "\n" .
+                               '<option disabled="" value="2">User</option>' . "\n" .
+                               '<option disabled="" value="3">User talk</option>' . "\n" .
+                               '<option disabled="" value="4">MyWiki</option>' . "\n" .
+                               '<option value="5">MyWiki Talk</option>' . "\n" .
+                               '<option value="6">File</option>' . "\n" .
+                               '<option value="7">File talk</option>' . "\n" .
+                               '<option value="8">MediaWiki</option>' . "\n" .
+                               '<option value="9">MediaWiki talk</option>' . "\n" .
+                               '<option value="10">Template</option>' . "\n" .
+                               '<option value="11">Template talk</option>' . "\n" .
+                               '<option value="14">Category</option>' . "\n" .
+                               '<option value="15">Category talk</option>' . "\n" .
+                               '<option value="100">Custom</option>' . "\n" .
+                               '<option value="101">Custom talk</option>' . "\n" .
                                '</select>',
                        Html::namespaceSelector( [
                                'disable' => [ 0, 1, 2, 3, 4 ]
@@ -478,7 +451,7 @@ class HtmlTest extends MediaWikiTestCase {
         */
        public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
                $this->assertEquals(
-                       '<input type=' . $HTML5InputType . '>',
+                       '<input type="' . $HTML5InputType . '"/>',
                        Html::element( 'input', [ 'type' => $HTML5InputType ] ),
                        'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"'
                );
@@ -528,14 +501,14 @@ class HtmlTest extends MediaWikiTestCase {
                $cases = [];
 
                # ## Generic cases, match $attribDefault static array
-               $cases[] = [ '<area>',
+               $cases[] = [ '<area/>',
                        'area', [ 'shape' => 'rect' ]
                ];
 
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'formaction' => 'GET' ]
                ];
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ]
                ];
 
@@ -553,7 +526,7 @@ class HtmlTest extends MediaWikiTestCase {
                        'canvas', [ 'width' => 300 ]
                ];
 
-               $cases[] = [ '<command>',
+               $cases[] = [ '<command/>',
                        'command', [ 'type' => 'command' ]
                ];
 
@@ -567,18 +540,18 @@ class HtmlTest extends MediaWikiTestCase {
                        'form', [ 'enctype' => 'application/x-www-form-urlencoded' ]
                ];
 
-               $cases[] = [ '<input>',
+               $cases[] = [ '<input/>',
                        'input', [ 'formaction' => 'GET' ]
                ];
-               $cases[] = [ '<input>',
+               $cases[] = [ '<input/>',
                        'input', [ 'type' => 'text' ]
                ];
 
-               $cases[] = [ '<keygen>',
+               $cases[] = [ '<keygen/>',
                        'keygen', [ 'keytype' => 'rsa' ]
                ];
 
-               $cases[] = [ '<link>',
+               $cases[] = [ '<link/>',
                        'link', [ 'media' => 'all' ]
                ];
 
@@ -604,44 +577,44 @@ class HtmlTest extends MediaWikiTestCase {
                # ## SPECIFIC CASES
 
                # <link type="text/css">
-               $cases[] = [ '<link>',
+               $cases[] = [ '<link/>',
                        'link', [ 'type' => 'text/css' ]
                ];
 
                # <input> specific handling
-               $cases[] = [ '<input type=checkbox>',
+               $cases[] = [ '<input type="checkbox"/>',
                        'input', [ 'type' => 'checkbox', 'value' => 'on' ],
                        'Default value "on" is stripped of checkboxes',
                ];
-               $cases[] = [ '<input type=radio>',
+               $cases[] = [ '<input type="radio"/>',
                        'input', [ 'type' => 'radio', 'value' => 'on' ],
                        'Default value "on" is stripped of radio buttons',
                ];
-               $cases[] = [ '<input type=submit value=Submit>',
+               $cases[] = [ '<input type="submit" value="Submit"/>',
                        'input', [ 'type' => 'submit', 'value' => 'Submit' ],
                        'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
                ];
-               $cases[] = [ '<input type=color>',
+               $cases[] = [ '<input type="color"/>',
                        'input', [ 'type' => 'color', 'value' => '' ],
                ];
-               $cases[] = [ '<input type=range>',
+               $cases[] = [ '<input type="range"/>',
                        'input', [ 'type' => 'range', 'value' => '' ],
                ];
 
                # <button> specific handling
                # see remarks on http://msdn.microsoft.com/en-us/library/ie/ms535211%28v=vs.85%29.aspx
-               $cases[] = [ '<button type=submit></button>',
+               $cases[] = [ '<button type="submit"></button>',
                        'button', [ 'type' => 'submit' ],
                        'According to standard the default type is "submit". '
                                . 'Depending on compatibility mode IE might use "button", instead.',
                ];
 
                # <select> specific handling
-               $cases[] = [ '<select multiple></select>',
+               $cases[] = [ '<select multiple=""></select>',
                        'select', [ 'size' => '4', 'multiple' => true ],
                ];
                # .. with numeric value
-               $cases[] = [ '<select multiple></select>',
+               $cases[] = [ '<select multiple=""></select>',
                        'select', [ 'size' => 4, 'multiple' => true ],
                ];
                $cases[] = [ '<select></select>',
@@ -693,7 +666,7 @@ class HtmlTest extends MediaWikiTestCase {
                        'Blacklist form validation attributes.'
                );
                $this->assertEquals(
-                       ' step=any',
+                       ' step="any"',
                        Html::expandAttributes(
                                [
                                        'min' => 1,
@@ -709,12 +682,12 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperInput() {
                $this->assertEquals(
-                       '<input type=radio value=testval name=testname>',
+                       '<input type="radio" value="testval" name="testname"/>',
                        Html::input( 'testname', 'testval', 'radio' ),
                        'Input wrapper with type and value.'
                );
                $this->assertEquals(
-                       '<input name=testname>',
+                       '<input name="testname"/>',
                        Html::input( 'testname' ),
                        'Input wrapper with all default values.'
                );
@@ -722,17 +695,17 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperCheck() {
                $this->assertEquals(
-                       '<input type=checkbox value=1 name=testname>',
+                       '<input type="checkbox" value="1" name="testname"/>',
                        Html::check( 'testname' ),
                        'Checkbox wrapper unchecked.'
                );
                $this->assertEquals(
-                       '<input checked type=checkbox value=1 name=testname>',
+                       '<input checked="" type="checkbox" value="1" name="testname"/>',
                        Html::check( 'testname', true ),
                        'Checkbox wrapper checked.'
                );
                $this->assertEquals(
-                       '<input type=checkbox value=testval name=testname>',
+                       '<input type="checkbox" value="testval" name="testname"/>',
                        Html::check( 'testname', false, [ 'value' => 'testval' ] ),
                        'Checkbox wrapper with a value override.'
                );
@@ -740,17 +713,17 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperRadio() {
                $this->assertEquals(
-                       '<input type=radio value=1 name=testname>',
+                       '<input type="radio" value="1" name="testname"/>',
                        Html::radio( 'testname' ),
                        'Radio wrapper unchecked.'
                );
                $this->assertEquals(
-                       '<input checked type=radio value=1 name=testname>',
+                       '<input checked="" type="radio" value="1" name="testname"/>',
                        Html::radio( 'testname', true ),
                        'Radio wrapper checked.'
                );
                $this->assertEquals(
-                       '<input type=radio value=testval name=testname>',
+                       '<input type="radio" value="testval" name="testname"/>',
                        Html::radio( 'testname', false, [ 'value' => 'testval' ] ),
                        'Radio wrapper with a value override.'
                );
@@ -758,7 +731,7 @@ class HtmlTest extends MediaWikiTestCase {
 
        public function testWrapperLabel() {
                $this->assertEquals(
-                       '<label for=testid>testlabel</label>',
+                       '<label for="testid">testlabel</label>',
                        Html::label( 'testlabel', 'testid' ),
                        'Label wrapper'
                );
index d701a81..6edf034 100644 (file)
@@ -13,7 +13,6 @@ class LinkerTest extends MediaWikiLangTestCase {
        public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                ] );
 
                $this->assertEquals( $expected,
@@ -112,7 +111,6 @@ class LinkerTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgScript' => '/wiki/index.php',
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                        'wgCapitalLinks' => true,
                        'wgConf' => $conf,
                ] );
@@ -277,7 +275,6 @@ class LinkerTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgScript' => '/wiki/index.php',
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                        'wgCapitalLinks' => true,
                        'wgConf' => $conf,
                ] );
@@ -367,7 +364,6 @@ class LinkerTest extends MediaWikiLangTestCase {
        public function testLinkBeginHook( $callback, $expected ) {
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                        'wgServer' => '//example.org',
                        'wgCanonicalServer' => 'http://example.org',
                        'wgScriptPath' => '/w',
@@ -414,7 +410,6 @@ class LinkerTest extends MediaWikiLangTestCase {
        public function testLinkEndHook( $callback, $expected ) {
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
-                       'wgWellFormedXml' => true,
                ] );
 
                $this->setMwGlobals( 'wgHooks', [ 'LinkEnd' => [ $callback ] ] );
index 467a2ad..51ef9d7 100644 (file)
@@ -243,6 +243,7 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
                        'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
                        'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
                        'GenderCache' => [ 'GenderCache', GenderCache::class ],
+                       'LinkCache' => [ 'LinkCache', LinkCache::class ],
                        '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
                        'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
                        'TitleParser' => [ 'TitleParser', TitleParser::class ],
index 8d4a347..9934749 100644 (file)
@@ -149,14 +149,14 @@ class OutputPageTest extends MediaWikiTestCase {
                        [
                                // Don't condition wrap raw modules (like the startup module)
                                [ 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ],
-                               '<script async src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
+                               '<script async="" src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
                        ],
                        // Load module styles only
                        // This also tests the order the modules are put into the url
                        [
                                [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
 
-                               '<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback">'
+                               '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
                        ],
                        // Load private module (only=scripts)
                        [
@@ -181,7 +181,7 @@ class OutputPageTest extends MediaWikiTestCase {
                        // noscript group
                        [
                                [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
-                               '<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"></noscript>'
+                               '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
                        ],
                        // Load two modules in separate groups
                        [
@@ -210,8 +210,6 @@ class OutputPageTest extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgResourceLoaderDebug' => false,
                        'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
-                       // Affects whether CDATA is inserted
-                       'wgWellFormedXml' => false,
                ] );
                $class = new ReflectionClass( 'OutputPage' );
                $method = $class->getMethod( 'makeResourceLoaderLink' );
index 0d10c1a..f80f512 100644 (file)
@@ -12,9 +12,6 @@ class XmlSelectTest extends MediaWikiTestCase {
 
        protected function setUp() {
                parent::setUp();
-               $this->setMwGlobals( [
-                       'wgWellFormedXml' => true,
-               ] );
                $this->select = new XmlSelect();
        }
 
index 00d429e..dbd1299 100644 (file)
@@ -30,7 +30,6 @@ class XmlTest extends MediaWikiTestCase {
 
                $this->setMwGlobals( [
                        'wgLang' => $langObj,
-                       'wgWellFormedXml' => true,
                        'wgUseMediaWikiUIEverywhere' => false,
                ] );
        }
index 91f27fb..545b964 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+use MediaWiki\MediaWikiServices;
 
 /**
  * @group ContentHandler
@@ -36,7 +37,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                MWNamespace::getCanonicalNamespaces( true );
                $wgContLang->resetNamespaces();
                // And LinkCache
-               LinkCache::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
        }
 
        protected function tearDown() {
@@ -46,7 +47,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                MWNamespace::getCanonicalNamespaces( true );
                $wgContLang->resetNamespaces();
                // And LinkCache
-               LinkCache::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
 
                parent::tearDown();
        }
index 8a48734..de8e371 100644 (file)
@@ -6,12 +6,6 @@
  */
 class JsonContentTest extends MediaWikiLangTestCase {
 
-       protected function setUp() {
-               parent::setUp();
-
-               $this->setMwGlobals( 'wgWellFormedXml', true );
-       }
-
        public static function provideValidConstruction() {
                return [
                        [ 'foo', false, null ],
diff --git a/tests/phpunit/includes/libs/XhprofDataTest.php b/tests/phpunit/includes/libs/XhprofDataTest.php
new file mode 100644 (file)
index 0000000..a0fb563
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @uses XhprofData
+ * @uses AutoLoader
+ * @author Bryan Davis <bd808@wikimedia.org>
+ * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
+ * @since 1.25
+ */
+class XhprofDataTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @covers XhprofData::splitKey
+        * @dataProvider provideSplitKey
+        */
+       public function testSplitKey( $key, $expect ) {
+               $this->assertSame( $expect, XhprofData::splitKey( $key ) );
+       }
+
+       public function provideSplitKey() {
+               return [
+                       [ 'main()', [ null, 'main()' ] ],
+                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
+                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
+                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
+                       [ '==>bar', [ '', 'bar' ] ],
+                       [ '', [ null, '' ] ],
+               ];
+       }
+
+       /**
+        * @covers XhprofData::pruneData
+        */
+       public function testInclude() {
+               $xhprofData = $this->getXhprofDataFixture( [
+                       'include' => [ 'main()' ],
+               ] );
+               $raw = $xhprofData->getRawData();
+               $this->assertArrayHasKey( 'main()', $raw );
+               $this->assertArrayHasKey( 'main()==>foo', $raw );
+               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
+               $this->assertSame( 3, count( $raw ) );
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getInclusiveMetrics
+        */
+       public function testInclusiveMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+               ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getInclusiveMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( $type === 'array' ) {
+                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
+                                       if ( $name === 'main()' ) {
+                                               $this->assertEquals( 100, $metric[$key]['percent'] );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getCompleteMetrics
+        */
+       public function testCompleteMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+                       'calls' => 'array',
+                       'subcalls' => 'array',
+               ];
+               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+                       'exclusive' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getCompleteMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric, $name );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( in_array( $key, $statsMetrics ) ) {
+                                       $this->assertArrayStructure(
+                                               $statStruct, $metric[$key], $key
+                                       );
+                                       $this->assertLessThanOrEqual(
+                                               $metric[$key]['total'], $metric[$key]['exclusive']
+                                       );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @covers XhprofData::getCallers
+        * @covers XhprofData::getCallees
+        * @uses XhprofData
+        */
+       public function testEdges() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $this->assertSame( [], $xhprofData->getCallers( 'main()' ) );
+               $this->assertSame( [ 'foo', 'xhprof_disable' ],
+                       $xhprofData->getCallees( 'main()' )
+               );
+               $this->assertSame( [ 'main()' ],
+                       $xhprofData->getCallers( 'foo' )
+               );
+               $this->assertSame( [], $xhprofData->getCallees( 'strlen' ) );
+       }
+
+       /**
+        * @covers XhprofData::getCriticalPath
+        * @uses XhprofData
+        */
+       public function testCriticalPath() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $path = $xhprofData->getCriticalPath();
+
+               $last = null;
+               foreach ( $path as $key => $value ) {
+                       list( $func, $call ) = XhprofData::splitKey( $key );
+                       $this->assertSame( $last, $func );
+                       $last = $call;
+               }
+               $this->assertSame( $last, 'bar@1' );
+       }
+
+       /**
+        * Get an Xhprof instance that has been primed with a set of known testing
+        * data. Tests for the Xhprof class should laregly be concerned with
+        * evaluating the manipulations of the data collected by xhprof rather
+        * than the data collection process itself.
+        *
+        * The returned Xhprof instance primed will be with a data set created by
+        * running this trivial program using the PECL xhprof implementation:
+        * @code
+        * function bar( $x ) {
+        *   if ( $x > 0 ) {
+        *     bar($x - 1);
+        *   }
+        * }
+        * function foo() {
+        *   for ( $idx = 0; $idx < 2; $idx++ ) {
+        *     bar( $idx );
+        *     $x = strlen( 'abc' );
+        *   }
+        * }
+        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
+        * foo();
+        * $x = xhprof_disable();
+        * var_export( $x );
+        * @endcode
+        *
+        * @return Xhprof
+        */
+       protected function getXhprofDataFixture( array $opts = [] ) {
+               return new XhprofData( [
+                       'foo==>bar' => [
+                               'ct' => 2,
+                               'wt' => 57,
+                               'cpu' => 92,
+                               'mu' => 1896,
+                               'pmu' => 0,
+                       ],
+                       'foo==>strlen' => [
+                               'ct' => 2,
+                               'wt' => 21,
+                               'cpu' => 141,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'bar==>bar@1' => [
+                               'ct' => 1,
+                               'wt' => 18,
+                               'cpu' => 19,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'main()==>foo' => [
+                               'ct' => 1,
+                               'wt' => 304,
+                               'cpu' => 307,
+                               'mu' => 4008,
+                               'pmu' => 0,
+                       ],
+                       'main()==>xhprof_disable' => [
+                               'ct' => 1,
+                               'wt' => 8,
+                               'cpu' => 10,
+                               'mu' => 768,
+                               'pmu' => 392,
+                       ],
+                       'main()' => [
+                               'ct' => 1,
+                               'wt' => 353,
+                               'cpu' => 351,
+                               'mu' => 6112,
+                               'pmu' => 1424,
+                       ],
+               ], $opts );
+       }
+
+       /**
+        * Assert that the given array has the described structure.
+        *
+        * @param array $struct Array of key => type mappings
+        * @param array $actual Array to check
+        * @param string $label
+        */
+       protected function assertArrayStructure( $struct, $actual, $label = null ) {
+               $this->assertInternalType( 'array', $actual, $label );
+               $this->assertCount( count( $struct ), $actual, $label );
+               foreach ( $struct as $key => $type ) {
+                       $this->assertArrayHasKey( $key, $actual );
+                       $this->assertInternalType( $type, $actual[$key] );
+               }
+       }
+}
index 22925bf..6748115 100644 (file)
  * @file
  */
 
-/**
- * @uses Xhprof
- * @uses AutoLoader
- * @author Bryan Davis <bd808@wikimedia.org>
- * @copyright © 2014 Bryan Davis and Wikimedia Foundation.
- * @since 1.25
- */
 class XhprofTest extends PHPUnit_Framework_TestCase {
-
-       public function setUp() {
-               if ( !function_exists( 'xhprof_enable' ) ) {
-                       $this->markTestSkipped( 'No xhprof support detected.' );
-               }
-       }
-
-       /**
-        * @covers Xhprof::splitKey
-        * @dataProvider provideSplitKey
-        */
-       public function testSplitKey( $key, $expect ) {
-               $this->assertSame( $expect, Xhprof::splitKey( $key ) );
-       }
-
-       public function provideSplitKey() {
-               return [
-                       [ 'main()', [ null, 'main()' ] ],
-                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
-                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
-                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
-                       [ '==>bar', [ '', 'bar' ] ],
-                       [ '', [ null, '' ] ],
-               ];
-       }
-
-       /**
-        * @covers Xhprof::__construct
-        * @covers Xhprof::stop
-        * @covers Xhprof::getRawData
-        * @dataProvider provideRawData
-        */
-       public function testRawData( $flags, $keys ) {
-               $xhprof = new Xhprof( [ 'flags' => $flags ] );
-               $raw = $xhprof->getRawData();
-               $this->assertArrayHasKey( 'main()', $raw );
-               foreach ( $keys as $key ) {
-                       $this->assertArrayHasKey( $key, $raw['main()'] );
-               }
-       }
-
-       public function provideRawData() {
-               $tests = [
-                       [ 0, [ 'ct', 'wt' ] ],
-               ];
-
-               if ( defined( 'XHPROF_FLAGS_CPU' ) && defined( 'XHPROF_FLAGS_CPU' ) ) {
-                       $tests[] = [ XHPROF_FLAGS_MEMORY, [
-                               'ct', 'wt', 'mu', 'pmu',
-                       ] ];
-                       $tests[] = [ XHPROF_FLAGS_CPU, [
-                               'ct', 'wt', 'cpu',
-                       ] ];
-                       $tests[] = [ XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU, [
-                                       'ct', 'wt', 'mu', 'pmu', 'cpu',
-                               ] ];
-               }
-
-               return $tests;
-       }
-
-       /**
-        * @covers Xhprof::pruneData
-        */
-       public function testInclude() {
-               $xhprof = $this->getXhprofFixture( [
-                       'include' => [ 'main()' ],
-               ] );
-               $raw = $xhprof->getRawData();
-               $this->assertArrayHasKey( 'main()', $raw );
-               $this->assertArrayHasKey( 'main()==>foo', $raw );
-               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
-               $this->assertSame( 3, count( $raw ) );
-       }
-
        /**
-        * Validate the structure of data returned by
-        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
+        * Trying to enable Xhprof when it is already enabled causes an exception
+        * to be thrown.
         *
-        * @covers Xhprof::getInclusiveMetrics
-        */
-       public function testInclusiveMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-               ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-               ];
-
-               $xhprof = $this->getXhprofFixture();
-               $metrics = $xhprof->getInclusiveMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( $type === 'array' ) {
-                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
-                                       if ( $name === 'main()' ) {
-                                               $this->assertEquals( 100, $metric[$key]['percent'] );
-                                       }
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Validate the structure of data returned by
-        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
-        *
-        * @covers Xhprof::getCompleteMetrics
-        */
-       public function testCompleteMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-                       'calls' => 'array',
-                       'subcalls' => 'array',
-               ];
-               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-                       'exclusive' => 'numeric',
-               ];
-
-               $xhprof = $this->getXhprofFixture();
-               $metrics = $xhprof->getCompleteMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric, $name );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( in_array( $key, $statsMetrics ) ) {
-                                       $this->assertArrayStructure(
-                                               $statStruct, $metric[$key], $key
-                                       );
-                                       $this->assertLessThanOrEqual(
-                                               $metric[$key]['total'], $metric[$key]['exclusive']
-                                       );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * @covers Xhprof::getCallers
-        * @covers Xhprof::getCallees
-        * @uses Xhprof
-        */
-       public function testEdges() {
-               $xhprof = $this->getXhprofFixture();
-               $this->assertSame( [], $xhprof->getCallers( 'main()' ) );
-               $this->assertSame( [ 'foo', 'xhprof_disable' ],
-                       $xhprof->getCallees( 'main()' )
-               );
-               $this->assertSame( [ 'main()' ],
-                       $xhprof->getCallers( 'foo' )
-               );
-               $this->assertSame( [], $xhprof->getCallees( 'strlen' ) );
-       }
-
-       /**
-        * @covers Xhprof::getCriticalPath
-        * @uses Xhprof
-        */
-       public function testCriticalPath() {
-               $xhprof = $this->getXhprofFixture();
-               $path = $xhprof->getCriticalPath();
-
-               $last = null;
-               foreach ( $path as $key => $value ) {
-                       list( $func, $call ) = Xhprof::splitKey( $key );
-                       $this->assertSame( $last, $func );
-                       $last = $call;
-               }
-               $this->assertSame( $last, 'bar@1' );
-       }
-
-       /**
-        * Get an Xhprof instance that has been primed with a set of known testing
-        * data. Tests for the Xhprof class should laregly be concerned with
-        * evaluating the manipulations of the data collected by xhprof rather
-        * than the data collection process itself.
-        *
-        * The returned Xhprof instance primed will be with a data set created by
-        * running this trivial program using the PECL xhprof implementation:
-        * @code
-        * function bar( $x ) {
-        *   if ( $x > 0 ) {
-        *     bar($x - 1);
-        *   }
-        * }
-        * function foo() {
-        *   for ( $idx = 0; $idx < 2; $idx++ ) {
-        *     bar( $idx );
-        *     $x = strlen( 'abc' );
-        *   }
-        * }
-        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
-        * foo();
-        * $x = xhprof_disable();
-        * var_export( $x );
-        * @endcode
-        *
-        * @return Xhprof
-        */
-       protected function getXhprofFixture( array $opts = [] ) {
-               $xhprof = new Xhprof( $opts );
-               $xhprof->loadRawData( [
-                       'foo==>bar' => [
-                               'ct' => 2,
-                               'wt' => 57,
-                               'cpu' => 92,
-                               'mu' => 1896,
-                               'pmu' => 0,
-                       ],
-                       'foo==>strlen' => [
-                               'ct' => 2,
-                               'wt' => 21,
-                               'cpu' => 141,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'bar==>bar@1' => [
-                               'ct' => 1,
-                               'wt' => 18,
-                               'cpu' => 19,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'main()==>foo' => [
-                               'ct' => 1,
-                               'wt' => 304,
-                               'cpu' => 307,
-                               'mu' => 4008,
-                               'pmu' => 0,
-                       ],
-                       'main()==>xhprof_disable' => [
-                               'ct' => 1,
-                               'wt' => 8,
-                               'cpu' => 10,
-                               'mu' => 768,
-                               'pmu' => 392,
-                       ],
-                       'main()' => [
-                               'ct' => 1,
-                               'wt' => 353,
-                               'cpu' => 351,
-                               'mu' => 6112,
-                               'pmu' => 1424,
-                       ],
-               ] );
-               return $xhprof;
-       }
-
-       /**
-        * Assert that the given array has the described structure.
-        *
-        * @param array $struct Array of key => type mappings
-        * @param array $actual Array to check
-        * @param string $label
-        */
-       protected function assertArrayStructure( $struct, $actual, $label = null ) {
-               $this->assertInternalType( 'array', $actual, $label );
-               $this->assertCount( count( $struct ), $actual, $label );
-               foreach ( $struct as $key => $type ) {
-                       $this->assertArrayHasKey( $key, $actual );
-                       $this->assertInternalType( $type, $actual[$key] );
-               }
+        * @expectedException        Exception
+        * @expectedExceptionMessage already enabled
+        * @covers Xhprof::enable
+        */
+       public function testEnable() {
+               $xhprof = new ReflectionClass( 'Xhprof' );
+               $enabled = $xhprof->getProperty( 'enabled' );
+               $enabled->setAccessible( true );
+               $enabled->setValue( true );
+               $xhprof->getMethod( 'enable' )->invoke( null );
        }
 }
diff --git a/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php b/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php
new file mode 100644 (file)
index 0000000..cf87a98
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * @group BagOStuff
+ */
+class RedisBagOStuffTest extends MediaWikiTestCase {
+       /** @var RedisBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->cache = TestingAccessWrapper::newFromObject( new RedisBagOStuff( [ 'servers' => [] ] ) );
+       }
+
+       /**
+        * @covers RedisBagOStuff::unserialize
+        * @dataProvider unserializeProvider
+        */
+       public function testUnserialize( $expected, $input, $message ) {
+               $actual = $this->cache->unserialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function unserializeProvider() {
+               return [
+                       [
+                               -1,
+                               '-1',
+                               'String representation of \'-1\'',
+                       ],
+                       [
+                               0,
+                               '0',
+                               'String representation of \'0\'',
+                       ],
+                       [
+                               1,
+                               '1',
+                               'String representation of \'1\'',
+                       ],
+                       [
+                               -1.0,
+                               'd:-1;',
+                               'Serialized negative double',
+                       ],
+                       [
+                               'foo',
+                               's:3:"foo";',
+                               'Serialized string',
+                       ]
+               ];
+       }
+
+       /**
+        * @covers RedisBagOStuff::serialize
+        * @dataProvider serializeProvider
+        */
+       public function testSerialize( $expected, $input, $message ) {
+               $actual = $this->cache->serialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function serializeProvider() {
+               return [
+                       [
+                               -1,
+                               -1,
+                               '-1 as integer',
+                       ],
+                       [
+                               0,
+                               0,
+                               '0 as integer',
+                       ],
+                       [
+                               1,
+                               1,
+                               '1 as integer',
+                       ],
+                       [
+                               'd:-1;',
+                               -1.0,
+                               'Negative double',
+                       ],
+                       [
+                               's:3:"2.1";',
+                               '2.1',
+                               'Decimal string',
+                       ],
+                       [
+                               's:1:"1";',
+                               '1',
+                               'String representation of 1',
+                       ],
+                       [
+                               's:3:"foo";',
+                               'foo',
+                               'String',
+                       ],
+               ];
+       }
+}
index 4c973e5..c024555 100644 (file)
@@ -100,7 +100,6 @@ class NewParserTest extends MediaWikiTestCase {
                $tmpGlobals['wgUseImageResize'] = true;
                $tmpGlobals['wgAllowExternalImages'] = true;
                $tmpGlobals['wgRawHtml'] = false;
-               $tmpGlobals['wgWellFormedXml'] = true;
                $tmpGlobals['wgExperimentalHtmlIds'] = false;
                $tmpGlobals['wgAdaptiveMessageCache'] = true;
                $tmpGlobals['wgUseDatabaseMessages'] = true;
@@ -150,7 +149,10 @@ class NewParserTest extends MediaWikiTestCase {
                $tmpGlobals['wgHooks'] = $tmpHooks;
                # add a namespace shadowing a interwiki link, to test
                # proper precedence when resolving links. (bug 51680)
-               $tmpGlobals['wgExtraNamespaces'] = [ 100 => 'MemoryAlpha' ];
+               $tmpGlobals['wgExtraNamespaces'] = [
+                       100 => 'MemoryAlpha',
+                       101 => 'MemoryAlpha_talk'
+               ];
 
                $tmpGlobals['wgLocalInterwikis'] = [ 'local', 'mi' ];
                # "extra language links"
@@ -433,7 +435,6 @@ class NewParserTest extends MediaWikiTestCase {
                        'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ],
                        'wgMaxTocLevel' => $maxtoclevel,
                        'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
-                       'wgWellFormedXml' => true,
                        'wgMathDirectory' => $uploadDir . '/math',
                        'wgDefaultLanguageVariant' => $variant,
                        'wgLinkHolderBatchSize' => $linkHolderBatchSize,
index 40065f5..e55a3a4 100644 (file)
@@ -191,7 +191,10 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                        // names ending in "a" to be female.
                        [ NS_USER, 'Lisa_Müller', '', '', 'de', 'Benutzerin:Lisa_Müller' ],
 
-                       [ NS_MAIN, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ]
+                       [ NS_MAIN, 'Remote_page', '', 'remotetestiw', 'en', 'remotetestiw:Remote_page' ],
+
+                       // non-existent namespace
+                       [ 10000000, 'Foobar', '', '', 'en', ':Foobar' ],
                ];
        }
 
index ee948bb..6ee0ff4 100644 (file)
        } );
 
        // HTML in wikitext
-       QUnit.test( 'HTML', 32, function ( assert ) {
+       QUnit.test( 'HTML', 33, function ( assert ) {
                mw.messages.set( 'jquerymsg-italics-msg', '<i>Very</i> important' );
 
                assertBothModes( assert, [ 'jquerymsg-italics-msg' ], mw.messages.get( 'jquerymsg-italics-msg' ), 'Simple italics unchanged' );
                        'Self-closing tags don\'t cause a parse error'
                );
 
+               mw.messages.set( 'jquerymsg-asciialphabetliteral-regression', '<b >>>="dir">asd</b>' );
+               assert.htmlEqual(
+                       formatParse( 'jquerymsg-asciialphabetliteral-regression' ),
+                       '<b>&gt;&gt;="dir"&gt;asd</b>',
+                       'Regression test for bad "asciiAlphabetLiteral" definition'
+               );
+
                mw.messages.set( 'jquerymsg-entities1', 'A&B' );
                mw.messages.set( 'jquerymsg-entities2', 'A&gt;B' );
                mw.messages.set( 'jquerymsg-entities3', 'A&rarr;B' );