Merge "Add equals() to UserIdentity"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 9 May 2018 17:28:00 +0000 (17:28 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 9 May 2018 17:28:00 +0000 (17:28 +0000)
86 files changed:
.gitignore
RELEASE-NOTES-1.32
autoload.php
includes/DefaultSettings.php
includes/Storage/MutableRevisionSlots.php
includes/Storage/RevisionSlots.php
includes/Storage/RevisionSlotsUpdate.php [new file with mode: 0644]
includes/Storage/SlotRecord.php
includes/api/ApiLogin.php
includes/installer/i18n/cs.json
includes/installer/i18n/nb.json
includes/parser/MWTidy.php
includes/preferences/DefaultPreferencesFactory.php
includes/search/SearchMySQL.php
includes/specials/SpecialBotPasswords.php
includes/specials/SpecialPreferences.php
includes/specials/formfields/Licenses.php
includes/specials/forms/PreferencesForm.php
includes/specials/forms/PreferencesFormLegacy.php [new file with mode: 0644]
includes/specials/forms/PreferencesFormOOUI.php [new file with mode: 0644]
includes/tidy/Balancer.php [deleted file]
includes/tidy/Html5Depurate.php [deleted file]
includes/tidy/Html5Internal.php [deleted file]
includes/user/BotPassword.php
languages/data/Names.php
languages/i18n/arq.json
languages/i18n/as.json
languages/i18n/bn.json
languages/i18n/cs.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/inh.json
languages/i18n/it.json
languages/i18n/lv.json
languages/i18n/nb.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/shy-latn.json
languages/messages/MessagesKo_kp.php
package.json
resources/Resources.php
resources/src/jquery/jquery.makeCollapsible.js
resources/src/mediawiki.legacy/oldshared.css
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
resources/src/mediawiki/mediawiki.js
tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php
tests/phpunit/includes/Storage/RevisionSlotsTest.php
tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/SlotRecordTest.php
tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php
tests/phpunit/includes/tidy/BalancerTest.php [deleted file]
tests/selenium/README.md
tests/selenium/pageobjects/createaccount.page.js
tests/selenium/pageobjects/delete.page.js
tests/selenium/pageobjects/edit.page.js
tests/selenium/pageobjects/history.page.js
tests/selenium/pageobjects/page.js
tests/selenium/pageobjects/preferences.page.js
tests/selenium/pageobjects/restore.page.js
tests/selenium/pageobjects/userlogin.page.js
tests/selenium/specs/page.js
tests/selenium/specs/user.js
tests/selenium/wdio-mediawiki/.eslintrc.json [new file with mode: 0644]
tests/selenium/wdio-mediawiki/Api.js [new file with mode: 0644]
tests/selenium/wdio-mediawiki/BlankPage.js [new file with mode: 0644]
tests/selenium/wdio-mediawiki/CHANGELOG.md [new file with mode: 0644]
tests/selenium/wdio-mediawiki/LICENSE [new file with mode: 0644]
tests/selenium/wdio-mediawiki/LoginPage.js [new file with mode: 0644]
tests/selenium/wdio-mediawiki/Page.js [new file with mode: 0644]
tests/selenium/wdio-mediawiki/README.md [new file with mode: 0644]
tests/selenium/wdio-mediawiki/index.js [new file with mode: 0644]
tests/selenium/wdio-mediawiki/package.json [new file with mode: 0644]
tests/selenium/wdio-mediawiki/specs/BlankPage.js [new file with mode: 0644]
tests/selenium/wdio.conf.js

index 0112cf3..d440e72 100644 (file)
@@ -49,6 +49,7 @@ sftp-config.json
 npm-debug.log
 node_modules/
 /tests/phpunit/phpunit.phar
+/tests/selenium/log
 
 # Composer
 /vendor
index b7500ae..9fd3161 100644 (file)
@@ -15,6 +15,8 @@ production.
   $wgJpegQuality (default 80). This aligns the quality to what ImageMagick uses.
 * $wgExperimentalHtmlIds, deprecated since 1.30, has been removed. The
   'html5-legacy' value for $wgFragmentMode is no longer accepted.
+* The experimental Html5Internal and Html5Depurate tidy drivers were removed.
+  RemexHtml, which is the default, should be used instead.
 
 === New features in 1.32 ===
 * (T112474) Generalized the ResourceLoader mechanism for overriding modules
@@ -76,6 +78,9 @@ because of Phabricator reports.
 * mw.util.updateTooltipAccessKeys(), deprecated in 1.24, was removed. Use
   jquery.accessKeyLabel instead.
 * The SqlDataUpdate class, deprecated in 1.28, has been removed.
+* The Html5Internal and Html5Depurate tidy driver classes were removed, along with the
+  Balancer tidy implementation. Both implementations were experimental, and were replaced
+  by RemexHtml.
 
 === Deprecations in 1.32 ===
 * Use of a StartProfiler.php file is deprecated in favour of placing
index e316bb5..ec0d59f 100644 (file)
@@ -965,19 +965,12 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Storage/RevisionLookup.php',
        'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Storage/RevisionRecord.php',
        'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Storage/RevisionSlots.php',
+       'MediaWiki\\Storage\\RevisionSlotsUpdate' => __DIR__ . '/includes/Storage/RevisionSlotsUpdate.php',
        'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Storage/RevisionStore.php',
        'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Storage/RevisionStoreRecord.php',
        'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Storage/SlotRecord.php',
        'MediaWiki\\Storage\\SqlBlobStore' => __DIR__ . '/includes/Storage/SqlBlobStore.php',
        'MediaWiki\\Storage\\SuppressedDataException' => __DIR__ . '/includes/Storage/SuppressedDataException.php',
-       'MediaWiki\\Tidy\\BalanceActiveFormattingElements' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\BalanceElement' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\BalanceMarker' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\BalanceSets' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\BalanceStack' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\Balancer' => __DIR__ . '/includes/tidy/Balancer.php',
-       'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php',
-       'MediaWiki\\Tidy\\Html5Internal' => __DIR__ . '/includes/tidy/Html5Internal.php',
        'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php',
        'MediaWiki\\Tidy\\RaggettExternal' => __DIR__ . '/includes/tidy/RaggettExternal.php',
        'MediaWiki\\Tidy\\RaggettInternalHHVM' => __DIR__ . '/includes/tidy/RaggettInternalHHVM.php',
@@ -1182,6 +1175,8 @@ $wgAutoloadLocalClasses = [
        'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php',
        'Preferences' => __DIR__ . '/includes/Preferences.php',
        'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesForm.php',
+       'PreferencesFormLegacy' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php',
+       'PreferencesFormOOUI' => __DIR__ . '/includes/specials/forms/PreferencesFormOOUI.php',
        'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php',
        'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php',
        'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php',
index 2dc43fe..0e98e33 100644 (file)
@@ -3237,6 +3237,14 @@ $wgHTMLFormAllowTableFormat = true;
  */
 $wgUseMediaWikiUIEverywhere = false;
 
+/**
+ * Temporary variable that determines whether the EditPage class should use OOjs UI or not.
+ * This will be removed later and OOjs UI will become the only option.
+ *
+ * @since 1.32
+ */
+$wgOOUIPreferences = false;
+
 /**
  * Whether to label the store-to-database-and-show-to-others button in the editor
  * as "Save page"/"Save changes" if false (the default) or, if true, instead as
@@ -4273,8 +4281,6 @@ $wgAllowImageTag = false;
  *    - RaggettInternalHHVM: Use the limited-functionality HHVM extension
  *    - RaggettInternalPHP: Use the PECL extension
  *    - RaggettExternal: Shell out to an external binary (tidyBin)
- *    - Html5Depurate: Use external Depurate service
- *    - Html5Internal: Use the Balancer library in PHP
  *    - RemexHtml: Use the RemexHtml library in PHP
  *
  *  - tidyConfigFile: Path to configuration file for any of the Raggett drivers
index 2e675c8..4cc3730 100644 (file)
@@ -102,36 +102,4 @@ class MutableRevisionSlots extends RevisionSlots {
                unset( $this->slots[$role] );
        }
 
-       /**
-        * Return all slots that are not inherited.
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @return SlotRecord[]
-        */
-       public function getTouchedSlots() {
-               return array_filter(
-                       $this->getSlots(),
-                       function ( SlotRecord $slot ) {
-                               return !$slot->isInherited();
-                       }
-               );
-       }
-
-       /**
-        * Return all slots that are inherited.
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @return SlotRecord[]
-        */
-       public function getInheritedSlots() {
-               return array_filter(
-                       $this->getSlots(),
-                       function ( SlotRecord $slot ) {
-                               return $slot->isInherited();
-                       }
-               );
-       }
-
 }
index 7fa5431..c7dcd13 100644 (file)
@@ -54,6 +54,8 @@ class RevisionSlots {
         * @param SlotRecord[] $slots
         */
        private function setSlotsInternal( array $slots ) {
+               Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
+
                $this->slots = [];
 
                // re-key the slot array
@@ -199,4 +201,71 @@ class RevisionSlots {
                }, null );
        }
 
+       /**
+        * Return all slots that are not inherited.
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @return SlotRecord[]
+        */
+       public function getTouchedSlots() {
+               return array_filter(
+                       $this->getSlots(),
+                       function ( SlotRecord $slot ) {
+                               return !$slot->isInherited();
+                       }
+               );
+       }
+
+       /**
+        * Return all slots that are inherited.
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @return SlotRecord[]
+        */
+       public function getInheritedSlots() {
+               return array_filter(
+                       $this->getSlots(),
+                       function ( SlotRecord $slot ) {
+                               return $slot->isInherited();
+                       }
+               );
+       }
+
+       /**
+        * Checks whether the other RevisionSlots instance has the same content
+        * as this instance. Note that this does not mean that the slots have to be the same:
+        * they could for instance belong to different revisions.
+        *
+        * @param RevisionSlots $other
+        *
+        * @return bool
+        */
+       public function hasSameContent( RevisionSlots $other ) {
+               if ( $other === $this ) {
+                       return true;
+               }
+
+               $aSlots = $this->getSlots();
+               $bSlots = $other->getSlots();
+
+               ksort( $aSlots );
+               ksort( $bSlots );
+
+               if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
+                       return false;
+               }
+
+               foreach ( $aSlots as $role => $s ) {
+                       $t = $bSlots[$role];
+
+                       if ( !$s->hasSameContent( $t ) ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
 }
diff --git a/includes/Storage/RevisionSlotsUpdate.php b/includes/Storage/RevisionSlotsUpdate.php
new file mode 100644 (file)
index 0000000..0eef90f
--- /dev/null
@@ -0,0 +1,242 @@
+<?php
+/**
+ * Value object representing a modification of revision slots.
+ *
+ * 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
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+
+/**
+ * Value object representing a modification of revision slots.
+ *
+ * @since 1.32
+ */
+class RevisionSlotsUpdate {
+
+       /**
+        * @var SlotRecord[] modified slots, using the slot role as the key.
+        */
+       private $modifiedSlots = [];
+
+       /**
+        * @var bool[] removed roles, stored in the keys of the array.
+        */
+       private $removedRoles = [];
+
+       /**
+        * Constructs a RevisionSlotsUpdate representing the update that turned $parentSlots
+        * into $newSlots. If $parentSlots is not given, $newSlots is assumed to come from a
+        * page's first revision.
+        *
+        * @param RevisionSlots $newSlots
+        * @param RevisionSlots|null $parentSlots
+        *
+        * @return RevisionSlotsUpdate
+        */
+       public static function newFromRevisionSlots(
+               RevisionSlots $newSlots,
+               RevisionSlots $parentSlots = null
+       ) {
+               $modified = $newSlots->getSlots();
+               $removed = [];
+
+               if ( $parentSlots ) {
+                       foreach ( $parentSlots->getSlots() as $role => $slot ) {
+                               if ( !isset( $modified[$role] ) ) {
+                                       $removed[] = $role;
+                               } elseif ( $slot->hasSameContent( $modified[$role] ) ) {
+                                       // Unset slots that had the same content in the parent revision from $modified.
+                                       unset( $modified[$role] );
+                               }
+                       }
+               }
+
+               return new RevisionSlotsUpdate( $modified, $removed );
+       }
+
+       /**
+        * @param SlotRecord[] $modifiedSlots
+        * @param string[] $removedRoles
+        */
+       public function __construct( array $modifiedSlots = [], array $removedRoles = [] ) {
+               foreach ( $modifiedSlots as $slot ) {
+                       $this->modifySlot( $slot );
+               }
+
+               foreach ( $removedRoles as $role ) {
+                       $this->removeSlot( $role );
+               }
+       }
+
+       /**
+        * Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),
+        * and not later removed by calling removeSlot().
+        *
+        * @return string[]
+        */
+       public function getModifiedRoles() {
+               return array_keys( $this->modifiedSlots );
+       }
+
+       /**
+        * Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),
+        * and not later re-introduced by calling modifySlot().
+        *
+        * @return string[]
+        */
+       public function getRemovedRoles() {
+               return array_keys( $this->removedRoles );
+       }
+
+       /**
+        * Returns a list of all slot roles that modified or removed.
+        *
+        * @return string[]
+        */
+       public function getTouchedRoles() {
+               return array_merge( $this->getModifiedRoles(), $this->getRemovedRoles() );
+       }
+
+       /**
+        * Sets the given slot to be modified.
+        * If a slot with the same role is already present, it is replaced.
+        *
+        * The roles used with modifySlot() will be returned from getModifiedRoles(),
+        * unless overwritten with removeSlot().
+        *
+        * @param SlotRecord $slot
+        */
+       public function modifySlot( SlotRecord $slot ) {
+               $role = $slot->getRole();
+
+               // XXX: We should perhaps require this to be an unsaved slot!
+               unset( $this->removedRoles[$role] );
+               $this->modifiedSlots[$role] = $slot;
+       }
+
+       /**
+        * Sets the content for the slot with the given role to be modified.
+        * If a slot with the same role is already present, it is replaced.
+        *
+        * @param string $role
+        * @param Content $content
+        */
+       public function modifyContent( $role, Content $content ) {
+               $slot = SlotRecord::newUnsaved( $role, $content );
+               $this->modifySlot( $slot );
+       }
+
+       /**
+        * Remove the slot for the given role, discontinue the corresponding stream.
+        *
+        * The roles used with removeSlot() will be returned from getRemovedSlots(),
+        * unless overwritten with modifySlot().
+        *
+        * @param string $role
+        */
+       public function removeSlot( $role ) {
+               unset( $this->modifiedSlots[$role] );
+               $this->removedRoles[$role] = true;
+       }
+
+       /**
+        * Returns the SlotRecord associated with the given role, if the slot with that role
+        * was modified (and not again removed).
+        *
+        * @note If the SlotRecord returned by this method returns a non-inherited slot,
+        *       the content of that slot may or may not already have PST applied. Methods
+        *       that take a RevisionSlotsUpdate as a parameter should specify whether they
+        *       expect PST to already have been applied to all slots. Inherited slots
+        *       should never have PST applied again.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @throws RevisionAccessException if the slot does not exist or was removed.
+        * @return SlotRecord
+        */
+       public function getModifiedSlot( $role ) {
+               if ( isset( $this->modifiedSlots[$role] ) ) {
+                       return $this->modifiedSlots[$role];
+               } else {
+                       throw new RevisionAccessException( 'No such slot: ' . $role );
+               }
+       }
+
+       /**
+        * Returns whether getModifiedSlot() will return a SlotRecord for the given role.
+        *
+        * Will return true for the role names returned by getModifiedRoles(), false otherwise.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @return bool
+        */
+       public function isModifiedSlot( $role ) {
+               return isset( $this->modifiedSlots[$role] );
+       }
+
+       /**
+        * Returns whether the given role is to be removed from the page.
+        *
+        * Will return true for the role names returned by getRemovedRoles(), false otherwise.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @return bool
+        */
+       public function isRemovedSlot( $role ) {
+               return isset( $this->removedRoles[$role] );
+       }
+
+       /**
+        * Returns true if $other represents the same update - that is,
+        * if all methods defined by RevisionSlotsUpdate when called on $this or $other
+        * will yield the same result when called with the same parameters.
+        *
+        * SlotRecords for the same role are compared based on their model and content.
+        *
+        * @param RevisionSlotsUpdate $other
+        * @return bool
+        */
+       public function hasSameUpdates( RevisionSlotsUpdate $other ) {
+               // NOTE: use != not !==, since the order of entries is not significant!
+
+               if ( $this->getModifiedRoles() != $other->getModifiedRoles() ) {
+                       return false;
+               }
+
+               if ( $this->getRemovedRoles() != $other->getRemovedRoles() ) {
+                       return false;
+               }
+
+               foreach ( $this->getModifiedRoles() as $role ) {
+                       $s = $this->getModifiedSlot( $role );
+                       $t = $other->getModifiedSlot( $role );
+
+                       if ( !$s->hasSameContent( $t ) ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+}
index 50d1100..9462518 100644 (file)
@@ -565,4 +565,50 @@ class SlotRecord {
                return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
        }
 
+       /**
+        * Returns true if $other has the same content as this slot.
+        * The check is performed based on the model, address size, and hash.
+        * Two slots can have the same content if they use different content addresses,
+        * but if they have the same address and the same model, they have the same content.
+        * Two slots can have the same content if they belong to different
+        * revisions or pages.
+        *
+        * Note that hasSameContent() may return false even if Content::equals returns true for
+        * the content of two slots. This may happen if the two slots have different serializations
+        * representing equivalent Content. Such false negatives are considered acceptable. Code
+        * that has to be absolutely sure the Content is really not the same if hasSameContent()
+        * returns false should call getContent() and compare the Content objects directly.
+        *
+        * @since 1.32
+        *
+        * @param SlotRecord $other
+        * @return bool
+        */
+       public function hasSameContent( SlotRecord $other ) {
+               if ( $other === $this ) {
+                       return true;
+               }
+
+               if ( $this->getModel() !== $other->getModel() ) {
+                       return false;
+               }
+
+               if ( $this->hasAddress()
+                       && $other->hasAddress()
+                       && $this->getAddress() == $other->getAddress()
+               ) {
+                       return true;
+               }
+
+               if ( $this->getSize() !== $other->getSize() ) {
+                       return false;
+               }
+
+               if ( $this->getSha1() !== $other->getSha1() ) {
+                       return false;
+               }
+
+               return true;
+       }
+
 }
index e4c4429..0248f25 100644 (file)
@@ -130,7 +130,10 @@ class ApiLogin extends ApiBase {
                                $session = $status->getValue();
                                $authRes = 'Success';
                                $loginType = 'BotPassword';
-                       } elseif ( !$botLoginData[2] || $status->hasMessage( 'login-throttled' ) ) {
+                       } elseif ( !$botLoginData[2] ||
+                               $status->hasMessage( 'login-throttled' ) ||
+                               $status->hasMessage( 'botpasswords-needs-reset' )
+                       ) {
                                $authRes = 'Failed';
                                $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authentication' )->info(
index ad2b910..8ab7abe 100644 (file)
        "config-nofile": "Soubor „$1“ nelze nalézt. Byl smazán?",
        "config-extension-link": "Věděli jste, že vaše wiki podporuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions rozšíření]?\n\nMůžete si prohlédnout [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category seznam rozšíření po kategoriích].",
        "config-skins-screenshots": "$1 (snímky obrazovky: $2)",
+       "config-extensions-requires": "$1 (vyžaduje $2)",
        "config-screenshot": "snímek obrazovky",
        "mainpagetext": "<strong>MediaWiki byla úspěšně nainstalována.</strong>",
        "mainpagedocfooter": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Uživatelská příručka] vám napoví, jak používat MediaWiki.\n\n== Začínáme ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Nastavení konfigurace]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Často kladené otázky o MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce E-mailová konference oznámení MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Překlad MediaWiki do vašeho jazyka]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Naučte se bojovat se spamem na vaší wiki]"
index 276d8a9..c00cf18 100644 (file)
        "config-nofile": "Filen \"$1\" ble ikke funnet. Kan den være blitt slettet?",
        "config-extension-link": "Visste du at wikien din kan brukes sammen med en mengde [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions utvidelser]?\n\nDu kan sjekke gjennom [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category utvidelser per kategori] eller [https://www.mediawiki.org/wiki/Extension_Matrix utvidelsesmatrisen] for å se den komplette listen av utvidelser.",
        "config-skins-screenshots": "$1 (skjermbilder: $2)",
+       "config-extensions-requires": "$1 (krever $2)",
        "config-screenshot": "skjermbilde",
        "mainpagetext": "<strong>MediaWiki har blitt installert.</strong>",
        "mainpagedocfooter": "Sjekk [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents brukerveiledningen] for å få informasjon om hvordan du bruker wiki-programvaren.\n\n==Hvordan komme igang==\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Innstillingsliste]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Ofte stilte spørsmål om MediaWiki]\n*[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-postliste]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Tilpass MediaWiki for ditt språk]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Lær deg å beskytte deg mot spam på wikien din]"
index 19cf573..5788986 100644 (file)
@@ -111,12 +111,6 @@ class MWTidy {
                        case 'RaggettExternal':
                                $instance = new MediaWiki\Tidy\RaggettExternal( $config );
                                break;
-                       case 'Html5Depurate':
-                               $instance = new MediaWiki\Tidy\Html5Depurate( $config );
-                               break;
-                       case 'Html5Internal':
-                               $instance = new MediaWiki\Tidy\Html5Internal( $config );
-                               break;
                        case 'RemexHtml':
                                $instance = new MediaWiki\Tidy\RemexDriver( $config );
                                break;
index 3bc21f7..2d7d73f 100644 (file)
@@ -42,13 +42,16 @@ use MessageLocalizer;
 use MWException;
 use MWNamespace;
 use MWTimestamp;
+use OutputPage;
 use Parser;
 use ParserOptions;
 use PreferencesForm;
+use PreferencesFormOOUI;
 use Psr\Log\LoggerAwareTrait;
 use Psr\Log\NullLogger;
 use Skin;
 use SpecialPage;
+use SpecialPreferences;
 use Status;
 use Title;
 use User;
@@ -127,6 +130,13 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        public function getFormDescriptor( User $user, IContextSource $context ) {
                $preferences = [];
 
+               if ( SpecialPreferences::isOouiEnabled( $context ) ) {
+                       OutputPage::setupOOUI(
+                               strtolower( $context->getSkin()->getSkinName() ),
+                               $context->getLanguage()->getDir()
+                       );
+               }
+
                $canIPUseHTTPS = wfCanIPUseHTTPS( $context->getRequest()->getIP() );
                $this->profilePreferences( $user, $context, $preferences, $canIPUseHTTPS );
                $this->skinPreferences( $user, $context, $preferences );
@@ -254,6 +264,8 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        protected function profilePreferences(
                User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS
        ) {
+               $oouiEnabled = SpecialPreferences::isOouiEnabled( $context );
+
                // retrieving user name for GENDER and misc.
                $userName = $user->getName();
 
@@ -365,13 +377,23 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
                        new PasswordAuthenticationRequest(), false )->isGood()
                ) {
-                       $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
-                               $context->msg( 'prefs-resetpass' )->text(), [],
-                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+                       if ( $oouiEnabled ) {
+                               $link = new \OOUI\ButtonWidget( [
+                                       'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
+                                               'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                       ] ),
+                                       'label' => $context->msg( 'prefs-resetpass' )->text(),
+                               ] );
+                       } else {
+                               $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
+                                       $context->msg( 'prefs-resetpass' )->text(), [],
+                                       [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+                       }
+
                        $defaultPreferences['password'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $link,
+                               'default' => (string)$link,
                                'label-message' => 'yourpassword',
                                'section' => 'personal/info',
                        ];
@@ -519,16 +541,28 @@ class DefaultPreferencesFactory implements PreferencesFactory {
 
                                $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
                                if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
-                                       $link = $this->linkRenderer->makeLink(
-                                               SpecialPage::getTitleFor( 'ChangeEmail' ),
-                                               $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
-                                               [],
-                                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
-                                       $emailAddress .= $emailAddress == '' ? $link : (
-                                               $context->msg( 'word-separator' )->escaped()
-                                               . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
-                                       );
+                                       if ( $oouiEnabled ) {
+                                               $link = new \OOUI\ButtonWidget( [
+                                                       'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
+                                                               'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                                       ] ),
+                                                       'label' =>
+                                                               $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+                                               ] );
+
+                                               $emailAddress .= $emailAddress == '' ? $link : ( '<br />' . $link );
+                                       } else {
+                                               $link = $this->linkRenderer->makeLink(
+                                                       SpecialPage::getTitleFor( 'ChangeEmail' ),
+                                                       $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+                                                       [],
+                                                       [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+
+                                               $emailAddress .= $emailAddress == '' ? $link : (
+                                                       $context->msg( 'word-separator' )->escaped()
+                                                       . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
+                                               );
+                                       }
                                }
 
                                $defaultPreferences['emailaddress'] = [
@@ -562,11 +596,19 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                                                $emailauthenticationclass = 'mw-email-authenticated';
                                        } else {
                                                $disableEmailPrefs = true;
-                                               $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
-                                                       $this->linkRenderer->makeKnownLink(
-                                                               SpecialPage::getTitleFor( 'Confirmemail' ),
-                                                               $context->msg( 'emailconfirmlink' )->text()
-                                                       ) . '<br />';
+                                               if ( $oouiEnabled ) {
+                                                       $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
+                                                               new \OOUI\ButtonWidget( [
+                                                                       'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
+                                                                       'label' => $context->msg( 'emailconfirmlink' )->text(),
+                                                               ] );
+                                               } else {
+                                                       $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
+                                                               $this->linkRenderer->makeKnownLink(
+                                                                       SpecialPage::getTitleFor( 'Confirmemail' ),
+                                                                       $context->msg( 'emailconfirmlink' )->text()
+                                                               ) . '<br />';
+                                               }
                                                $emailauthenticationclass = "mw-email-not-authenticated";
                                        }
                                } else {
@@ -810,6 +852,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                        'default' => $tzSetting,
                        'size' => 20,
                        'section' => 'rendering/timeoffset',
+                       'id' => 'wpTimeCorrection',
                ];
        }
 
@@ -1048,28 +1091,44 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        protected function watchlistPreferences(
                User $user, IContextSource $context, &$defaultPreferences
        ) {
+               $oouiEnabled = SpecialPreferences::isOouiEnabled( $context );
+
                $watchlistdaysMax = ceil( $this->config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
 
                # # Watchlist #####################################
                if ( $user->isAllowed( 'editmywatchlist' ) ) {
-                       $editWatchlistLinks = [];
+                       $editWatchlistLinks = '';
+                       $editWatchlistLinksOld = [];
                        $editWatchlistModes = [
-                               'edit' => [ 'EditWatchlist', false ],
-                               'raw' => [ 'EditWatchlist', 'raw' ],
-                               'clear' => [ 'EditWatchlist', 'clear' ],
+                               'edit' => [ 'subpage' => false, 'flags' => [] ],
+                               'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
+                               'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
                        ];
-                       foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
+                       foreach ( $editWatchlistModes as $mode => $options ) {
                                // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
-                               $editWatchlistLinks[] = $this->linkRenderer->makeKnownLink(
-                                       SpecialPage::getTitleFor( $mode[0], $mode[1] ),
-                                       new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
-                               );
+                               if ( $oouiEnabled ) {
+                                       $editWatchlistLinks .=
+                                               new \OOUI\ButtonWidget( [
+                                                       'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
+                                                       'flags' => $options[ 'flags' ],
+                                                       'label' => new \OOUI\HtmlSnippet(
+                                                               $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
+                                                       ),
+                                               ] );
+                               } else {
+                                       $editWatchlistLinksOld[] = $this->linkRenderer->makeKnownLink(
+                                               SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] ),
+                                               new HtmlArmor( $context->msg( "prefs-editwatchlist-{$mode}" )->parse() )
+                                       );
+                               }
                        }
 
                        $defaultPreferences['editwatchlist'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
+                               'default' => $oouiEnabled ?
+                                       $editWatchlistLinks :
+                                       $context->getLanguage()->pipeList( $editWatchlistLinksOld ),
                                'label-message' => 'prefs-editwatchlist-label',
                                'section' => 'watchlist/editwatchlist',
                        ];
@@ -1191,13 +1250,31 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                $defaultPreferences['watchlisttoken'] = [
                        'type' => 'api',
                ];
-               $defaultPreferences['watchlisttoken-info'] = [
-                       'type' => 'info',
-                       'section' => 'watchlist/tokenwatchlist',
-                       'label-message' => 'prefs-watchlist-token',
-                       'default' => $user->getTokenFromOption( 'watchlisttoken' ),
-                       'help-message' => 'prefs-help-watchlist-token2',
-               ];
+
+               if ( $oouiEnabled ) {
+                       $tokenButton = new \OOUI\ButtonWidget( [
+                               'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
+                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                               ] ),
+                               'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
+                       ] );
+                       $defaultPreferences['watchlisttoken-info'] = [
+                               'type' => 'info',
+                               'section' => 'watchlist/tokenwatchlist',
+                               'label-message' => 'prefs-watchlist-token',
+                               'help-message' => 'prefs-help-tokenmanagement',
+                               'raw' => true,
+                               'default' => (string)$tokenButton,
+                       ];
+               } else {
+                       $defaultPreferences['watchlisttoken-info'] = [
+                               'type' => 'info',
+                               'section' => 'watchlist/tokenwatchlist',
+                               'label-message' => 'prefs-watchlist-token',
+                               'default' => $user->getTokenFromOption( 'watchlisttoken' ),
+                               'help-message' => 'prefs-help-watchlist-token2',
+                       ];
+               }
        }
 
        /**
@@ -1406,14 +1483,19 @@ class DefaultPreferencesFactory implements PreferencesFactory {
         * @param IContextSource $context
         * @param string $formClass
         * @param array $remove Array of items to remove
-        * @return PreferencesForm|HTMLForm
+        * @return PreferencesForm
         */
        public function getForm(
                User $user,
                IContextSource $context,
-               $formClass = PreferencesForm::class,
+               $formClass = PreferencesFormOOUI::class,
                array $remove = []
        ) {
+               if ( SpecialPreferences::isOouiEnabled( $context ) ) {
+                       // We use ButtonWidgets in some of the getPreferences() functions
+                       $context->getOutput()->enableOOUI();
+               }
+
                $formDescriptor = $this->getFormDescriptor( $user, $context );
                if ( count( $remove ) ) {
                        $removeKeys = array_flip( $remove );
@@ -1642,17 +1724,25 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                $res = $this->saveFormData( $formData, $form );
 
                if ( $res ) {
+                       $context = $form->getContext();
+
                        $urlOptions = [];
 
                        if ( $res === 'eauth' ) {
                                $urlOptions['eauth'] = 1;
                        }
 
+                       if (
+                               $context->getRequest()->getFuzzyBool( 'ooui' ) !==
+                               $context->getConfig()->get( 'OOUIPreferences' )
+                       ) {
+                               $urlOptions[ 'ooui' ] = $context->getRequest()->getFuzzyBool( 'ooui' ) ? 1 : 0;
+                       }
+
                        $urlOptions += $form->getExtraSuccessRedirectParameters();
 
                        $url = $form->getTitle()->getFullURL( $urlOptions );
 
-                       $context = $form->getContext();
                        // Set session data for the success message
                        $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
 
index 8e705c1..c98f7e3 100644 (file)
@@ -34,13 +34,13 @@ class SearchMySQL extends SearchDatabase {
        private static $mMinSearchLength;
 
        /**
-        * Parse the user's query and transform it into an SQL fragment which will
-        * become part of a WHERE clause
+        * Parse the user's query and transform it into two SQL fragments:
+        * a WHERE condition and an ORDER BY expression
         *
         * @param string $filteredText
         * @param string $fulltext
         *
-        * @return string
+        * @return array
         */
        function parseQuery( $filteredText, $fulltext ) {
                global $wgContLang;
@@ -127,7 +127,10 @@ class SearchMySQL extends SearchDatabase {
 
                $searchon = $this->db->addQuotes( $searchon );
                $field = $this->getIndexField( $fulltext );
-               return " MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ";
+               return [
+                       " MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ",
+                       " MATCH($field) AGAINST($searchon IN NATURAL LANGUAGE MODE) DESC "
+               ];
        }
 
        function regexTerm( $string, $wildcard ) {
@@ -303,7 +306,8 @@ class SearchMySQL extends SearchDatabase {
                $query['fields'][] = 'page_namespace';
                $query['fields'][] = 'page_title';
                $query['conds'][] = 'page_id=si_page';
-               $query['conds'][] = $match;
+               $query['conds'][] = $match[0];
+               $query['options']['ORDER BY'] = $match[1];
        }
 
        /**
@@ -318,7 +322,7 @@ class SearchMySQL extends SearchDatabase {
                $query = [
                        'tables' => [ 'page', 'searchindex' ],
                        'fields' => [ 'COUNT(*) as c' ],
-                       'conds' => [ 'page_id=si_page', $match ],
+                       'conds' => [ 'page_id=si_page', $match[0] ],
                        'options' => [],
                        'joins' => [],
                ];
index f76c318..7b2d1bc 100644 (file)
@@ -107,6 +107,9 @@ class SpecialBotPasswords extends FormSpecialPage {
                                        'type' => 'check',
                                        'label-message' => 'botpasswords-label-resetpassword',
                                ];
+                               if ( $this->botPassword->isInvalid() ) {
+                                       $fields['resetPassword']['default'] = true;
+                               }
                        }
 
                        $lang = $this->getLanguage();
@@ -153,22 +156,39 @@ class SpecialBotPasswords extends FormSpecialPage {
 
                } else {
                        $linkRenderer = $this->getLinkRenderer();
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( $this->getConfig() );
+
                        $dbr = BotPassword::getDB( DB_REPLICA );
                        $res = $dbr->select(
                                'bot_passwords',
-                               [ 'bp_app_id' ],
+                               [ 'bp_app_id', 'bp_password' ],
                                [ 'bp_user' => $this->userId ],
                                __METHOD__
                        );
                        foreach ( $res as $row ) {
+                               try {
+                                       $password = $passwordFactory->newFromCiphertext( $row->bp_password );
+                                       $passwordInvalid = $password instanceof InvalidPassword;
+                                       unset( $password );
+                               } catch ( PasswordError $ex ) {
+                                       $passwordInvalid = true;
+                               }
+
+                               $text = $linkRenderer->makeKnownLink(
+                                       $this->getPageTitle( $row->bp_app_id ),
+                                       $row->bp_app_id
+                               );
+                               if ( $passwordInvalid ) {
+                                       $text .= $this->msg( 'word-separator' )->escaped()
+                                               . $this->msg( 'botpasswords-label-needsreset' )->parse();
+                               }
+
                                $fields[] = [
                                        'section' => 'existing',
                                        'type' => 'info',
                                        'raw' => true,
-                                       'default' => $linkRenderer->makeKnownLink(
-                                               $this->getPageTitle( $row->bp_app_id ),
-                                               $row->bp_app_id
-                                       ),
+                                       'default' => $text,
                                ];
                        }
 
index a5c24e7..f67fe9f 100644 (file)
@@ -29,8 +29,26 @@ use MediaWiki\MediaWikiServices;
  * @ingroup SpecialPage
  */
 class SpecialPreferences extends SpecialPage {
+       /**
+        * @var bool Whether OOUI should be enabled here
+        */
+       private $oouiEnabled = false;
+
        function __construct() {
                parent::__construct( 'Preferences' );
+
+               $this->oouiEnabled = self::isOouiEnabled( $this->getContext() );
+       }
+
+       /**
+        * Check if OOUI mode is enabled, by config or query string
+        * @param IContextSource $context The context.
+        * @return bool
+        */
+       public static function isOouiEnabled( IContextSource $context ) {
+               return $context->getRequest()->getFuzzyBool( 'ooui',
+                       $context->getConfig()->get( 'OOUIPreferences' )
+               );
        }
 
        public function doesWrites() {
@@ -52,8 +70,13 @@ class SpecialPreferences extends SpecialPage {
                        return;
                }
 
-               $out->addModules( 'mediawiki.special.preferences' );
-               $out->addModuleStyles( 'mediawiki.special.preferences.styles' );
+               if ( $this->oouiEnabled ) {
+                       $out->addModules( 'mediawiki.special.preferences.ooui' );
+                       $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' );
+               } else {
+                       $out->addModules( 'mediawiki.special.preferences' );
+                       $out->addModuleStyles( 'mediawiki.special.preferences.styles' );
+               }
 
                $session = $this->getRequest()->getSession();
                if ( $session->get( 'specialPreferencesSaveSuccess' ) ) {
@@ -86,35 +109,53 @@ class SpecialPreferences extends SpecialPage {
                $htmlForm = $this->getFormObject( $user, $this->getContext() );
                $sectionTitles = $htmlForm->getPreferenceSections();
 
-               $prefTabs = '';
-               foreach ( $sectionTitles as $key ) {
-                       $prefTabs .= Html::rawElement( 'li',
-                               [
-                                       'role' => 'presentation',
-                                       'class' => ( $key === 'personal' ) ? 'selected' : null
-                               ],
-                               Html::rawElement( 'a',
+               if ( $this->oouiEnabled ) {
+                       $prefTabs = [];
+                       foreach ( $sectionTitles as $key ) {
+                               $prefTabs[] = [
+                                       'name' => $key,
+                                       'label' => $htmlForm->getLegend( $key ),
+                               ];
+                       }
+                       $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs );
+
+                       // TODO: Render fake tabs here to avoid FOUC.
+                       // $out->addHTML( $fakeTabs );
+               } else {
+
+                       $prefTabs = '';
+                       foreach ( $sectionTitles as $key ) {
+                               $prefTabs .= Html::rawElement( 'li',
                                        [
-                                               'id' => 'preftab-' . $key,
-                                               'role' => 'tab',
-                                               'href' => '#mw-prefsection-' . $key,
-                                               'aria-controls' => 'mw-prefsection-' . $key,
-                                               'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false',
-                                               'tabIndex' => ( $key === 'personal' ) ? 0 : -1,
+                                               'role' => 'presentation',
+                                               'class' => ( $key === 'personal' ) ? 'selected' : null
                                        ],
-                                       $htmlForm->getLegend( $key )
-                               )
+                                       Html::rawElement( 'a',
+                                               [
+                                                       'id' => 'preftab-' . $key,
+                                                       'role' => 'tab',
+                                                       'href' => '#mw-prefsection-' . $key,
+                                                       'aria-controls' => 'mw-prefsection-' . $key,
+                                                       'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false',
+                                                       'tabIndex' => ( $key === 'personal' ) ? 0 : -1,
+                                               ],
+                                               $htmlForm->getLegend( $key )
+                                       )
+                               );
+                       }
+
+                       $out->addHTML(
+                               Html::rawElement( 'ul',
+                                       [
+                                               'id' => 'preftoc',
+                                               'role' => 'tablist'
+                                       ],
+                                       $prefTabs )
                        );
                }
 
-               $out->addHTML(
-                       Html::rawElement( 'ul',
-                               [
-                                       'id' => 'preftoc',
-                                       'role' => 'tablist'
-                               ],
-                               $prefTabs )
-               );
+               $htmlForm->addHiddenField( 'ooui', $this->oouiEnabled ? '1' : '0' );
+
                $htmlForm->show();
        }
 
@@ -126,7 +167,11 @@ class SpecialPreferences extends SpecialPage {
         */
        protected function getFormObject( $user, IContextSource $context ) {
                $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
-               $form = $preferencesFactory->getForm( $user, $context );
+               if ( $this->oouiEnabled ) {
+                       $form = $preferencesFactory->getForm( $user, $context, PreferencesFormOOUI::class );
+               } else {
+                       $form = $preferencesFactory->getForm( $user, $context, PreferencesFormLegacy::class );
+               }
                return $form;
        }
 
@@ -139,7 +184,9 @@ class SpecialPreferences extends SpecialPage {
 
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage
-               $htmlForm = new HTMLForm( [], $context, 'prefs-restore' );
+               $htmlForm = HTMLForm::factory(
+                       $this->oouiEnabled ? 'ooui' : 'vform', [], $context, 'prefs-restore'
+               );
 
                $htmlForm->setSubmitTextMsg( 'restoreprefs' );
                $htmlForm->setSubmitDestructive();
index 931cd24..a2f3128 100644 (file)
@@ -57,9 +57,25 @@ class Licenses extends HTMLFormField {
         * @return string
         */
        protected static function getMessageFromParams( $params ) {
-               return empty( $params['licenses'] )
-                       ? wfMessage( 'licenses' )->inContentLanguage()->plain()
-                       : $params['licenses'];
+               global $wgContLang;
+
+               if ( !empty( $params['licenses'] ) ) {
+                       return $params['licenses'];
+               }
+
+               // If the licenses page is in $wgForceUIMsgAsContentMsg (which is the case
+               // on Commons), translations will be in the database, in subpages of this
+               // message (e.g. MediaWiki:Licenses/<lang>)
+               // If there is no such translation, the result will be '-' (the empty default
+               // in the i18n files), so we'll need to force it to look up the actual licenses
+               // in the default site language (= get the translation from MediaWiki:Licenses)
+               // Also see https://phabricator.wikimedia.org/T3495
+               $defaultMsg = wfMessage( 'licenses' )->inContentLanguage();
+               if ( !$defaultMsg->exists() || $defaultMsg->plain() === '-' ) {
+                       $defaultMsg = wfMessage( 'licenses' )->inLanguage( $wgContLang );
+               }
+
+               return $defaultMsg->plain();
        }
 
        /**
index d4e5ef4..a124410 100644 (file)
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
- * Form to edit user preferences.
+ * Temporarily define PreferencesForm as an interface, so PreferencesFormOOUI
+ * and PreferencesFormLegacy can implement it.
+ *
+ * When PreferencesFormLegacy we can merge PreferencesFormOOUI with PreferencesForm.
  */
-class PreferencesForm extends HTMLForm {
-       // Override default value from HTMLForm
-       protected $mSubSectionBeforeFields = false;
-
-       private $modifiedUser;
-
-       /**
-        * @param User $user
-        */
-       public function setModifiedUser( $user ) {
-               $this->modifiedUser = $user;
-       }
-
-       /**
-        * @return User
-        */
-       public function getModifiedUser() {
-               if ( $this->modifiedUser === null ) {
-                       return $this->getUser();
-               } else {
-                       return $this->modifiedUser;
-               }
-       }
-
-       /**
-        * Get extra parameters for the query string when redirecting after
-        * successful save.
-        *
-        * @return array
-        */
-       public function getExtraSuccessRedirectParameters() {
-               return [];
-       }
-
-       /**
-        * @param string $html
-        * @return string
-        */
-       function wrapForm( $html ) {
-               $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
-
-               return parent::wrapForm( $html );
-       }
-
-       /**
-        * @return string
-        */
-       function getButtons() {
-               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
-
-               if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
-                       return '';
-               }
-
-               $html = parent::getButtons();
-
-               if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
-                       $t = $this->getTitle()->getSubpage( 'reset' );
-
-                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-                       $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(),
-                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
-
-                       $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
-               }
-
-               return $html;
-       }
-
-       /**
-        * Separate multi-option preferences into multiple preferences, since we
-        * have to store them separately
-        * @param array $data
-        * @return array
-        */
-       function filterDataForSubmit( $data ) {
-               foreach ( $this->mFlatFields as $fieldname => $field ) {
-                       if ( $field instanceof HTMLNestedFilterable ) {
-                               $info = $field->mParams;
-                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
-                               foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
-                                       $data["$prefix$key"] = $value;
-                               }
-                               unset( $data[$fieldname] );
-                       }
-               }
-
-               return $data;
-       }
-
-       /**
-        * Get the whole body of the form.
-        * @return string
-        */
-       function getBody() {
-               return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
-       }
-
-       /**
-        * Get the "<legend>" for a given section key. Normally this is the
-        * prefs-$key message but we'll allow extensions to override it.
-        * @param string $key
-        * @return string
-        */
-       function getLegend( $key ) {
-               $legend = parent::getLegend( $key );
-               Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
-               return $legend;
-       }
-
-       /**
-        * Get the keys of each top level preference section.
-        * @return array of section keys
-        */
-       function getPreferenceSections() {
-               return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
-       }
+interface PreferencesForm {
 }
diff --git a/includes/specials/forms/PreferencesFormLegacy.php b/includes/specials/forms/PreferencesFormLegacy.php
new file mode 100644 (file)
index 0000000..e6bc494
--- /dev/null
@@ -0,0 +1,143 @@
+<?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 MediaWiki\MediaWikiServices;
+
+/**
+ * Form to edit user preferences.
+ */
+class PreferencesFormLegacy extends HTMLForm implements PreferencesForm {
+       // Override default value from HTMLForm
+       protected $mSubSectionBeforeFields = false;
+
+       private $modifiedUser;
+
+       /**
+        * @param User $user
+        */
+       public function setModifiedUser( $user ) {
+               $this->modifiedUser = $user;
+       }
+
+       /**
+        * @return User
+        */
+       public function getModifiedUser() {
+               if ( $this->modifiedUser === null ) {
+                       return $this->getUser();
+               } else {
+                       return $this->modifiedUser;
+               }
+       }
+
+       /**
+        * Get extra parameters for the query string when redirecting after
+        * successful save.
+        *
+        * @return array
+        */
+       public function getExtraSuccessRedirectParameters() {
+               return [];
+       }
+
+       /**
+        * @param string $html
+        * @return string
+        */
+       function wrapForm( $html ) {
+               $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
+
+               return parent::wrapForm( $html );
+       }
+
+       /**
+        * @return string
+        */
+       function getButtons() {
+               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
+
+               if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+                       return '';
+               }
+
+               $html = parent::getButtons();
+
+               if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
+                       $t = $this->getTitle()->getSubpage( 'reset' );
+
+                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+                       $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(),
+                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
+
+                       $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
+               }
+
+               return $html;
+       }
+
+       /**
+        * Separate multi-option preferences into multiple preferences, since we
+        * have to store them separately
+        * @param array $data
+        * @return array
+        */
+       function filterDataForSubmit( $data ) {
+               foreach ( $this->mFlatFields as $fieldname => $field ) {
+                       if ( $field instanceof HTMLNestedFilterable ) {
+                               $info = $field->mParams;
+                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
+                               foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
+                                       $data["$prefix$key"] = $value;
+                               }
+                               unset( $data[$fieldname] );
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Get the whole body of the form.
+        * @return string
+        */
+       function getBody() {
+               return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
+       }
+
+       /**
+        * Get the "<legend>" for a given section key. Normally this is the
+        * prefs-$key message but we'll allow extensions to override it.
+        * @param string $key
+        * @return string
+        */
+       function getLegend( $key ) {
+               $legend = parent::getLegend( $key );
+               Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
+               return $legend;
+       }
+
+       /**
+        * Get the keys of each top level preference section.
+        * @return array of section keys
+        */
+       function getPreferenceSections() {
+               return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
+       }
+}
diff --git a/includes/specials/forms/PreferencesFormOOUI.php b/includes/specials/forms/PreferencesFormOOUI.php
new file mode 100644 (file)
index 0000000..a781254
--- /dev/null
@@ -0,0 +1,144 @@
+<?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
+ */
+
+/**
+ * Form to edit user preferences.
+ */
+class PreferencesFormOOUI extends OOUIHTMLForm implements PreferencesForm {
+       // Override default value from HTMLForm
+       protected $mSubSectionBeforeFields = false;
+
+       private $modifiedUser;
+
+       /**
+        * @param User $user
+        */
+       public function setModifiedUser( $user ) {
+               $this->modifiedUser = $user;
+       }
+
+       /**
+        * @return User
+        */
+       public function getModifiedUser() {
+               if ( $this->modifiedUser === null ) {
+                       return $this->getUser();
+               } else {
+                       return $this->modifiedUser;
+               }
+       }
+
+       /**
+        * Get extra parameters for the query string when redirecting after
+        * successful save.
+        *
+        * @return array
+        */
+       public function getExtraSuccessRedirectParameters() {
+               return [];
+       }
+
+       /**
+        * @param string $html
+        * @return string
+        */
+       function wrapForm( $html ) {
+               $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
+
+               return parent::wrapForm( $html );
+       }
+
+       /**
+        * @return string
+        */
+       function getButtons() {
+               if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+                       return '';
+               }
+
+               $html = parent::getButtons();
+
+               if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
+                       $t = $this->getTitle()->getSubpage( 'reset' );
+
+                       $html .= new OOUI\ButtonWidget( [
+                               'infusable' => true,
+                               'id' => 'mw-prefs-restoreprefs',
+                               'label' => $this->msg( 'restoreprefs' )->text(),
+                               'href' => $t->getLinkURL(),
+                               'flags' => [ 'destructive' ],
+                               'framed' => false,
+                       ] );
+
+                       $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
+               }
+
+               return $html;
+       }
+
+       /**
+        * Separate multi-option preferences into multiple preferences, since we
+        * have to store them separately
+        * @param array $data
+        * @return array
+        */
+       function filterDataForSubmit( $data ) {
+               foreach ( $this->mFlatFields as $fieldname => $field ) {
+                       if ( $field instanceof HTMLNestedFilterable ) {
+                               $info = $field->mParams;
+                               $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
+                               foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
+                                       $data["$prefix$key"] = $value;
+                               }
+                               unset( $data[$fieldname] );
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Get the whole body of the form.
+        * @return string
+        */
+       function getBody() {
+               return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
+       }
+
+       /**
+        * Get the "<legend>" for a given section key. Normally this is the
+        * prefs-$key message but we'll allow extensions to override it.
+        * @param string $key
+        * @return string
+        */
+       function getLegend( $key ) {
+               $legend = parent::getLegend( $key );
+               Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
+               return $legend;
+       }
+
+       /**
+        * Get the keys of each top level preference section.
+        * @return array of section keys
+        */
+       function getPreferenceSections() {
+               return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
+       }
+}
diff --git a/includes/tidy/Balancer.php b/includes/tidy/Balancer.php
deleted file mode 100644 (file)
index 6671f49..0000000
+++ /dev/null
@@ -1,3584 +0,0 @@
-<?php
-/**
- * An implementation of the tree building portion of the HTML5 parsing
- * spec.
- *
- * 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
- * @ingroup Parser
- * @since 1.27
- * @author C. Scott Ananian, 2016
- */
-
-namespace MediaWiki\Tidy;
-
-use ExplodeIterator;
-use IteratorAggregate;
-use ReverseArrayIterator;
-use Sanitizer;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Assert\ParameterAssertionException;
-
-// A note for future librarization[1] -- this file is a good candidate
-// for splitting into an independent library, except that it is currently
-// highly optimized for MediaWiki use.  It only implements the portions
-// of the HTML5 tree builder used by tags supported by MediaWiki, and
-// does not contain a true tokenizer pass, instead relying on
-// comment stripping, attribute normalization, and escaping done by
-// the MediaWiki Sanitizer.  It also deliberately avoids building
-// a true DOM in memory, instead serializing elements to an output string
-// as soon as possible (usually as soon as the tag is closed) to reduce
-// its memory footprint.
-
-// We've been gradually lifting some of these restrictions to handle
-// non-sanitized output generated by extensions, but we shortcut the tokenizer
-// for speed (primarily by splitting on `<`) and so rely on syntactic
-// well-formedness.
-
-// On the other hand, I've been pretty careful to note with comments in the
-// code the places where this implementation omits features of the spec or
-// depends on the MediaWiki Sanitizer.  Perhaps in the future we'll want to
-// implement the missing pieces and make this a standalone PHP HTML5 parser.
-// In order to do so, some sort of MediaWiki-specific API will need
-// to be added to (a) allow the Balancer to bypass the tokenizer,
-// and (b) support on-the-fly flattening instead of DOM node creation.
-
-// [1]: https://www.mediawiki.org/wiki/Library_infrastructure_for_MediaWiki
-
-/**
- * Utility constants and sets for the HTML5 tree building algorithm.
- * Sets are associative arrays indexed first by namespace and then by
- * lower-cased tag name.
- *
- * @ingroup Parser
- * @since 1.27
- */
-class BalanceSets {
-       const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
-       const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
-       const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
-
-       public static $unsupportedSet = [
-               self::HTML_NAMESPACE => [
-                       'html' => true, 'head' => true, 'body' => true, 'frameset' => true,
-                       'frame' => true,
-                       'plaintext' => true,
-                       'xmp' => true, 'iframe' => true, 'noembed' => true,
-                       'noscript' => true, 'script' => true,
-                       'title' => true
-               ]
-       ];
-
-       public static $emptyElementSet = [
-               self::HTML_NAMESPACE => [
-                       'area' => true, 'base' => true, 'basefont' => true,
-                       'bgsound' => true, 'br' => true, 'col' => true, 'command' => true,
-                       'embed' => true, 'frame' => true, 'hr' => true, 'img' => true,
-                       'input' => true, 'keygen' => true, 'link' => true, 'meta' => true,
-                       'param' => true, 'source' => true, 'track' => true, 'wbr' => true
-               ]
-       ];
-
-       public static $extraLinefeedSet = [
-               self::HTML_NAMESPACE => [
-                       'pre' => true, 'textarea' => true, 'listing' => true,
-               ]
-       ];
-
-       public static $headingSet = [
-               self::HTML_NAMESPACE => [
-                       'h1' => true, 'h2' => true, 'h3' => true,
-                       'h4' => true, 'h5' => true, 'h6' => true
-               ]
-       ];
-
-       public static $specialSet = [
-               self::HTML_NAMESPACE => [
-                       'address' => true, 'applet' => true, 'area' => true,
-                       'article' => true, 'aside' => true, 'base' => true,
-                       'basefont' => true, 'bgsound' => true, 'blockquote' => true,
-                       'body' => true, 'br' => true, 'button' => true, 'caption' => true,
-                       'center' => true, 'col' => true, 'colgroup' => true, 'dd' => true,
-                       'details' => true, 'dir' => true, 'div' => true, 'dl' => true,
-                       'dt' => true, 'embed' => true, 'fieldset' => true,
-                       'figcaption' => true, 'figure' => true, 'footer' => true,
-                       'form' => true, 'frame' => true, 'frameset' => true, 'h1' => true,
-                       'h2' => true, 'h3' => true, 'h4' => true, 'h5' => true,
-                       'h6' => true, 'head' => true, 'header' => true, 'hgroup' => true,
-                       'hr' => true, 'html' => true, 'iframe' => true, 'img' => true,
-                       'input' => true, 'li' => true, 'link' => true,
-                       'listing' => true, 'main' => true, 'marquee' => true,
-                       'menu' => true, 'meta' => true, 'nav' => true,
-                       'noembed' => true, 'noframes' => true, 'noscript' => true,
-                       'object' => true, 'ol' => true, 'p' => true, 'param' => true,
-                       'plaintext' => true, 'pre' => true, 'script' => true,
-                       'section' => true, 'select' => true, 'source' => true,
-                       'style' => true, 'summary' => true, 'table' => true,
-                       'tbody' => true, 'td' => true, 'template' => true,
-                       'textarea' => true, 'tfoot' => true, 'th' => true, 'thead' => true,
-                       'title' => true, 'tr' => true, 'track' => true, 'ul' => true,
-                       'wbr' => true, 'xmp' => true
-               ],
-               self::SVG_NAMESPACE => [
-                       'foreignobject' => true, 'desc' => true, 'title' => true
-               ],
-               self::MATHML_NAMESPACE => [
-                       'mi' => true, 'mo' => true, 'mn' => true, 'ms' => true,
-                       'mtext' => true, 'annotation-xml' => true
-               ]
-       ];
-
-       public static $addressDivPSet = [
-               self::HTML_NAMESPACE => [
-                       'address' => true, 'div' => true, 'p' => true
-               ]
-       ];
-
-       public static $tableSectionRowSet = [
-               self::HTML_NAMESPACE => [
-                       'table' => true, 'thead' => true, 'tbody' => true,
-                       'tfoot' => true, 'tr' => true
-               ]
-       ];
-
-       public static $impliedEndTagsSet = [
-               self::HTML_NAMESPACE => [
-                       'dd' => true, 'dt' => true, 'li' => true,
-                       'menuitem' => true, 'optgroup' => true,
-                       'option' => true, 'p' => true, 'rb' => true, 'rp' => true,
-                       'rt' => true, 'rtc' => true
-               ]
-       ];
-
-       public static $thoroughImpliedEndTagsSet = [
-               self::HTML_NAMESPACE => [
-                       'caption' => true, 'colgroup' => true, 'dd' => true, 'dt' => true,
-                       'li' => true, 'optgroup' => true, 'option' => true, 'p' => true,
-                       'rb' => true, 'rp' => true, 'rt' => true, 'rtc' => true,
-                       'tbody' => true, 'td' => true, 'tfoot' => true, 'th' => true,
-                       'thead' => true, 'tr' => true
-               ]
-       ];
-
-       public static $tableCellSet = [
-               self::HTML_NAMESPACE => [
-                       'td' => true, 'th' => true
-               ]
-       ];
-       public static $tableContextSet = [
-               self::HTML_NAMESPACE => [
-                       'table' => true, 'template' => true, 'html' => true
-               ]
-       ];
-
-       public static $tableBodyContextSet = [
-               self::HTML_NAMESPACE => [
-                       'tbody' => true, 'tfoot' => true, 'thead' => true,
-                       'template' => true, 'html' => true
-               ]
-       ];
-
-       public static $tableRowContextSet = [
-               self::HTML_NAMESPACE => [
-                       'tr' => true, 'template' => true, 'html' => true
-               ]
-       ];
-
-       // See https://html.spec.whatwg.org/multipage/forms.html#form-associated-element
-       public static $formAssociatedSet = [
-               self::HTML_NAMESPACE => [
-                       'button' => true, 'fieldset' => true, 'input' => true,
-                       'keygen' => true, 'object' => true, 'output' => true,
-                       'select' => true, 'textarea' => true, 'img' => true
-               ]
-       ];
-
-       public static $inScopeSet = [
-               self::HTML_NAMESPACE => [
-                       'applet' => true, 'caption' => true, 'html' => true,
-                       'marquee' => true, 'object' => true,
-                       'table' => true, 'td' => true, 'template' => true,
-                       'th' => true
-               ],
-               self::SVG_NAMESPACE => [
-                       'foreignobject' => true, 'desc' => true, 'title' => true
-               ],
-               self::MATHML_NAMESPACE => [
-                       'mi' => true, 'mo' => true, 'mn' => true, 'ms' => true,
-                       'mtext' => true, 'annotation-xml' => true
-               ]
-       ];
-
-       private static $inListItemScopeSet = null;
-       public static function inListItemScopeSet() {
-               if ( self::$inListItemScopeSet === null ) {
-                       self::$inListItemScopeSet = self::$inScopeSet;
-                       self::$inListItemScopeSet[self::HTML_NAMESPACE]['ol'] = true;
-                       self::$inListItemScopeSet[self::HTML_NAMESPACE]['ul'] = true;
-               }
-               return self::$inListItemScopeSet;
-       }
-
-       private static $inButtonScopeSet = null;
-       public static function inButtonScopeSet() {
-               if ( self::$inButtonScopeSet === null ) {
-                       self::$inButtonScopeSet = self::$inScopeSet;
-                       self::$inButtonScopeSet[self::HTML_NAMESPACE]['button'] = true;
-               }
-               return self::$inButtonScopeSet;
-       }
-
-       public static $inTableScopeSet = [
-               self::HTML_NAMESPACE => [
-                       'html' => true, 'table' => true, 'template' => true
-               ]
-       ];
-
-       public static $inInvertedSelectScopeSet = [
-               self::HTML_NAMESPACE => [
-                       'option' => true, 'optgroup' => true
-               ]
-       ];
-
-       public static $mathmlTextIntegrationPointSet = [
-               self::MATHML_NAMESPACE => [
-                       'mi' => true, 'mo' => true, 'mn' => true, 'ms' => true,
-                       'mtext' => true
-               ]
-       ];
-
-       public static $htmlIntegrationPointSet = [
-               self::SVG_NAMESPACE => [
-                       'foreignobject' => true,
-                       'desc' => true,
-                       'title' => true
-               ]
-       ];
-
-       // For tidy compatibility.
-       public static $tidyPWrapSet = [
-               self::HTML_NAMESPACE => [
-                       'body' => true, 'blockquote' => true,
-                       // We parse with <body> as the fragment context, but the top-level
-                       // element on the stack is actually <html>.  We could use the
-                       // "adjusted current node" everywhere to work around this, but it's
-                       // easier just to add <html> to the p-wrap set.
-                       'html' => true,
-               ],
-       ];
-       public static $tidyInlineSet = [
-               self::HTML_NAMESPACE => [
-                       'a' => true, 'abbr' => true, 'acronym' => true, 'applet' => true,
-                       'b' => true, 'basefont' => true, 'bdo' => true, 'big' => true,
-                       'br' => true, 'button' => true, 'cite' => true, 'code' => true,
-                       'dfn' => true, 'em' => true, 'font' => true, 'i' => true,
-                       'iframe' => true, 'img' => true, 'input' => true, 'kbd' => true,
-                       'label' => true, 'legend' => true, 'map' => true, 'object' => true,
-                       'param' => true, 'q' => true, 'rb' => true, 'rbc' => true,
-                       'rp' => true, 'rt' => true, 'rtc' => true, 'ruby' => true,
-                       's' => true, 'samp' => true, 'select' => true, 'small' => true,
-                       'span' => true, 'strike' => true, 'strong' => true, 'sub' => true,
-                       'sup' => true, 'textarea' => true, 'tt' => true, 'u' => true,
-                       'var' => true,
-                       // Those defined in tidy.conf
-                       'video' => true, 'audio' => true, 'bdi' => true, 'data' => true,
-                       'time' => true, 'mark' => true,
-               ],
-       ];
-}
-
-/**
- * A BalanceElement is a simplified version of a DOM Node.  The main
- * difference is that we only keep BalanceElements around for nodes
- * currently on the BalanceStack of open elements.  As soon as an
- * element is closed, with some minor exceptions relating to the
- * tree builder "adoption agency algorithm", the element and all its
- * children are serialized to a string using the flatten() method.
- * This keeps our memory usage low.
- *
- * @ingroup Parser
- * @since 1.27
- */
-class BalanceElement {
-       /**
-        * The namespace of the element.
-        * @var string $namespaceURI
-        */
-       public $namespaceURI;
-       /**
-        * The lower-cased name of the element.
-        * @var string $localName
-        */
-       public $localName;
-       /**
-        * Attributes for the element, in array form
-        * @var array $attribs
-        */
-       public $attribs;
-
-       /**
-        * Parent of this element, or the string "flat" if this element has
-        * already been flattened into its parent.
-        * @var BalanceElement|string|null $parent
-        */
-       public $parent;
-
-       /**
-        * An array of children of this element.  Typically only the last
-        * child will be an actual BalanceElement object; the rest will
-        * be strings, representing either text nodes or flattened
-        * BalanceElement objects.
-        * @var BalanceElement[]|string[] $children
-        */
-       public $children;
-
-       /**
-        * A unique string identifier for Noah's Ark purposes, lazy initialized
-        */
-       private $noahKey;
-
-       /**
-        * The next active formatting element in the list, or null if this is the
-        * end of the AFE list or if the element is not in the AFE list.
-        */
-       public $nextAFE;
-
-       /**
-        * The previous active formatting element in the list, or null if this is
-        * the start of the list or if the element is not in the AFE list.
-        */
-       public $prevAFE;
-
-       /**
-        * The next element in the Noah's Ark species bucket.
-        */
-       public $nextNoah;
-
-       /**
-        * Make a new BalanceElement corresponding to the HTML DOM Element
-        * with the given localname, namespace, and attributes.
-        *
-        * @param string $namespaceURI The namespace of the element.
-        * @param string $localName The lowercased name of the tag.
-        * @param array $attribs Attributes of the element
-        */
-       public function __construct( $namespaceURI, $localName, array $attribs ) {
-               $this->localName = $localName;
-               $this->namespaceURI = $namespaceURI;
-               $this->attribs = $attribs;
-               $this->contents = '';
-               $this->parent = null;
-               $this->children = [];
-       }
-
-       /**
-        * Remove the given child from this element.
-        * @param BalanceElement $elt
-        */
-       private function removeChild( BalanceElement $elt ) {
-               Assert::precondition(
-                       $this->parent !== 'flat', "Can't removeChild after flattening $this"
-               );
-               Assert::parameter(
-                       $elt->parent === $this, 'elt', 'must have $this as a parent'
-               );
-               $idx = array_search( $elt, $this->children, true );
-               Assert::parameter( $idx !== false, '$elt', 'must be a child of $this' );
-               $elt->parent = null;
-               array_splice( $this->children, $idx, 1 );
-       }
-
-       /**
-        * Find $a in the list of children and insert $b before it.
-        * @param BalanceElement $a
-        * @param BalanceElement|string $b
-        */
-       public function insertBefore( BalanceElement $a, $b ) {
-               Assert::precondition(
-                       $this->parent !== 'flat', "Can't insertBefore after flattening."
-               );
-               $idx = array_search( $a, $this->children, true );
-               Assert::parameter( $idx !== false, '$a', 'must be a child of $this' );
-               if ( is_string( $b ) ) {
-                       array_splice( $this->children, $idx, 0, [ $b ] );
-               } else {
-                       Assert::parameter( $b->parent !== 'flat', '$b', "Can't be flat" );
-                       if ( $b->parent !== null ) {
-                               $b->parent->removeChild( $b );
-                       }
-                       array_splice( $this->children, $idx, 0, [ $b ] );
-                       $b->parent = $this;
-               }
-       }
-
-       /**
-        * Append $elt to the end of the list of children.
-        * @param BalanceElement|string $elt
-        */
-       public function appendChild( $elt ) {
-               Assert::precondition(
-                       $this->parent !== 'flat', "Can't appendChild after flattening."
-               );
-               if ( is_string( $elt ) ) {
-                       array_push( $this->children, $elt );
-                       return;
-               }
-               // Remove $elt from parent, if it had one.
-               if ( $elt->parent !== null ) {
-                       $elt->parent->removeChild( $elt );
-               }
-               array_push( $this->children, $elt );
-               $elt->parent = $this;
-       }
-
-       /**
-        * Transfer all of the children of $elt to $this.
-        * @param BalanceElement $elt
-        */
-       public function adoptChildren( BalanceElement $elt ) {
-               Assert::precondition(
-                       $elt->parent !== 'flat', "Can't adoptChildren after flattening."
-               );
-               foreach ( $elt->children as $child ) {
-                       if ( !is_string( $child ) ) {
-                               // This is an optimization which avoids an O(n^2) set of
-                               // array_splice operations.
-                               $child->parent = null;
-                       }
-                       $this->appendChild( $child );
-               }
-               $elt->children = [];
-       }
-
-       /**
-        * Flatten this node and all of its children into a string, as specified
-        * by the HTML serialization specification, and replace this node
-        * in its parent by that string.
-        *
-        * @param array $config Balancer configuration; see Balancer::__construct().
-        * @return string
-        *
-        * @see __toString()
-        */
-       public function flatten( array $config ) {
-               Assert::parameter( $this->parent !== null, '$this', 'must be a child' );
-               Assert::parameter( $this->parent !== 'flat', '$this', 'already flat' );
-               $idx = array_search( $this, $this->parent->children, true );
-               Assert::parameter(
-                       $idx !== false, '$this', 'must be a child of its parent'
-               );
-               $tidyCompat = $config['tidyCompat'];
-               if ( $tidyCompat ) {
-                       $blank = true;
-                       foreach ( $this->children as $elt ) {
-                               if ( !is_string( $elt ) ) {
-                                       $elt = $elt->flatten( $config );
-                               }
-                               if ( $blank && preg_match( '/[^\t\n\f\r ]/', $elt ) ) {
-                                       $blank = false;
-                               }
-                       }
-                       if ( $this->isHtmlNamed( 'mw:p-wrap' ) ) {
-                               $this->localName = 'p';
-                       } elseif ( $blank ) {
-                               // Add 'mw-empty-elt' class so elements can be hidden via CSS
-                               // for compatibility with legacy tidy.
-                               if ( !count( $this->attribs ) &&
-                                       ( $this->localName === 'tr' || $this->localName === 'li' )
-                               ) {
-                                       $this->attribs = [ 'class' => "mw-empty-elt" ];
-                               }
-                               $blank = false;
-                       } elseif (
-                               $this->isA( BalanceSets::$extraLinefeedSet ) &&
-                               count( $this->children ) > 0 &&
-                               substr( $this->children[0], 0, 1 ) == "\n"
-                       ) {
-                               // Double the linefeed after pre/listing/textarea
-                               // according to the (old) HTML5 fragment serialization
-                               // algorithm (see https://github.com/whatwg/html/issues/944)
-                               // to ensure this will round-trip.
-                               array_unshift( $this->children, "\n" );
-                       }
-                       $flat = $blank ? '' : "{$this}";
-               } else {
-                       $flat = "{$this}";
-               }
-               $this->parent->children[$idx] = $flat;
-               $this->parent = 'flat'; // for assertion checking
-               return $flat;
-       }
-
-       /**
-        * Serialize this node and all of its children to a string, as specified
-        * by the HTML serialization specification.
-        *
-        * @return string The serialization of the BalanceElement
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#serialising-html-fragments
-        */
-       public function __toString() {
-               $encAttribs = '';
-               foreach ( $this->attribs as $name => $value ) {
-                       $encValue = Sanitizer::encodeAttribute( $value );
-                       $encAttribs .= " $name=\"$encValue\"";
-               }
-               if ( !$this->isA( BalanceSets::$emptyElementSet ) ) {
-                       $out = "<{$this->localName}{$encAttribs}>";
-                       $len = strlen( $out );
-                       // flatten children
-                       foreach ( $this->children as $elt ) {
-                               $out .= "{$elt}";
-                       }
-                       $out .= "</{$this->localName}>";
-               } else {
-                       $out = "<{$this->localName}{$encAttribs} />";
-                       Assert::invariant(
-                               count( $this->children ) === 0,
-                               "Empty elements shouldn't have children."
-                       );
-               }
-               return $out;
-       }
-
-       // Utility functions on BalanceElements.
-
-       /**
-        * Determine if $this represents a specific HTML tag, is a member of
-        * a tag set, or is equal to another BalanceElement.
-        *
-        * @param BalanceElement|array|string $set The target BalanceElement,
-        *   set (from the BalanceSets class), or string (HTML tag name).
-        * @return bool
-        */
-       public function isA( $set ) {
-               if ( $set instanceof BalanceElement ) {
-                       return $this === $set;
-               } elseif ( is_array( $set ) ) {
-                       return isset( $set[$this->namespaceURI] ) &&
-                               isset( $set[$this->namespaceURI][$this->localName] );
-               } else {
-                       // assume this is an HTML element name.
-                       return $this->isHtml() && $this->localName === $set;
-               }
-       }
-
-       /**
-        * Determine if this element is an HTML element with the specified name
-        * @param string $tagName
-        * @return bool
-        */
-       public function isHtmlNamed( $tagName ) {
-               return $this->namespaceURI === BalanceSets::HTML_NAMESPACE
-                       && $this->localName === $tagName;
-       }
-
-       /**
-        * Determine if $this represents an element in the HTML namespace.
-        *
-        * @return bool
-        */
-       public function isHtml() {
-               return $this->namespaceURI === BalanceSets::HTML_NAMESPACE;
-       }
-
-       /**
-        * Determine if $this represents a MathML text integration point,
-        * as defined in the HTML5 specification.
-        *
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#mathml-text-integration-point
-        */
-       public function isMathmlTextIntegrationPoint() {
-               return $this->isA( BalanceSets::$mathmlTextIntegrationPointSet );
-       }
-
-       /**
-        * Determine if $this represents an HTML integration point,
-        * as defined in the HTML5 specification.
-        *
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
-        */
-       public function isHtmlIntegrationPoint() {
-               if ( $this->isA( BalanceSets::$htmlIntegrationPointSet ) ) {
-                       return true;
-               }
-               if (
-                       $this->namespaceURI === BalanceSets::MATHML_NAMESPACE &&
-                       $this->localName === 'annotation-xml' &&
-                       isset( $this->attribs['encoding'] ) &&
-                       ( strcasecmp( $this->attribs['encoding'], 'text/html' ) == 0 ||
-                       strcasecmp( $this->attribs['encoding'], 'application/xhtml+xml' ) == 0 )
-               ) {
-                       return true;
-               }
-               return false;
-       }
-
-       /**
-        * Get a string key for the Noah's Ark algorithm
-        * @return string
-        */
-       public function getNoahKey() {
-               if ( $this->noahKey === null ) {
-                       $attribs = $this->attribs;
-                       ksort( $attribs );
-                       $this->noahKey = serialize( [ $this->namespaceURI, $this->localName, $attribs ] );
-               }
-               return $this->noahKey;
-       }
-}
-
-/**
- * The "stack of open elements" as defined in the HTML5 tree builder
- * spec.  This contains methods to ensure that content (start tags, text)
- * are inserted at the correct place in the output string, and to
- * flatten BalanceElements are they are closed to avoid holding onto
- * a complete DOM tree for the document in memory.
- *
- * The stack defines a PHP iterator to traverse it in "reverse order",
- * that is, the most-recently-added element is visited first in a
- * foreach loop.
- *
- * @ingroup Parser
- * @since 1.27
- * @see https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements
- */
-class BalanceStack implements IteratorAggregate {
-       /**
-        * Backing storage for the stack.
-        * @var BalanceElement[] $elements
-        */
-       private $elements = [];
-       /**
-        * Foster parent mode determines how nodes are inserted into the
-        * stack.
-        * @var bool $fosterParentMode
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#foster-parent
-        */
-       public $fosterParentMode = false;
-       /**
-        * Configuration options governing flattening.
-        * @var array $config
-        * @see Balancer::__construct()
-        */
-       private $config;
-       /**
-        * Reference to the current element
-        */
-       public $currentNode;
-
-       /**
-        * Create a new BalanceStack with a single BalanceElement on it,
-        * representing the root &lt;html&gt; node.
-        * @param array $config Balancer configuration; see Balancer::_construct().
-        */
-       public function __construct( array $config ) {
-               // always a root <html> element on the stack
-               array_push(
-                       $this->elements,
-                       new BalanceElement( BalanceSets::HTML_NAMESPACE, 'html', [] )
-               );
-               $this->currentNode = $this->elements[0];
-               $this->config = $config;
-       }
-
-       /**
-        * Return a string representing the output of the tree builder:
-        * all the children of the root &lt;html&gt; node.
-        * @return string
-        */
-       public function getOutput() {
-               // Don't include the outer '<html>....</html>'
-               $out = '';
-               foreach ( $this->elements[0]->children as $elt ) {
-                       $out .= is_string( $elt ) ? $elt :
-                               $elt->flatten( $this->config );
-               }
-               return $out;
-       }
-
-       /**
-        * Insert a comment at the appropriate place for inserting a node.
-        * @param string $value Content of the comment.
-        * @return string
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#insert-a-comment
-        */
-       public function insertComment( $value ) {
-               // Just another type of text node, except for tidy p-wrapping.
-               return $this->insertText( '<!--' . $value . '-->', true );
-       }
-
-       /**
-        * Insert text at the appropriate place for inserting a node.
-        * @param string $value
-        * @param bool $isComment
-        * @return string
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#appropriate-place-for-inserting-a-node
-        */
-       public function insertText( $value, $isComment = false ) {
-               if (
-                       $this->fosterParentMode &&
-                       $this->currentNode->isA( BalanceSets::$tableSectionRowSet )
-               ) {
-                       $this->fosterParent( $value );
-               } elseif (
-                       $this->config['tidyCompat'] && !$isComment &&
-                       $this->currentNode->isA( BalanceSets::$tidyPWrapSet )
-               ) {
-                       $this->insertHTMLElement( 'mw:p-wrap', [] );
-                       return $this->insertText( $value );
-               } else {
-                       $this->currentNode->appendChild( $value );
-               }
-       }
-
-       /**
-        * Insert a BalanceElement at the appropriate place, pushing it
-        * on to the open elements stack.
-        * @param string $namespaceURI The element namespace
-        * @param string $tag The tag name
-        * @param string $attribs Normalized attributes, as a string.
-        * @return BalanceElement
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#insert-a-foreign-element
-        */
-       public function insertForeignElement( $namespaceURI, $tag, $attribs ) {
-               return $this->insertElement(
-                       new BalanceElement( $namespaceURI, $tag, $attribs )
-               );
-       }
-
-       /**
-        * Insert an HTML element at the appropriate place, pushing it on to
-        * the open elements stack.
-        * @param string $tag The tag name
-        * @param string $attribs Normalized attributes, as a string.
-        * @return BalanceElement
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#insert-an-html-element
-        */
-       public function insertHTMLElement( $tag, $attribs ) {
-               return $this->insertForeignElement(
-                       BalanceSets::HTML_NAMESPACE, $tag, $attribs
-               );
-       }
-
-       /**
-        * Insert an element at the appropriate place and push it on to the
-        * open elements stack.
-        * @param BalanceElement $elt
-        * @return BalanceElement
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#appropriate-place-for-inserting-a-node
-        */
-       public function insertElement( BalanceElement $elt ) {
-               if (
-                       $this->currentNode->isHtmlNamed( 'mw:p-wrap' ) &&
-                       !$elt->isA( BalanceSets::$tidyInlineSet )
-               ) {
-                       // Tidy compatibility.
-                       $this->pop();
-               }
-               if (
-                       $this->fosterParentMode &&
-                       $this->currentNode->isA( BalanceSets::$tableSectionRowSet )
-               ) {
-                       $elt = $this->fosterParent( $elt );
-               } else {
-                       $this->currentNode->appendChild( $elt );
-               }
-               Assert::invariant( $elt->parent !== null, "$elt must be in tree" );
-               Assert::invariant( $elt->parent !== 'flat', "$elt must not have been previous flattened" );
-               array_push( $this->elements, $elt );
-               $this->currentNode = $elt;
-               return $elt;
-       }
-
-       /**
-        * Determine if the stack has $tag in scope.
-        * @param BalanceElement|array|string $tag
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
-        */
-       public function inScope( $tag ) {
-               return $this->inSpecificScope( $tag, BalanceSets::$inScopeSet );
-       }
-
-       /**
-        * Determine if the stack has $tag in button scope.
-        * @param BalanceElement|array|string $tag
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
-        */
-       public function inButtonScope( $tag ) {
-               return $this->inSpecificScope( $tag, BalanceSets::inButtonScopeSet() );
-       }
-
-       /**
-        * Determine if the stack has $tag in list item scope.
-        * @param BalanceElement|array|string $tag
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-list-item-scope
-        */
-       public function inListItemScope( $tag ) {
-               return $this->inSpecificScope( $tag, BalanceSets::inListItemScopeSet() );
-       }
-
-       /**
-        * Determine if the stack has $tag in table scope.
-        * @param BalanceElement|array|string $tag
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-table-scope
-        */
-       public function inTableScope( $tag ) {
-               return $this->inSpecificScope( $tag, BalanceSets::$inTableScopeSet );
-       }
-
-       /**
-        * Determine if the stack has $tag in select scope.
-        * @param BalanceElement|array|string $tag
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-select-scope
-        */
-       public function inSelectScope( $tag ) {
-               // Can't use inSpecificScope to implement this, since it involves
-               // *inverting* a set of tags.  Implement manually.
-               foreach ( $this as $elt ) {
-                       if ( $elt->isA( $tag ) ) {
-                               return true;
-                       }
-                       if ( !$elt->isA( BalanceSets::$inInvertedSelectScopeSet ) ) {
-                               return false;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Determine if the stack has $tag in a specific scope, $set.
-        * @param BalanceElement|array|string $tag
-        * @param BalanceElement|array|string $set
-        * @return bool
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-the-specific-scope
-        */
-       public function inSpecificScope( $tag, $set ) {
-               foreach ( $this as $elt ) {
-                       if ( $elt->isA( $tag ) ) {
-                               return true;
-                       }
-                       if ( $elt->isA( $set ) ) {
-                               return false;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Generate implied end tags.
-        * @param string $butnot
-        * @param bool $thorough True if we should generate end tags thoroughly.
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
-        */
-       public function generateImpliedEndTags( $butnot = null, $thorough = false ) {
-               $endTagSet = $thorough ?
-                       BalanceSets::$thoroughImpliedEndTagsSet :
-                       BalanceSets::$impliedEndTagsSet;
-               while ( $this->currentNode ) {
-                       if ( $butnot !== null && $this->currentNode->isHtmlNamed( $butnot ) ) {
-                               break;
-                       }
-                       if ( !$this->currentNode->isA( $endTagSet ) ) {
-                               break;
-                       }
-                       $this->pop();
-               }
-       }
-
-       /**
-        * Return the adjusted current node.
-        * @param string $fragmentContext
-        * @return string
-        */
-       public function adjustedCurrentNode( $fragmentContext ) {
-               return ( $fragmentContext && count( $this->elements ) === 1 ) ?
-                       $fragmentContext : $this->currentNode;
-       }
-
-       /**
-        * Return an iterator over this stack which visits the current node
-        * first, and the root node last.
-        * @return \Iterator
-        */
-       public function getIterator() {
-               return new ReverseArrayIterator( $this->elements );
-       }
-
-       /**
-        * Return the BalanceElement at the given position $idx, where
-        * position 0 represents the root element.
-        * @param int $idx
-        * @return BalanceElement
-        */
-       public function node( $idx ) {
-               return $this->elements[ $idx ];
-       }
-
-       /**
-        * Replace the element at position $idx in the BalanceStack with $elt.
-        * @param int $idx
-        * @param BalanceElement $elt
-        */
-       public function replaceAt( $idx, BalanceElement $elt ) {
-               Assert::precondition(
-                       $this->elements[$idx]->parent !== 'flat',
-                       'Replaced element should not have already been flattened.'
-               );
-               Assert::precondition(
-                       $elt->parent !== 'flat',
-                       'New element should not have already been flattened.'
-               );
-               $this->elements[$idx] = $elt;
-               if ( $idx === count( $this->elements ) - 1 ) {
-                       $this->currentNode = $elt;
-               }
-       }
-
-       /**
-        * Return the position of the given BalanceElement, set, or
-        * HTML tag name string in the BalanceStack.
-        * @param BalanceElement|array|string $tag
-        * @return int
-        */
-       public function indexOf( $tag ) {
-               for ( $i = count( $this->elements ) - 1; $i >= 0; $i-- ) {
-                       if ( $this->elements[$i]->isA( $tag ) ) {
-                               return $i;
-                       }
-               }
-               return -1;
-       }
-
-       /**
-        * Return the number of elements currently in the BalanceStack.
-        * @return int
-        */
-       public function length() {
-               return count( $this->elements );
-       }
-
-       /**
-        * Remove the current node from the BalanceStack, flattening it
-        * in the process.
-        */
-       public function pop() {
-               $elt = array_pop( $this->elements );
-               if ( count( $this->elements ) ) {
-                       $this->currentNode = $this->elements[ count( $this->elements ) - 1 ];
-               } else {
-                       $this->currentNode = null;
-               }
-               if ( !$elt->isHtmlNamed( 'mw:p-wrap' ) ) {
-                       $elt->flatten( $this->config );
-               }
-       }
-
-       /**
-        * Remove all nodes up to and including position $idx from the
-        * BalanceStack, flattening them in the process.
-        * @param int $idx
-        */
-       public function popTo( $idx ) {
-               for ( $length = count( $this->elements ); $length > $idx; $length-- ) {
-                       $this->pop();
-               }
-       }
-
-       /**
-        * Pop elements off the stack up to and including the first
-        * element with the specified HTML tagname (or matching the given
-        * set).
-        * @param BalanceElement|array|string $tag
-        */
-       public function popTag( $tag ) {
-               while ( $this->currentNode ) {
-                       if ( $this->currentNode->isA( $tag ) ) {
-                               $this->pop();
-                               break;
-                       }
-                       $this->pop();
-               }
-       }
-
-       /**
-        * Pop elements off the stack *not including* the first element
-        * in the specified set.
-        * @param BalanceElement|array|string $set
-        */
-       public function clearToContext( $set ) {
-               // Note that we don't loop to 0. Never pop the <html> elt off.
-               for ( $length = count( $this->elements ); $length > 1; $length-- ) {
-                       if ( $this->currentNode->isA( $set ) ) {
-                               break;
-                       }
-                       $this->pop();
-               }
-       }
-
-       /**
-        * Remove the given $elt from the BalanceStack, optionally
-        * flattening it in the process.
-        * @param BalanceElement $elt The element to remove.
-        * @param bool $flatten Whether to flatten the removed element.
-        */
-       public function removeElement( BalanceElement $elt, $flatten = true ) {
-               Assert::parameter(
-                       $elt->parent !== 'flat',
-                       '$elt',
-                       '$elt should not already have been flattened.'
-               );
-               Assert::parameter(
-                       $elt->parent->parent !== 'flat',
-                       '$elt',
-                       'The parent of $elt should not already have been flattened.'
-               );
-               $idx = array_search( $elt, $this->elements, true );
-               Assert::parameter( $idx !== false, '$elt', 'must be in stack' );
-               array_splice( $this->elements, $idx, 1 );
-               if ( $idx === count( $this->elements ) ) {
-                       $this->currentNode = $this->elements[$idx - 1];
-               }
-               if ( $flatten ) {
-                       // serialize $elt into its parent
-                       // otherwise, it will eventually serialize when the parent
-                       // is serialized, we just hold onto the memory for its
-                       // tree of objects a little longer.
-                       $elt->flatten( $this->config );
-               }
-               Assert::postcondition(
-                       array_search( $elt, $this->elements, true ) === false,
-                       '$elt should no longer be in open elements stack'
-               );
-       }
-
-       /**
-        * Find $a in the BalanceStack and insert $b after it.
-        * @param BalanceElement $a
-        * @param BalanceElement $b
-        */
-       public function insertAfter( BalanceElement $a, BalanceElement $b ) {
-               $idx = $this->indexOf( $a );
-               Assert::parameter( $idx !== false, '$a', 'must be in stack' );
-               if ( $idx === count( $this->elements ) - 1 ) {
-                       array_push( $this->elements, $b );
-                       $this->currentNode = $b;
-               } else {
-                       array_splice( $this->elements, $idx + 1, 0, [ $b ] );
-               }
-       }
-
-       // Fostering and adoption.
-
-       /**
-        * Foster parent the given $elt in the stack of open elements.
-        * @param BalanceElement|string $elt
-        * @return BalanceElement|string
-        *
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#foster-parent
-        */
-       private function fosterParent( $elt ) {
-               $lastTable = $this->indexOf( 'table' );
-               $lastTemplate = $this->indexOf( 'template' );
-               $parent = null;
-               $before = null;
-
-               if ( $lastTemplate >= 0 && ( $lastTable < 0 || $lastTemplate > $lastTable ) ) {
-                       $parent = $this->elements[$lastTemplate];
-               } elseif ( $lastTable >= 0 ) {
-                       $parent = $this->elements[$lastTable]->parent;
-                       // Assume all tables have parents, since we're not running scripts!
-                       Assert::invariant(
-                               $parent !== null, "All tables should have parents"
-                       );
-                       $before = $this->elements[$lastTable];
-               } else {
-                       $parent = $this->elements[0]; // the `html` element.
-               }
-
-               if ( $this->config['tidyCompat'] ) {
-                       if ( is_string( $elt ) ) {
-                               // We're fostering text: do we need a p-wrapper?
-                               if ( $parent->isA( BalanceSets::$tidyPWrapSet ) ) {
-                                       $this->insertHTMLElement( 'mw:p-wrap', [] );
-                                       $this->insertText( $elt );
-                                       return $elt;
-                               }
-                       } else {
-                               // We're fostering an element; do we need to merge p-wrappers?
-                               if ( $elt->isHtmlNamed( 'mw:p-wrap' ) ) {
-                                       $idx = $before ?
-                                               array_search( $before, $parent->children, true ) :
-                                               count( $parent->children );
-                                       $after = $idx > 0 ? $parent->children[$idx - 1] : '';
-                                       if (
-                                               $after instanceof BalanceElement &&
-                                               $after->isHtmlNamed( 'mw:p-wrap' )
-                                       ) {
-                                               return $after; // Re-use existing p-wrapper.
-                                       }
-                               }
-                       }
-               }
-
-               if ( $before ) {
-                       $parent->insertBefore( $before, $elt );
-               } else {
-                       $parent->appendChild( $elt );
-               }
-               return $elt;
-       }
-
-       /**
-        * Run the "adoption agency algoritm" (AAA) for the given subject
-        * tag name.
-        * @param string $tag The subject tag name.
-        * @param BalanceActiveFormattingElements $afe The current
-        *   active formatting elements list.
-        * @return true if the adoption agency algorithm "did something", false
-        *   if more processing is required by the caller.
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#adoption-agency-algorithm
-        */
-       public function adoptionAgency( $tag, $afe ) {
-               // If the current node is an HTML element whose tag name is subject,
-               // and the current node is not in the list of active formatting
-               // elements, then pop the current node off the stack of open
-               // elements and abort these steps.
-               if (
-                       $this->currentNode->isHtmlNamed( $tag ) &&
-                       !$afe->isInList( $this->currentNode )
-               ) {
-                       $this->pop();
-                       return true; // no more handling required
-               }
-
-               // Outer loop: If outer loop counter is greater than or
-               // equal to eight, then abort these steps.
-               for ( $outer = 0; $outer < 8; $outer++ ) {
-                       // Let the formatting element be the last element in the list
-                       // of active formatting elements that: is between the end of
-                       // the list and the last scope marker in the list, if any, or
-                       // the start of the list otherwise, and has the same tag name
-                       // as the token.
-                       $fmtElt = $afe->findElementByTag( $tag );
-
-                       // If there is no such node, then abort these steps and instead
-                       // act as described in the "any other end tag" entry below.
-                       if ( !$fmtElt ) {
-                               return false; // false means handle by the default case
-                       }
-
-                       // Otherwise, if there is such a node, but that node is not in
-                       // the stack of open elements, then this is a parse error;
-                       // remove the element from the list, and abort these steps.
-                       $index = $this->indexOf( $fmtElt );
-                       if ( $index < 0 ) {
-                               $afe->remove( $fmtElt );
-                               return true;   // true means no more handling required
-                       }
-
-                       // Otherwise, if there is such a node, and that node is also in
-                       // the stack of open elements, but the element is not in scope,
-                       // then this is a parse error; ignore the token, and abort
-                       // these steps.
-                       if ( !$this->inScope( $fmtElt ) ) {
-                               return true;
-                       }
-
-                       // Let the furthest block be the topmost node in the stack of
-                       // open elements that is lower in the stack than the formatting
-                       // element, and is an element in the special category. There
-                       // might not be one.
-                       $furthestBlock = null;
-                       $furthestBlockIndex = -1;
-                       $stackLength = $this->length();
-                       for ( $i = $index + 1; $i < $stackLength; $i++ ) {
-                               if ( $this->node( $i )->isA( BalanceSets::$specialSet ) ) {
-                                       $furthestBlock = $this->node( $i );
-                                       $furthestBlockIndex = $i;
-                                       break;
-                               }
-                       }
-
-                       // If there is no furthest block, then the UA must skip the
-                       // subsequent steps and instead just pop all the nodes from the
-                       // bottom of the stack of open elements, from the current node
-                       // up to and including the formatting element, and remove the
-                       // formatting element from the list of active formatting
-                       // elements.
-                       if ( !$furthestBlock ) {
-                               $this->popTag( $fmtElt );
-                               $afe->remove( $fmtElt );
-                               return true;
-                       }
-
-                       // Let the common ancestor be the element immediately above
-                       // the formatting element in the stack of open elements.
-                       $ancestor = $this->node( $index - 1 );
-
-                       // Let a bookmark note the position of the formatting
-                       // element in the list of active formatting elements
-                       // relative to the elements on either side of it in the
-                       // list.
-                       $BOOKMARK = new BalanceElement( '[bookmark]', '[bookmark]', [] );
-                       $afe->insertAfter( $fmtElt, $BOOKMARK );
-
-                       // Let node and last node be the furthest block.
-                       $node = $furthestBlock;
-                       $lastNode = $furthestBlock;
-                       $nodeIndex = $furthestBlockIndex;
-                       $isAFE = false;
-
-                       // Inner loop
-                       for ( $inner = 1; true; $inner++ ) {
-                               // Let node be the element immediately above node in
-                               // the stack of open elements, or if node is no longer
-                               // in the stack of open elements (e.g. because it got
-                               // removed by this algorithm), the element that was
-                               // immediately above node in the stack of open elements
-                               // before node was removed.
-                               $node = $this->node( --$nodeIndex );
-
-                               // If node is the formatting element, then go
-                               // to the next step in the overall algorithm.
-                               if ( $node === $fmtElt ) break;
-
-                               // If the inner loop counter is greater than three and node
-                               // is in the list of active formatting elements, then remove
-                               // node from the list of active formatting elements.
-                               $isAFE = $afe->isInList( $node );
-                               if ( $inner > 3 && $isAFE ) {
-                                       $afe->remove( $node );
-                                       $isAFE = false;
-                               }
-
-                               // If node is not in the list of active formatting
-                               // elements, then remove node from the stack of open
-                               // elements and then go back to the step labeled inner
-                               // loop.
-                               if ( !$isAFE ) {
-                                       // Don't flatten here, since we're about to relocate
-                                       // parts of this $node.
-                                       $this->removeElement( $node, false );
-                                       continue;
-                               }
-
-                               // Create an element for the token for which the
-                               // element node was created with common ancestor as
-                               // the intended parent, replace the entry for node
-                               // in the list of active formatting elements with an
-                               // entry for the new element, replace the entry for
-                               // node in the stack of open elements with an entry for
-                               // the new element, and let node be the new element.
-                               $newElt = new BalanceElement(
-                                       $node->namespaceURI, $node->localName, $node->attribs );
-                               $afe->replace( $node, $newElt );
-                               $this->replaceAt( $nodeIndex, $newElt );
-                               $node = $newElt;
-
-                               // If last node is the furthest block, then move the
-                               // aforementioned bookmark to be immediately after the
-                               // new node in the list of active formatting elements.
-                               if ( $lastNode === $furthestBlock ) {
-                                       $afe->remove( $BOOKMARK );
-                                       $afe->insertAfter( $newElt, $BOOKMARK );
-                               }
-
-                               // Insert last node into node, first removing it from
-                               // its previous parent node if any.
-                               $node->appendChild( $lastNode );
-
-                               // Let last node be node.
-                               $lastNode = $node;
-                       }
-
-                       // If the common ancestor node is a table, tbody, tfoot,
-                       // thead, or tr element, then, foster parent whatever last
-                       // node ended up being in the previous step, first removing
-                       // it from its previous parent node if any.
-                       if (
-                               $this->fosterParentMode &&
-                               $ancestor->isA( BalanceSets::$tableSectionRowSet )
-                       ) {
-                               $this->fosterParent( $lastNode );
-                       } else {
-                               // Otherwise, append whatever last node ended up being in
-                               // the previous step to the common ancestor node, first
-                               // removing it from its previous parent node if any.
-                               $ancestor->appendChild( $lastNode );
-                       }
-
-                       // Create an element for the token for which the
-                       // formatting element was created, with furthest block
-                       // as the intended parent.
-                       $newElt2 = new BalanceElement(
-                               $fmtElt->namespaceURI, $fmtElt->localName, $fmtElt->attribs );
-
-                       // Take all of the child nodes of the furthest block and
-                       // append them to the element created in the last step.
-                       $newElt2->adoptChildren( $furthestBlock );
-
-                       // Append that new element to the furthest block.
-                       $furthestBlock->appendChild( $newElt2 );
-
-                       // Remove the formatting element from the list of active
-                       // formatting elements, and insert the new element into the
-                       // list of active formatting elements at the position of
-                       // the aforementioned bookmark.
-                       $afe->remove( $fmtElt );
-                       $afe->replace( $BOOKMARK, $newElt2 );
-
-                       // Remove the formatting element from the stack of open
-                       // elements, and insert the new element into the stack of
-                       // open elements immediately below the position of the
-                       // furthest block in that stack.
-                       $this->removeElement( $fmtElt );
-                       $this->insertAfter( $furthestBlock, $newElt2 );
-               }
-
-               return true;
-       }
-
-       /**
-        * Return the contents of the open elements stack as a string for
-        * debugging.
-        * @return string
-        */
-       public function __toString() {
-               $r = [];
-               foreach ( $this->elements as $elt ) {
-                       array_push( $r, $elt->localName );
-               }
-               return implode( ' ', $r );
-       }
-}
-
-/**
- * A pseudo-element used as a marker in the list of active formatting elements
- *
- * @ingroup Parser
- * @since 1.27
- */
-class BalanceMarker {
-       public $nextAFE;
-       public $prevAFE;
-}
-
-/**
- * The list of active formatting elements, which is used to handle
- * mis-nested formatting element tags in the HTML5 tree builder
- * specification.
- *
- * @ingroup Parser
- * @since 1.27
- * @see https://html.spec.whatwg.org/multipage/syntax.html#list-of-active-formatting-elements
- */
-class BalanceActiveFormattingElements {
-       /** The last (most recent) element in the list */
-       private $tail;
-
-       /** The first (least recent) element in the list */
-       private $head;
-
-       /**
-        * An array of arrays representing the population of elements in each bucket
-        * according to the Noah's Ark clause. The outer array is stack-like, with each
-        * integer-indexed element representing a segment of the list, bounded by
-        * markers. The first element represents the segment of the list before the
-        * first marker.
-        *
-        * The inner arrays are indexed by "Noah key", which is a string which uniquely
-        * identifies each bucket according to the rules in the spec. The value in
-        * the inner array is the first (least recently inserted) element in the bucket,
-        * and subsequent members of the bucket can be found by iterating through the
-        * singly-linked list via $node->nextNoah.
-        *
-        * This is optimised for the most common case of inserting into a bucket
-        * with zero members, and deleting a bucket containing one member. In the
-        * worst case, iteration through the list is still O(1) in the document
-        * size, since each bucket can have at most 3 members.
-        */
-       private $noahTableStack = [ [] ];
-
-       public function __destruct() {
-               $next = null;
-               for ( $node = $this->head; $node; $node = $next ) {
-                       $next = $node->nextAFE;
-                       $node->prevAFE = $node->nextAFE = $node->nextNoah = null;
-               }
-               $this->head = $this->tail = $this->noahTableStack = null;
-       }
-
-       public function insertMarker() {
-               $elt = new BalanceMarker;
-               if ( $this->tail ) {
-                       $this->tail->nextAFE = $elt;
-                       $elt->prevAFE = $this->tail;
-               } else {
-                       $this->head = $elt;
-               }
-               $this->tail = $elt;
-               $this->noahTableStack[] = [];
-       }
-
-       /**
-        * Follow the steps required when the spec requires us to "push onto the
-        * list of active formatting elements".
-        * @param BalanceElement $elt
-        */
-       public function push( BalanceElement $elt ) {
-               // Must not be in the list already
-               if ( $elt->prevAFE !== null || $this->head === $elt ) {
-                       throw new ParameterAssertionException( '$elt',
-                               'Cannot insert a node into the AFE list twice' );
-               }
-
-               // "Noah's Ark clause" -- if there are already three copies of
-               // this element before we encounter a marker, then drop the last
-               // one.
-               $noahKey = $elt->getNoahKey();
-               $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ];
-               if ( !isset( $table[$noahKey] ) ) {
-                       $table[$noahKey] = $elt;
-               } else {
-                       $count = 1;
-                       $head = $tail = $table[$noahKey];
-                       while ( $tail->nextNoah ) {
-                               $tail = $tail->nextNoah;
-                               $count++;
-                       }
-                       if ( $count >= 3 ) {
-                               $this->remove( $head );
-                       }
-                       $tail->nextNoah = $elt;
-               }
-               // Add to the main AFE list
-               if ( $this->tail ) {
-                       $this->tail->nextAFE = $elt;
-                       $elt->prevAFE = $this->tail;
-               } else {
-                       $this->head = $elt;
-               }
-               $this->tail = $elt;
-       }
-
-       /**
-        * Follow the steps required when the spec asks us to "clear the list of
-        * active formatting elements up to the last marker".
-        */
-       public function clearToMarker() {
-               // Iterate back through the list starting from the tail
-               $tail = $this->tail;
-               while ( $tail && !( $tail instanceof BalanceMarker ) ) {
-                       // Unlink the element
-                       $prev = $tail->prevAFE;
-                       $tail->prevAFE = null;
-                       if ( $prev ) {
-                               $prev->nextAFE = null;
-                       }
-                       $tail->nextNoah = null;
-                       $tail = $prev;
-               }
-               // If we finished on a marker, unlink it and pop it off the Noah table stack
-               if ( $tail ) {
-                       $prev = $tail->prevAFE;
-                       if ( $prev ) {
-                               $prev->nextAFE = null;
-                       }
-                       $tail = $prev;
-                       array_pop( $this->noahTableStack );
-               } else {
-                       // No marker: wipe the top-level Noah table (which is the only one)
-                       $this->noahTableStack[0] = [];
-               }
-               // If we removed all the elements, clear the head pointer
-               if ( !$tail ) {
-                       $this->head = null;
-               }
-               $this->tail = $tail;
-       }
-
-       /**
-        * Find and return the last element with the specified tag between the
-        * end of the list and the last marker on the list.
-        * Used when parsing &lt;a&gt; "in body mode".
-        * @param string $tag
-        * @return null|Node
-        */
-       public function findElementByTag( $tag ) {
-               $elt = $this->tail;
-               while ( $elt && !( $elt instanceof BalanceMarker ) ) {
-                       if ( $elt->localName === $tag ) {
-                               return $elt;
-                       }
-                       $elt = $elt->prevAFE;
-               }
-               return null;
-       }
-
-       /**
-        * Determine whether an element is in the list of formatting elements.
-        * @param BalanceElement $elt
-        * @return bool
-        */
-       public function isInList( BalanceElement $elt ) {
-               return $this->head === $elt || $elt->prevAFE;
-       }
-
-       /**
-        * Find the element $elt in the list and remove it.
-        * Used when parsing &lt;a&gt; in body mode.
-        *
-        * @param BalanceElement $elt
-        */
-       public function remove( BalanceElement $elt ) {
-               if ( $this->head !== $elt && !$elt->prevAFE ) {
-                       throw new ParameterAssertionException( '$elt',
-                               "Attempted to remove an element which is not in the AFE list" );
-               }
-               // Update head and tail pointers
-               if ( $this->head === $elt ) {
-                       $this->head = $elt->nextAFE;
-               }
-               if ( $this->tail === $elt ) {
-                       $this->tail = $elt->prevAFE;
-               }
-               // Update previous element
-               if ( $elt->prevAFE ) {
-                       $elt->prevAFE->nextAFE = $elt->nextAFE;
-               }
-               // Update next element
-               if ( $elt->nextAFE ) {
-                       $elt->nextAFE->prevAFE = $elt->prevAFE;
-               }
-               // Clear pointers so that isInList() etc. will work
-               $elt->prevAFE = $elt->nextAFE = null;
-               // Update Noah list
-               $this->removeFromNoahList( $elt );
-       }
-
-       private function addToNoahList( BalanceElement $elt ) {
-               $noahKey = $elt->getNoahKey();
-               $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ];
-               if ( !isset( $table[$noahKey] ) ) {
-                       $table[$noahKey] = $elt;
-               } else {
-                       $tail = $table[$noahKey];
-                       while ( $tail->nextNoah ) {
-                               $tail = $tail->nextNoah;
-                       }
-                       $tail->nextNoah = $elt;
-               }
-       }
-
-       private function removeFromNoahList( BalanceElement $elt ) {
-               $table =& $this->noahTableStack[ count( $this->noahTableStack ) - 1 ];
-               $key = $elt->getNoahKey();
-               $noahElt = $table[$key];
-               if ( $noahElt === $elt ) {
-                       if ( $noahElt->nextNoah ) {
-                               $table[$key] = $noahElt->nextNoah;
-                               $noahElt->nextNoah = null;
-                       } else {
-                               unset( $table[$key] );
-                       }
-               } else {
-                       do {
-                               $prevNoahElt = $noahElt;
-                               $noahElt = $prevNoahElt->nextNoah;
-                               if ( $noahElt === $elt ) {
-                                       // Found it, unlink
-                                       $prevNoahElt->nextNoah = $elt->nextNoah;
-                                       $elt->nextNoah = null;
-                                       break;
-                               }
-                       } while ( $noahElt );
-               }
-       }
-
-       /**
-        * Find element $a in the list and replace it with element $b
-        *
-        * @param BalanceElement $a
-        * @param BalanceElement $b
-        */
-       public function replace( BalanceElement $a, BalanceElement $b ) {
-               if ( $this->head !== $a && !$a->prevAFE ) {
-                       throw new ParameterAssertionException( '$a',
-                               "Attempted to replace an element which is not in the AFE list" );
-               }
-               // Update head and tail pointers
-               if ( $this->head === $a ) {
-                       $this->head = $b;
-               }
-               if ( $this->tail === $a ) {
-                       $this->tail = $b;
-               }
-               // Update previous element
-               if ( $a->prevAFE ) {
-                       $a->prevAFE->nextAFE = $b;
-               }
-               // Update next element
-               if ( $a->nextAFE ) {
-                       $a->nextAFE->prevAFE = $b;
-               }
-               $b->prevAFE = $a->prevAFE;
-               $b->nextAFE = $a->nextAFE;
-               $a->nextAFE = $a->prevAFE = null;
-               // Update Noah list
-               $this->removeFromNoahList( $a );
-               $this->addToNoahList( $b );
-       }
-
-       /**
-        * Find $a in the list and insert $b after it.
-
-        * @param BalanceElement $a
-        * @param BalanceElement $b
-        */
-       public function insertAfter( BalanceElement $a, BalanceElement $b ) {
-               if ( $this->head !== $a && !$a->prevAFE ) {
-                       throw new ParameterAssertionException( '$a',
-                               "Attempted to insert after an element which is not in the AFE list" );
-               }
-               if ( $this->tail === $a ) {
-                       $this->tail = $b;
-               }
-               if ( $a->nextAFE ) {
-                       $a->nextAFE->prevAFE = $b;
-               }
-               $b->nextAFE = $a->nextAFE;
-               $b->prevAFE = $a;
-               $a->nextAFE = $b;
-               $this->addToNoahList( $b );
-       }
-
-       /**
-        * Reconstruct the active formatting elements.
-        * @param BalanceStack $stack The open elements stack
-        * @see https://html.spec.whatwg.org/multipage/syntax.html#reconstruct-the-active-formatting-elements
-        */
-       public function reconstruct( $stack ) {
-               $entry = $this->tail;
-               // If there are no entries in the list of active formatting elements,
-               // then there is nothing to reconstruct
-               if ( !$entry ) {
-                       return;
-               }
-               // If the last is a marker, do nothing.
-               if ( $entry instanceof BalanceMarker ) {
-                       return;
-               }
-               // Or if it is an open element, do nothing.
-               if ( $stack->indexOf( $entry ) >= 0 ) {
-                       return;
-               }
-
-               // Loop backward through the list until we find a marker or an
-               // open element
-               $foundIt = false;
-               while ( $entry->prevAFE ) {
-                       $entry = $entry->prevAFE;
-                       if ( $entry instanceof BalanceMarker || $stack->indexOf( $entry ) >= 0 ) {
-                               $foundIt = true;
-                               break;
-                       }
-               }
-
-               // Now loop forward, starting from the element after the current one (or
-               // the first element if we didn't find a marker or open element),
-               // recreating formatting elements and pushing them back onto the list
-               // of open elements.
-               if ( $foundIt ) {
-                       $entry = $entry->nextAFE;
-               }
-               do {
-                       $newElement = $stack->insertHTMLElement(
-                               $entry->localName,
-                               $entry->attribs );
-                       $this->replace( $entry, $newElement );
-                       $entry = $newElement->nextAFE;
-               } while ( $entry );
-       }
-
-       /**
-        * Get a string representation of the AFE list, for debugging
-        */
-       public function __toString() {
-               $prev = null;
-               $s = '';
-               for ( $node = $this->head; $node; $prev = $node, $node = $node->nextAFE ) {
-                       if ( $node instanceof BalanceMarker ) {
-                               $s .= "MARKER\n";
-                               continue;
-                       }
-                       $s .= $node->localName . '#' . substr( md5( spl_object_hash( $node ) ), 0, 8 );
-                       if ( $node->nextNoah ) {
-                               $s .= " (noah sibling: {$node->nextNoah->localName}#" .
-                                       substr( md5( spl_object_hash( $node->nextNoah ) ), 0, 8 ) .
-                                       ')';
-                       }
-                       if ( $node->nextAFE && $node->nextAFE->prevAFE !== $node ) {
-                               $s .= " (reverse link is wrong!)";
-                       }
-                       $s .= "\n";
-               }
-               if ( $prev !== $this->tail ) {
-                       $s .= "(tail pointer is wrong!)\n";
-               }
-               return $s;
-       }
-}
-
-/**
- * An implementation of the tree building portion of the HTML5 parsing
- * spec.
- *
- * This is used to balance and tidy output so that the result can
- * always be cleanly serialized/deserialized by an HTML5 parser.  It
- * does *not* guarantee "conforming" output -- the HTML5 spec contains
- * a number of constraints which are not enforced by the HTML5 parsing
- * process.  But the result will be free of gross errors: misnested or
- * unclosed tags, for example, and will be unchanged by spec-complient
- * parsing followed by serialization.
- *
- * The tree building stage is structured as a state machine.
- * When comparing the implementation to
- * https://www.w3.org/TR/html5/syntax.html#tree-construction
- * note that each state is implemented as a function with a
- * name ending in `Mode` (because the HTML spec refers to them
- * as insertion modes).  The current insertion mode is held by
- * the $parseMode property.
- *
- * The following simplifications have been made:
- * - We handle body content only (ie, we start `in body`.)
- * - The document is never in "quirks mode".
- * - All occurrences of < and > have been entity escaped, so we
- *   can parse tags by simply splitting on those two characters.
- *   (This also simplifies the handling of < inside <textarea>.)
- *   The character < must not appear inside comments.
- *   Similarly, all attributes have been "cleaned" and are double-quoted
- *   and escaped.
- * - All null characters are assumed to have been removed.
- * - The following elements are disallowed: <html>, <head>, <body>, <frameset>,
- *   <frame>, <plaintext>, <xmp>, <iframe>,
- *   <noembed>, <noscript>, <script>, <title>.  As a result,
- *   further simplifications can be made:
- *   - `frameset-ok` is not tracked.
- *   - `head element pointer` is not tracked (but presumed non-null)
- *   - Tokenizer has only a single mode. (<textarea> wants RCDATA and
- *     <style>/<noframes> want RAWTEXT modes which we only loosely emulate.)
- *
- *   We generally mark places where we omit cases from the spec due to
- *   disallowed elements with a comment: `// OMITTED: <element-name>`.
- *
- *   The HTML spec keeps a flag during the parsing process to track
- *   whether or not a "parse error" has been encountered.  We don't
- *   bother to track that flag, we just implement the error-handling
- *   process as specified.
- *
- * @ingroup Parser
- * @since 1.27
- * @see https://html.spec.whatwg.org/multipage/syntax.html#tree-construction
- */
-class Balancer {
-       private $parseMode;
-       /** @var \Iterator */
-       private $bitsIterator;
-       private $allowedHtmlElements;
-       /** @var BalanceActiveFormattingElements */
-       private $afe;
-       /** @var BalanceStack */
-       private $stack;
-       private $strict;
-       private $allowComments;
-       private $config;
-
-       private $textIntegrationMode;
-       private $pendingTableText;
-       private $originalInsertionMode;
-       private $fragmentContext;
-       private $formElementPointer;
-       private $ignoreLinefeed;
-       private $inRCDATA;
-       private $inRAWTEXT;
-
-       /** @var callable|null */
-       private $processingCallback;
-       /** @var array */
-       private $processingArgs;
-
-       /**
-        * Valid HTML5 comments.
-        * Regex borrowed from Tim Starling's "remex-html" project.
-        */
-       const VALID_COMMENT_REGEX = "~ !--
-               (                           # 1. Comment match detector
-                       > | -> | # Invalid short close
-                       (                         # 2. Comment contents
-                               (?:
-                                       (?! --> )
-                                       (?! --!> )
-                                       (?! --! \z )
-                                       (?! -- \z )
-                                       (?! - \z )
-                                       .
-                               )*+
-                       )
-                       (                         # 3. Comment close
-                               --> |   # Normal close
-                               --!> |  # Comment end bang
-                               (                       # 4. Indicate matches requiring EOF
-                                       --! |                   # EOF in comment end bang state
-                                       -- |                    # EOF in comment end state
-                                       -  |                    # EOF in comment end dash state
-                                       (?#nothing)             # EOF in comment state
-                               )
-                       )
-               )
-               ([^<]*) \z                  # 5. Non-tag text after the comment
-               ~xs";
-
-       /**
-        * Create a new Balancer.
-        * @param array $config Balancer configuration.  Includes:
-        *     'strict' : boolean, defaults to false.
-        *         When true, enforces syntactic constraints on input:
-        *         all non-tag '<' must be escaped, all attributes must be
-        *         separated by a single space and double-quoted.  This is
-        *         consistent with the output of the Sanitizer.
-        *     'allowedHtmlElements' : array, defaults to null.
-        *         When present, the keys of this associative array give
-        *         the acceptable HTML tag names.  When not present, no
-        *         tag sanitization is done.
-        *     'tidyCompat' : boolean, defaults to false.
-        *         When true, the serialization algorithm is tweaked to
-        *         provide historical compatibility with the old "tidy"
-        *         program: <p>-wrapping is done to the children of
-        *         <body> and <blockquote> elements, and empty elements
-        *         are removed.  The <pre>/<listing>/<textarea> serialization
-        *         is also tweaked to allow lossless round trips.
-        *         (See: https://github.com/whatwg/html/issues/944)
-        *     'allowComments': boolean, defaults to true.
-        *         When true, allows HTML comments in the input.
-        *         The Sanitizer generally strips all comments, so if you
-        *         are running on sanitized output you can set this to
-        *         false to get a bit more performance.
-        */
-       public function __construct( array $config = [] ) {
-               $this->config = $config = $config + [
-                       'strict' => false,
-                       'allowedHtmlElements' => null,
-                       'tidyCompat' => false,
-                       'allowComments' => true,
-               ];
-               $this->allowedHtmlElements = $config['allowedHtmlElements'];
-               $this->strict = $config['strict'];
-               $this->allowComments = $config['allowComments'];
-               if ( $this->allowedHtmlElements !== null ) {
-                       // Sanity check!
-                       $bad = array_uintersect_assoc(
-                               $this->allowedHtmlElements,
-                               BalanceSets::$unsupportedSet[BalanceSets::HTML_NAMESPACE],
-                               function ( $a, $b ) {
-                                       // Ignore the values (just intersect the keys) by saying
-                                       // all values are equal to each other.
-                                       return 0;
-                               }
-                       );
-                       if ( count( $bad ) > 0 ) {
-                               $badstr = implode( ',', array_keys( $bad ) );
-                               throw new ParameterAssertionException(
-                                       '$config',
-                                       'Balance attempted with sanitization including ' .
-                                       "unsupported elements: {$badstr}"
-                               );
-                       }
-               }
-       }
-
-       /**
-        * Return a balanced HTML string for the HTML fragment given by $text,
-        * subject to the caveats listed in the class description.  The result
-        * will typically be idempotent -- that is, rebalancing the output
-        * would result in no change.
-        *
-        * @param string $text The markup to be balanced
-        * @param callable $processingCallback Callback to do any variable or
-        *   parameter replacements in HTML attributes values
-        * @param array|bool $processingArgs Arguments for the processing callback
-        * @return string The balanced markup
-        */
-       public function balance( $text, $processingCallback = null, $processingArgs = [] ) {
-               $this->parseMode = 'inBodyMode';
-               $this->bitsIterator = new ExplodeIterator( '<', $text );
-               $this->afe = new BalanceActiveFormattingElements();
-               $this->stack = new BalanceStack( $this->config );
-               $this->processingCallback = $processingCallback;
-               $this->processingArgs = $processingArgs;
-
-               $this->textIntegrationMode =
-                       $this->ignoreLinefeed =
-                       $this->inRCDATA =
-                       $this->inRAWTEXT = false;
-
-               // The stack is constructed with an <html> element already on it.
-               // Set this up as a fragment parsed with <body> as the context.
-               $this->fragmentContext =
-                       new BalanceElement( BalanceSets::HTML_NAMESPACE, 'body', [] );
-               $this->resetInsertionMode();
-               $this->formElementPointer = null;
-               for ( $e = $this->fragmentContext; $e != null; $e = $e->parent ) {
-                       if ( $e->isHtmlNamed( 'form' ) ) {
-                               $this->formElementPointer = $e;
-                               break;
-                       }
-               }
-
-               // First element is text not tag
-               $x = $this->bitsIterator->current();
-               $this->bitsIterator->next();
-               $this->insertToken( 'text', str_replace( '>', '&gt;', $x ) );
-               // Now process each tag.
-               while ( $this->bitsIterator->valid() ) {
-                       $this->advance();
-               }
-               $this->insertToken( 'eof', null );
-               $result = $this->stack->getOutput();
-               // Free memory before returning.
-               $this->bitsIterator = null;
-               $this->afe = null;
-               $this->stack = null;
-               $this->fragmentContext = null;
-               $this->formElementPointer = null;
-               return $result;
-       }
-
-       /**
-        * Pass a token to the tree builder.  The $token will be one of the
-        * strings "tag", "endtag", or "text".
-        */
-       private function insertToken( $token, $value, $attribs = null, $selfClose = false ) {
-               // validate tags against $unsupportedSet
-               if ( $token === 'tag' || $token === 'endtag' ) {
-                       if ( isset( BalanceSets::$unsupportedSet[BalanceSets::HTML_NAMESPACE][$value] ) ) {
-                               // As described in "simplifications" above, these tags are
-                               // not supported in the balancer.
-                               Assert::invariant(
-                                       !$this->strict,
-                                       "Unsupported $token <$value> found."
-                               );
-                               return false;
-                       }
-               } elseif ( $token === 'text' && $value === '' ) {
-                       // Don't actually inject the empty string as a text token.
-                       return true;
-               }
-               // Support pre/listing/textarea by suppressing initial linefeed
-               if ( $this->ignoreLinefeed ) {
-                       $this->ignoreLinefeed = false;
-                       if ( $token === 'text' ) {
-                               if ( $value[0] === "\n" ) {
-                                       if ( $value === "\n" ) {
-                                               // Nothing would be left, don't inject the empty string.
-                                               return true;
-                                       }
-                                       $value = substr( $value, 1 );
-                               }
-                       }
-               }
-               // Some hoops we have to jump through
-               $adjusted = $this->stack->adjustedCurrentNode( $this->fragmentContext );
-
-               // The spec calls this the "tree construction dispatcher".
-               $isForeign = true;
-               if (
-                       $this->stack->length() === 0 ||
-                       $adjusted->isHtml() ||
-                       $token === 'eof'
-               ) {
-                       $isForeign = false;
-               } elseif ( $adjusted->isMathmlTextIntegrationPoint() ) {
-                       if ( $token === 'text' ) {
-                               $isForeign = false;
-                       } elseif (
-                               $token === 'tag' &&
-                               $value !== 'mglyph' && $value !== 'malignmark'
-                       ) {
-                               $isForeign = false;
-                       }
-               } elseif (
-                       $adjusted->namespaceURI === BalanceSets::MATHML_NAMESPACE &&
-                       $adjusted->localName === 'annotation-xml' &&
-                       $token === 'tag' && $value === 'svg'
-               ) {
-                       $isForeign = false;
-               } elseif (
-                       $adjusted->isHtmlIntegrationPoint() &&
-                       ( $token === 'tag' || $token === 'text' )
-               ) {
-                       $isForeign = false;
-               }
-               if ( $isForeign ) {
-                       return $this->insertForeignToken( $token, $value, $attribs, $selfClose );
-               } else {
-                       $func = $this->parseMode;
-                       return $this->$func( $token, $value, $attribs, $selfClose );
-               }
-       }
-
-       private function insertForeignToken( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       $this->stack->insertText( $value );
-                       return true;
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'font':
-                                       if ( isset( $attribs['color'] )
-                                               || isset( $attribs['face'] )
-                                               || isset( $attribs['size'] )
-                                       ) {
-                                               break;
-                                       }
-                                       // otherwise, fall through
-                               case 'b':
-                               case 'big':
-                               case 'blockquote':
-                               case 'body':
-                               case 'br':
-                               case 'center':
-                               case 'code':
-                               case 'dd':
-                               case 'div':
-                               case 'dl':
-                               case 'dt':
-                               case 'em':
-                               case 'embed':
-                               case 'h1':
-                               case 'h2':
-                               case 'h3':
-                               case 'h4':
-                               case 'h5':
-                               case 'h6':
-                               case 'head':
-                               case 'hr':
-                               case 'i':
-                               case 'img':
-                               case 'li':
-                               case 'listing':
-                               case 'menu':
-                               case 'meta':
-                               case 'nobr':
-                               case 'ol':
-                               case 'p':
-                               case 'pre':
-                               case 'ruby':
-                               case 's':
-                               case 'small':
-                               case 'span':
-                               case 'strong':
-                               case 'strike':
-                               case 'sub':
-                               case 'sup':
-                               case 'table':
-                               case 'tt':
-                               case 'u':
-                               case 'ul':
-                               case 'var':
-                                       if ( $this->fragmentContext ) {
-                                               break;
-                                       }
-                                       while ( true ) {
-                                               $this->stack->pop();
-                                               $node = $this->stack->currentNode;
-                                               if (
-                                                       $node->isMathmlTextIntegrationPoint() ||
-                                                       $node->isHtmlIntegrationPoint() ||
-                                                       $node->isHtml()
-                                               ) {
-                                                       break;
-                                               }
-                                       }
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-                       }
-                       // "Any other start tag"
-                       $adjusted = ( $this->fragmentContext && $this->stack->length() === 1 ) ?
-                               $this->fragmentContext : $this->stack->currentNode;
-                       $this->stack->insertForeignElement(
-                               $adjusted->namespaceURI, $value, $attribs
-                       );
-                       if ( $selfClose ) {
-                               $this->stack->pop();
-                       }
-                       return true;
-               } elseif ( $token === 'endtag' ) {
-                       $first = true;
-                       foreach ( $this->stack as $i => $node ) {
-                               if ( $node->isHtml() && !$first ) {
-                                       // process the end tag as HTML
-                                       $func = $this->parseMode;
-                                       return $this->$func( $token, $value, $attribs, $selfClose );
-                               } elseif ( $i === 0 ) {
-                                       return true;
-                               } elseif ( $node->localName === $value ) {
-                                       $this->stack->popTag( $node );
-                                       return true;
-                               }
-                               $first = false;
-                       }
-               }
-       }
-
-       /**
-        * Grab the next "token" from $bitsIterator.  This is either a open/close
-        * tag or text or a comment, depending on whether the Sanitizer approves.
-        */
-       private function advance() {
-               $x = $this->bitsIterator->current();
-               $this->bitsIterator->next();
-               $regs = [];
-               // Handle comments.  These won't be generated by mediawiki (they
-               // are stripped in the Sanitizer) but may be generated by extensions.
-               if (
-                       $this->allowComments &&
-                       !( $this->inRCDATA || $this->inRAWTEXT ) &&
-                       preg_match( self::VALID_COMMENT_REGEX, $x, $regs, PREG_OFFSET_CAPTURE ) &&
-                       // verify EOF condition where necessary
-                       ( $regs[4][1] < 0 || !$this->bitsIterator->valid() )
-               ) {
-                       $contents = $regs[2][0];
-                       $rest = $regs[5][0];
-                       $this->insertToken( 'comment', $contents );
-                       $this->insertToken( 'text', str_replace( '>', '&gt;', $rest ) );
-                       return;
-               }
-               // $slash: Does the current element start with a '/'?
-               // $t: Current element name
-               // $attribStr: String between element name and >
-               // $brace: Ending '>' or '/>'
-               // $rest: Everything until the next element from the $bitsIterator
-               if ( preg_match( Sanitizer::ELEMENT_BITS_REGEX, $x, $regs ) ) {
-                       list( /* $qbar */, $slash, $t, $attribStr, $brace, $rest ) = $regs;
-                       $t = strtolower( $t );
-                       if ( $this->strict ) {
-                               // Verify that attributes are all properly double-quoted
-                               Assert::invariant(
-                                       preg_match(
-                                               '/^( [:_A-Z0-9][-.:_A-Z0-9]*="[^"]*")*[ ]*$/i', $attribStr
-                                       ),
-                                       "Bad attribute string found"
-                               );
-                       }
-               } else {
-                       Assert::invariant(
-                               !$this->strict, "< found which does not start a valid tag"
-                       );
-                       $slash = $t = $attribStr = $brace = $rest = null;
-               }
-               $goodTag = $t;
-               if ( $this->inRCDATA ) {
-                       if ( $slash && $t === $this->inRCDATA ) {
-                               $this->inRCDATA = false;
-                       } else {
-                               // No tags allowed; this emulates the "rcdata" tokenizer mode.
-                               $goodTag = false;
-                       }
-               }
-               if ( $this->inRAWTEXT ) {
-                       if ( $slash && $t === $this->inRAWTEXT ) {
-                               $this->inRAWTEXT = false;
-                       } else {
-                               // No tags allowed, no entity-escaping done.
-                               $goodTag = false;
-                       }
-               }
-               $sanitize = $this->allowedHtmlElements !== null;
-               if ( $sanitize ) {
-                       $goodTag = $t && isset( $this->allowedHtmlElements[$t] );
-               }
-               if ( $goodTag ) {
-                       if ( is_callable( $this->processingCallback ) ) {
-                               call_user_func_array( $this->processingCallback, [ &$attribStr, $this->processingArgs ] );
-                       }
-                       if ( $sanitize ) {
-                               $goodTag = Sanitizer::validateTag( $attribStr, $t );
-                       }
-               }
-               if ( $goodTag ) {
-                       if ( $sanitize ) {
-                               $attribs = Sanitizer::decodeTagAttributes( $attribStr );
-                               $attribs = Sanitizer::validateTagAttributes( $attribs, $t );
-                       } else {
-                               $attribs = Sanitizer::decodeTagAttributes( $attribStr );
-                       }
-                       $goodTag = $this->insertToken(
-                               $slash ? 'endtag' : 'tag', $t, $attribs, $brace === '/>'
-                       );
-               }
-               if ( $goodTag ) {
-                       $rest = str_replace( '>', '&gt;', $rest );
-                       $this->insertToken( 'text', str_replace( '>', '&gt;', $rest ) );
-               } elseif ( $this->inRAWTEXT ) {
-                       $this->insertToken( 'text', "<$x" );
-               } else {
-                       // bad tag; serialize entire thing as text.
-                       $this->insertToken( 'text', '&lt;' . str_replace( '>', '&gt;', $x ) );
-               }
-       }
-
-       private function switchMode( $mode ) {
-               Assert::parameter(
-                       substr( $mode, -4 ) === 'Mode', '$mode', 'should end in Mode'
-               );
-               $oldMode = $this->parseMode;
-               $this->parseMode = $mode;
-               return $oldMode;
-       }
-
-       private function switchModeAndReprocess( $mode, $token, $value, $attribs, $selfClose ) {
-               $this->switchMode( $mode );
-               return $this->insertToken( $token, $value, $attribs, $selfClose );
-       }
-
-       private function resetInsertionMode() {
-               $last = false;
-               foreach ( $this->stack as $i => $node ) {
-                       if ( $i === 0 ) {
-                               $last = true;
-                               if ( $this->fragmentContext ) {
-                                       $node = $this->fragmentContext;
-                               }
-                       }
-                       if ( $node->isHtml() ) {
-                               switch ( $node->localName ) {
-                                       case 'select':
-                                               $stackLength = $this->stack->length();
-                                               for ( $j = $i + 1; $j < $stackLength - 1; $j++ ) {
-                                                       $ancestor = $this->stack->node( $stackLength - $j - 1 );
-                                                       if ( $ancestor->isHtmlNamed( 'template' ) ) {
-                                                               break;
-                                                       }
-                                                       if ( $ancestor->isHtmlNamed( 'table' ) ) {
-                                                               $this->switchMode( 'inSelectInTableMode' );
-                                                               return;
-                                                       }
-                                               }
-                                               $this->switchMode( 'inSelectMode' );
-                                               return;
-                                       case 'tr':
-                                               $this->switchMode( 'inRowMode' );
-                                               return;
-                                       case 'tbody':
-                                       case 'tfoot':
-                                       case 'thead':
-                                               $this->switchMode( 'inTableBodyMode' );
-                                               return;
-                                       case 'caption':
-                                               $this->switchMode( 'inCaptionMode' );
-                                               return;
-                                       case 'colgroup':
-                                               $this->switchMode( 'inColumnGroupMode' );
-                                               return;
-                                       case 'table':
-                                               $this->switchMode( 'inTableMode' );
-                                               return;
-                                       case 'template':
-                                               $this->switchMode(
-                                                       array_slice( $this->templateInsertionModes, -1 )[0]
-                                               );
-                                               return;
-                                       case 'body':
-                                               $this->switchMode( 'inBodyMode' );
-                                               return;
-                                       // OMITTED: <frameset>
-                                       // OMITTED: <html>
-                                       // OMITTED: <head>
-                                       default:
-                                               if ( !$last ) {
-                                                       // OMITTED: <head>
-                                                       if ( $node->isA( BalanceSets::$tableCellSet ) ) {
-                                                               $this->switchMode( 'inCellMode' );
-                                                               return;
-                                                       }
-                                               }
-                               }
-                       }
-                       if ( $last ) {
-                               $this->switchMode( 'inBodyMode' );
-                               return;
-                       }
-               }
-       }
-
-       private function stopParsing() {
-               // Most of the spec methods are inapplicable, other than step 2:
-               // "pop all the nodes off the stack of open elements".
-               // We're going to keep the top-most <html> element on the stack, though.
-
-               // Clear the AFE list first, otherwise the element objects will stay live
-               // during serialization, potentially using O(N^2) memory. Note that
-               // popping the stack will never result in reconstructing the active
-               // formatting elements.
-               $this->afe = null;
-               $this->stack->popTo( 1 );
-       }
-
-       private function parseRawText( $value, $attribs = null ) {
-               $this->stack->insertHTMLElement( $value, $attribs );
-               $this->inRAWTEXT = $value;
-               $this->originalInsertionMode = $this->switchMode( 'inTextMode' );
-               return true;
-       }
-
-       private function inTextMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       $this->stack->insertText( $value );
-                       return true;
-               } elseif ( $token === 'eof' ) {
-                       $this->stack->pop();
-                       return $this->switchModeAndReprocess(
-                               $this->originalInsertionMode, $token, $value, $attribs, $selfClose
-                       );
-               } elseif ( $token === 'endtag' ) {
-                       $this->stack->pop();
-                       $this->switchMode( $this->originalInsertionMode );
-                       return true;
-               }
-               return true;
-       }
-
-       private function inHeadMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       if ( preg_match( '/^[\x09\x0A\x0C\x0D\x20]+/', $value, $matches ) ) {
-                               $this->stack->insertText( $matches[0] );
-                               $value = substr( $value, strlen( $matches[0] ) );
-                       }
-                       if ( strlen( $value ) === 0 ) {
-                               return true; // All text handled.
-                       }
-                       // Fall through to handle non-whitespace below.
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'meta':
-                                       // OMITTED: in a full HTML parser, this might change the encoding.
-                                       // falls through
-                               // OMITTED: <html>
-                               case 'base':
-                               case 'basefont':
-                               case 'bgsound':
-                               case 'link':
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       return true;
-                               // OMITTED: <title>
-                               // OMITTED: <noscript>
-                               case 'noframes':
-                               case 'style':
-                                       return $this->parseRawText( $value, $attribs );
-                               // OMITTED: <script>
-                               case 'template':
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->afe->insertMarker();
-                                       // OMITTED: frameset_ok
-                                       $this->switchMode( 'inTemplateMode' );
-                                       $this->templateInsertionModes[] = $this->parseMode;
-                                       return true;
-                               // OMITTED: <head>
-                       }
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               // OMITTED: <head>
-                               // OMITTED: <body>
-                               // OMITTED: <html>
-                               case 'br':
-                                       break; // handle at the bottom of the function
-                               case 'template':
-                                       if ( $this->stack->indexOf( $value ) < 0 ) {
-                                               return true; // Ignore the token.
-                                       }
-                                       $this->stack->generateImpliedEndTags( null, true /* thorough */ );
-                                       $this->stack->popTag( $value );
-                                       $this->afe->clearToMarker();
-                                       array_pop( $this->templateInsertionModes );
-                                       $this->resetInsertionMode();
-                                       return true;
-                               default:
-                                       // ignore any other end tag
-                                       return true;
-                       }
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               }
-
-               // If not handled above
-               $this->inHeadMode( 'endtag', 'head' ); // synthetic </head>
-               // Then redo this one
-               return $this->insertToken( $token, $value, $attribs, $selfClose );
-       }
-
-       private function inBodyMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       $this->afe->reconstruct( $this->stack );
-                       $this->stack->insertText( $value );
-                       return true;
-               } elseif ( $token === 'eof' ) {
-                       if ( !empty( $this->templateInsertionModes ) ) {
-                               return $this->inTemplateMode( $token, $value, $attribs, $selfClose );
-                       }
-                       $this->stopParsing();
-                       return true;
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               // OMITTED: <html>
-                               case 'base':
-                               case 'basefont':
-                               case 'bgsound':
-                               case 'link':
-                               case 'meta':
-                               case 'noframes':
-                               // OMITTED: <script>
-                               case 'style':
-                               case 'template':
-                               // OMITTED: <title>
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                               // OMITTED: <body>
-                               // OMITTED: <frameset>
-
-                               case 'address':
-                               case 'article':
-                               case 'aside':
-                               case 'blockquote':
-                               case 'center':
-                               case 'details':
-                               case 'dialog':
-                               case 'dir':
-                               case 'div':
-                               case 'dl':
-                               case 'fieldset':
-                               case 'figcaption':
-                               case 'figure':
-                               case 'footer':
-                               case 'header':
-                               case 'hgroup':
-                               case 'main':
-                               case 'nav':
-                               case 'ol':
-                               case 'p':
-                               case 'section':
-                               case 'summary':
-                               case 'ul':
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'menu':
-                                       if ( $this->stack->inButtonScope( "p" ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'h1':
-                               case 'h2':
-                               case 'h3':
-                               case 'h4':
-                               case 'h5':
-                               case 'h6':
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       if ( $this->stack->currentNode->isA( BalanceSets::$headingSet ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'pre':
-                               case 'listing':
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->ignoreLinefeed = true;
-                                       // OMITTED: frameset_ok
-                                       return true;
-
-                               case 'form':
-                                       if (
-                                               $this->formElementPointer &&
-                                               $this->stack->indexOf( 'template' ) < 0
-                                       ) {
-                                               return true; // in a form, not in a template.
-                                       }
-                                       if ( $this->stack->inButtonScope( "p" ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $elt = $this->stack->insertHTMLElement( $value, $attribs );
-                                       if ( $this->stack->indexOf( 'template' ) < 0 ) {
-                                               $this->formElementPointer = $elt;
-                                       }
-                                       return true;
-
-                               case 'li':
-                                       // OMITTED: frameset_ok
-                                       foreach ( $this->stack as $node ) {
-                                               if ( $node->isHtmlNamed( 'li' ) ) {
-                                                       $this->inBodyMode( 'endtag', 'li' );
-                                                       break;
-                                               }
-                                               if (
-                                                       $node->isA( BalanceSets::$specialSet ) &&
-                                                       !$node->isA( BalanceSets::$addressDivPSet )
-                                               ) {
-                                                       break;
-                                               }
-                                       }
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'dd':
-                               case 'dt':
-                                       // OMITTED: frameset_ok
-                                       foreach ( $this->stack as $node ) {
-                                               if ( $node->isHtmlNamed( 'dd' ) ) {
-                                                       $this->inBodyMode( 'endtag', 'dd' );
-                                                       break;
-                                               }
-                                               if ( $node->isHtmlNamed( 'dt' ) ) {
-                                                       $this->inBodyMode( 'endtag', 'dt' );
-                                                       break;
-                                               }
-                                               if (
-                                                       $node->isA( BalanceSets::$specialSet ) &&
-                                                       !$node->isA( BalanceSets::$addressDivPSet )
-                                               ) {
-                                                       break;
-                                               }
-                                       }
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               // OMITTED: <plaintext>
-
-                               case 'button':
-                                       if ( $this->stack->inScope( 'button' ) ) {
-                                               $this->inBodyMode( 'endtag', 'button' );
-                                               return $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'a':
-                                       $activeElement = $this->afe->findElementByTag( 'a' );
-                                       if ( $activeElement ) {
-                                               $this->inBodyMode( 'endtag', 'a' );
-                                               if ( $this->afe->isInList( $activeElement ) ) {
-                                                       $this->afe->remove( $activeElement );
-                                                       // Don't flatten here, since when we fall
-                                                       // through below we might foster parent
-                                                       // the new <a> tag inside this one.
-                                                       $this->stack->removeElement( $activeElement, false );
-                                               }
-                                       }
-                                       // Falls through
-                               case 'b':
-                               case 'big':
-                               case 'code':
-                               case 'em':
-                               case 'font':
-                               case 'i':
-                               case 's':
-                               case 'small':
-                               case 'strike':
-                               case 'strong':
-                               case 'tt':
-                               case 'u':
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
-                                       return true;
-
-                               case 'nobr':
-                                       $this->afe->reconstruct( $this->stack );
-                                       if ( $this->stack->inScope( 'nobr' ) ) {
-                                               $this->inBodyMode( 'endtag', 'nobr' );
-                                               $this->afe->reconstruct( $this->stack );
-                                       }
-                                       $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
-                                       return true;
-
-                               case 'applet':
-                               case 'marquee':
-                               case 'object':
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->afe->insertMarker();
-                                       // OMITTED: frameset_ok
-                                       return true;
-
-                               case 'table':
-                                       // The document is never in "quirks mode"; see simplifications
-                                       // above.
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       // OMITTED: frameset_ok
-                                       $this->switchMode( 'inTableMode' );
-                                       return true;
-
-                               case 'area':
-                               case 'br':
-                               case 'embed':
-                               case 'img':
-                               case 'keygen':
-                               case 'wbr':
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       // OMITTED: frameset_ok
-                                       return true;
-
-                               case 'input':
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       // OMITTED: frameset_ok
-                                       // (hence we don't need to examine the tag's "type" attribute)
-                                       return true;
-
-                               case 'param':
-                               case 'source':
-                               case 'track':
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       return true;
-
-                               case 'hr':
-                                       if ( $this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'endtag', 'p' );
-                                       }
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       return true;
-
-                               case 'image':
-                                       // warts!
-                                       return $this->inBodyMode( $token, 'img', $attribs, $selfClose );
-
-                               case 'textarea':
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->ignoreLinefeed = true;
-                                       $this->inRCDATA = $value; // emulate rcdata tokenizer mode
-                                       // OMITTED: frameset_ok
-                                       return true;
-
-                               // OMITTED: <xmp>
-                               // OMITTED: <iframe>
-                               // OMITTED: <noembed>
-                               // OMITTED: <noscript>
-
-                               case 'select':
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       switch ( $this->parseMode ) {
-                                               case 'inTableMode':
-                                               case 'inCaptionMode':
-                                               case 'inTableBodyMode':
-                                               case 'inRowMode':
-                                               case 'inCellMode':
-                                                       $this->switchMode( 'inSelectInTableMode' );
-                                                       return true;
-                                               default:
-                                                       $this->switchMode( 'inSelectMode' );
-                                                       return true;
-                                       }
-
-                               case 'optgroup':
-                               case 'option':
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
-                                               $this->inBodyMode( 'endtag', 'option' );
-                                       }
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'menuitem':
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->afe->reconstruct( $this->stack );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'rb':
-                               case 'rtc':
-                                       if ( $this->stack->inScope( 'ruby' ) ) {
-                                               $this->stack->generateImpliedEndTags();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'rp':
-                               case 'rt':
-                                       if ( $this->stack->inScope( 'ruby' ) ) {
-                                               $this->stack->generateImpliedEndTags( 'rtc' );
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-
-                               case 'math':
-                                       $this->afe->reconstruct( $this->stack );
-                                       // We skip the spec's "adjust MathML attributes" and
-                                       // "adjust foreign attributes" steps, since the browser will
-                                       // do this later when it parses the output and it doesn't affect
-                                       // balancing.
-                                       $this->stack->insertForeignElement(
-                                               BalanceSets::MATHML_NAMESPACE, $value, $attribs
-                                       );
-                                       if ( $selfClose ) {
-                                               // emit explicit </math> tag.
-                                               $this->stack->pop();
-                                       }
-                                       return true;
-
-                               case 'svg':
-                                       $this->afe->reconstruct( $this->stack );
-                                       // We skip the spec's "adjust SVG attributes" and
-                                       // "adjust foreign attributes" steps, since the browser will
-                                       // do this later when it parses the output and it doesn't affect
-                                       // balancing.
-                                       $this->stack->insertForeignElement(
-                                               BalanceSets::SVG_NAMESPACE, $value, $attribs
-                                       );
-                                       if ( $selfClose ) {
-                                               // emit explicit </svg> tag.
-                                               $this->stack->pop();
-                                       }
-                                       return true;
-
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <frame>
-                               case 'head':
-                               case 'tbody':
-                               case 'td':
-                               case 'tfoot':
-                               case 'th':
-                               case 'thead':
-                               case 'tr':
-                                       // Ignore table tags if we're not inTableMode
-                                       return true;
-                       }
-
-                       // Handle any other start tag here
-                       $this->afe->reconstruct( $this->stack );
-                       $this->stack->insertHTMLElement( $value, $attribs );
-                       return true;
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               // </body>,</html> are unsupported.
-
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-
-                               case 'address':
-                               case 'article':
-                               case 'aside':
-                               case 'blockquote':
-                               case 'button':
-                               case 'center':
-                               case 'details':
-                               case 'dialog':
-                               case 'dir':
-                               case 'div':
-                               case 'dl':
-                               case 'fieldset':
-                               case 'figcaption':
-                               case 'figure':
-                               case 'footer':
-                               case 'header':
-                               case 'hgroup':
-                               case 'listing':
-                               case 'main':
-                               case 'menu':
-                               case 'nav':
-                               case 'ol':
-                               case 'pre':
-                               case 'section':
-                               case 'summary':
-                               case 'ul':
-                                       // Ignore if there is not a matching open tag
-                                       if ( !$this->stack->inScope( $value ) ) {
-                                               return true;
-                                       }
-                                       $this->stack->generateImpliedEndTags();
-                                       $this->stack->popTag( $value );
-                                       return true;
-
-                               case 'form':
-                                       if ( $this->stack->indexOf( 'template' ) < 0 ) {
-                                               $openform = $this->formElementPointer;
-                                               $this->formElementPointer = null;
-                                               if ( !$openform || !$this->stack->inScope( $openform ) ) {
-                                                       return true;
-                                               }
-                                               $this->stack->generateImpliedEndTags();
-                                               // Don't flatten yet if we're removing a <form> element
-                                               // out-of-order. (eg. `<form><div></form>`)
-                                               $flatten = ( $this->stack->currentNode === $openform );
-                                               $this->stack->removeElement( $openform, $flatten );
-                                       } else {
-                                               if ( !$this->stack->inScope( 'form' ) ) {
-                                                       return true;
-                                               }
-                                               $this->stack->generateImpliedEndTags();
-                                               $this->stack->popTag( 'form' );
-                                       }
-                                       return true;
-
-                               case 'p':
-                                       if ( !$this->stack->inButtonScope( 'p' ) ) {
-                                               $this->inBodyMode( 'tag', 'p', [] );
-                                               return $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       $this->stack->generateImpliedEndTags( $value );
-                                       $this->stack->popTag( $value );
-                                       return true;
-
-                               case 'li':
-                                       if ( !$this->stack->inListItemScope( $value ) ) {
-                                               return true; // ignore
-                                       }
-                                       $this->stack->generateImpliedEndTags( $value );
-                                       $this->stack->popTag( $value );
-                                       return true;
-
-                               case 'dd':
-                               case 'dt':
-                                       if ( !$this->stack->inScope( $value ) ) {
-                                               return true; // ignore
-                                       }
-                                       $this->stack->generateImpliedEndTags( $value );
-                                       $this->stack->popTag( $value );
-                                       return true;
-
-                               case 'h1':
-                               case 'h2':
-                               case 'h3':
-                               case 'h4':
-                               case 'h5':
-                               case 'h6':
-                                       if ( !$this->stack->inScope( BalanceSets::$headingSet ) ) {
-                                               return true; // ignore
-                                       }
-                                       $this->stack->generateImpliedEndTags();
-                                       $this->stack->popTag( BalanceSets::$headingSet );
-                                       return true;
-
-                               case 'sarcasm':
-                                       // Take a deep breath, then:
-                                       break;
-
-                               case 'a':
-                               case 'b':
-                               case 'big':
-                               case 'code':
-                               case 'em':
-                               case 'font':
-                               case 'i':
-                               case 'nobr':
-                               case 's':
-                               case 'small':
-                               case 'strike':
-                               case 'strong':
-                               case 'tt':
-                               case 'u':
-                                       if ( $this->stack->adoptionAgency( $value, $this->afe ) ) {
-                                               return true; // If we did something, we're done.
-                                       }
-                                       break; // Go to the "any other end tag" case.
-
-                               case 'applet':
-                               case 'marquee':
-                               case 'object':
-                                       if ( !$this->stack->inScope( $value ) ) {
-                                               return true; // ignore
-                                       }
-                                       $this->stack->generateImpliedEndTags();
-                                       $this->stack->popTag( $value );
-                                       $this->afe->clearToMarker();
-                                       return true;
-
-                               case 'br':
-                                       // Turn </br> into <br>
-                                       return $this->inBodyMode( 'tag', $value, [] );
-                       }
-
-                       // Any other end tag goes here
-                       foreach ( $this->stack as $i => $node ) {
-                               if ( $node->isHtmlNamed( $value ) ) {
-                                       $this->stack->generateImpliedEndTags( $value );
-                                       $this->stack->popTo( $i ); // including $i
-                                       break;
-                               } elseif ( $node->isA( BalanceSets::$specialSet ) ) {
-                                       return true; // ignore this close token.
-                               }
-                       }
-                       return true;
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               } else {
-                       Assert::invariant( false, "Bad token type: $token" );
-               }
-       }
-
-       private function inTableMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       if ( $this->textIntegrationMode ) {
-                               return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-                       } elseif ( $this->stack->currentNode->isA( BalanceSets::$tableSectionRowSet ) ) {
-                               $this->pendingTableText = '';
-                               $this->originalInsertionMode = $this->parseMode;
-                               return $this->switchModeAndReprocess( 'inTableTextMode',
-                                       $token, $value, $attribs, $selfClose );
-                       }
-                       // fall through to default case.
-               } elseif ( $token === 'eof' ) {
-                       $this->stopParsing();
-                       return true;
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'caption':
-                                       $this->afe->insertMarker();
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->switchMode( 'inCaptionMode' );
-                                       return true;
-                               case 'colgroup':
-                                       $this->stack->clearToContext( BalanceSets::$tableContextSet );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->switchMode( 'inColumnGroupMode' );
-                                       return true;
-                               case 'col':
-                                       $this->inTableMode( 'tag', 'colgroup', [] );
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                                       $this->stack->clearToContext( BalanceSets::$tableContextSet );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->switchMode( 'inTableBodyMode' );
-                                       return true;
-                               case 'td':
-                               case 'th':
-                               case 'tr':
-                                       $this->inTableMode( 'tag', 'tbody', [] );
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-                               case 'table':
-                                       if ( !$this->stack->inTableScope( $value ) ) {
-                                               return true; // Ignore this tag.
-                                       }
-                                       $this->inTableMode( 'endtag', $value );
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-
-                               case 'style':
-                               // OMITTED: <script>
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-
-                               case 'input':
-                                       if ( !isset( $attribs['type'] ) || strcasecmp( $attribs['type'], 'hidden' ) !== 0 ) {
-                                               break; // Handle this as "everything else"
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       return true;
-
-                               case 'form':
-                                       if (
-                                               $this->formElementPointer ||
-                                               $this->stack->indexOf( 'template' ) >= 0
-                                       ) {
-                                               return true; // ignore this token
-                                       }
-                                       $this->formElementPointer =
-                                               $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->popTag( $this->formElementPointer );
-                                       return true;
-                       }
-                       // Fall through for "anything else" clause.
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'table':
-                                       if ( !$this->stack->inTableScope( $value ) ) {
-                                               return true; // Ignore.
-                                       }
-                                       $this->stack->popTag( $value );
-                                       $this->resetInsertionMode();
-                                       return true;
-                               // OMITTED: <body>
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <html>
-                               case 'tbody':
-                               case 'td':
-                               case 'tfoot':
-                               case 'th':
-                               case 'thead':
-                               case 'tr':
-                                       return true; // Ignore the token.
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-                       // Fall through for "anything else" clause.
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               }
-               // This is the "anything else" case:
-               $this->stack->fosterParentMode = true;
-               $this->inBodyMode( $token, $value, $attribs, $selfClose );
-               $this->stack->fosterParentMode = false;
-               return true;
-       }
-
-       private function inTableTextMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       $this->pendingTableText .= $value;
-                       return true;
-               }
-               // Non-text token:
-               $text = $this->pendingTableText;
-               $this->pendingTableText = '';
-               if ( preg_match( '/[^\x09\x0A\x0C\x0D\x20]/', $text ) ) {
-                       // This should match the "anything else" case inTableMode
-                       $this->stack->fosterParentMode = true;
-                       $this->inBodyMode( 'text', $text );
-                       $this->stack->fosterParentMode = false;
-               } else {
-                       // Pending text is just whitespace.
-                       $this->stack->insertText( $text );
-               }
-               return $this->switchModeAndReprocess(
-                       $this->originalInsertionMode, $token, $value, $attribs, $selfClose
-               );
-       }
-
-       // helper for inCaptionMode
-       private function endCaption() {
-               if ( !$this->stack->inTableScope( 'caption' ) ) {
-                       return false;
-               }
-               $this->stack->generateImpliedEndTags();
-               $this->stack->popTag( 'caption' );
-               $this->afe->clearToMarker();
-               $this->switchMode( 'inTableMode' );
-               return true;
-       }
-
-       private function inCaptionMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               case 'tbody':
-                               case 'td':
-                               case 'tfoot':
-                               case 'th':
-                               case 'thead':
-                               case 'tr':
-                                       if ( $this->endCaption() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                       }
-                       // Fall through to "anything else" case.
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'caption':
-                                       $this->endCaption();
-                                       return true;
-                               case 'table':
-                                       if ( $this->endCaption() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                               case 'body':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <html>
-                               case 'tbody':
-                               case 'td':
-                               case 'tfoot':
-                               case 'th':
-                               case 'thead':
-                               case 'tr':
-                                       // Ignore the token
-                                       return true;
-                       }
-                       // Fall through to "anything else" case.
-               }
-               // The Anything Else case
-               return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-       }
-
-       private function inColumnGroupMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       if ( preg_match( '/^[\x09\x0A\x0C\x0D\x20]+/', $value, $matches ) ) {
-                               $this->stack->insertText( $matches[0] );
-                               $value = substr( $value, strlen( $matches[0] ) );
-                       }
-                       if ( strlen( $value ) === 0 ) {
-                               return true; // All text handled.
-                       }
-                       // Fall through to handle non-whitespace below.
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               // OMITTED: <html>
-                               case 'col':
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->stack->pop();
-                                       return true;
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-                       // Fall through for "anything else".
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'colgroup':
-                                       if ( !$this->stack->currentNode->isHtmlNamed( 'colgroup' ) ) {
-                                               return true; // Ignore the token.
-                                       }
-                                       $this->stack->pop();
-                                       $this->switchMode( 'inTableMode' );
-                                       return true;
-                               case 'col':
-                                       return true; // Ignore the token.
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-                       // Fall through for "anything else".
-               } elseif ( $token === 'eof' ) {
-                       return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               }
-
-               // Anything else
-               if ( !$this->stack->currentNode->isHtmlNamed( 'colgroup' ) ) {
-                       return true; // Ignore the token.
-               }
-               $this->inColumnGroupMode( 'endtag', 'colgroup' );
-               return $this->insertToken( $token, $value, $attribs, $selfClose );
-       }
-
-       // Helper function for inTableBodyMode
-       private function endSection() {
-               if ( !(
-                       $this->stack->inTableScope( 'tbody' ) ||
-                       $this->stack->inTableScope( 'thead' ) ||
-                       $this->stack->inTableScope( 'tfoot' )
-               ) ) {
-                       return false;
-               }
-               $this->stack->clearToContext( BalanceSets::$tableBodyContextSet );
-               $this->stack->pop();
-               $this->switchMode( 'inTableMode' );
-               return true;
-       }
-       private function inTableBodyMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'tr':
-                                       $this->stack->clearToContext( BalanceSets::$tableBodyContextSet );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->switchMode( 'inRowMode' );
-                                       return true;
-                               case 'th':
-                               case 'td':
-                                       $this->inTableBodyMode( 'tag', 'tr', [] );
-                                       $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       return true;
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                                       if ( $this->endSection() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                       }
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'table':
-                                       if ( $this->endSection() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                                       if ( $this->stack->inTableScope( $value ) ) {
-                                               $this->endSection();
-                                       }
-                                       return true;
-                               // OMITTED: <body>
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <html>
-                               case 'td':
-                               case 'th':
-                               case 'tr':
-                                       return true; // Ignore the token.
-                       }
-               }
-               // Anything else:
-               return $this->inTableMode( $token, $value, $attribs, $selfClose );
-       }
-
-       // Helper function for inRowMode
-       private function endRow() {
-               if ( !$this->stack->inTableScope( 'tr' ) ) {
-                       return false;
-               }
-               $this->stack->clearToContext( BalanceSets::$tableRowContextSet );
-               $this->stack->pop();
-               $this->switchMode( 'inTableBodyMode' );
-               return true;
-       }
-       private function inRowMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'th':
-                               case 'td':
-                                       $this->stack->clearToContext( BalanceSets::$tableRowContextSet );
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       $this->switchMode( 'inCellMode' );
-                                       $this->afe->insertMarker();
-                                       return true;
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                               case 'tr':
-                                       if ( $this->endRow() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                       }
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'tr':
-                                       $this->endRow();
-                                       return true;
-                               case 'table':
-                                       if ( $this->endRow() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                                       if (
-                                               $this->stack->inTableScope( $value ) &&
-                                               $this->endRow()
-                                       ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                               // OMITTED: <body>
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <html>
-                               case 'td':
-                               case 'th':
-                                       return true; // Ignore the token.
-                       }
-               }
-               // Anything else:
-               return $this->inTableMode( $token, $value, $attribs, $selfClose );
-       }
-
-       // Helper for inCellMode
-       private function endCell() {
-               if ( $this->stack->inTableScope( 'td' ) ) {
-                       $this->inCellMode( 'endtag', 'td' );
-                       return true;
-               } elseif ( $this->stack->inTableScope( 'th' ) ) {
-                       $this->inCellMode( 'endtag', 'th' );
-                       return true;
-               } else {
-                       return false;
-               }
-       }
-       private function inCellMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               case 'tbody':
-                               case 'td':
-                               case 'tfoot':
-                               case 'th':
-                               case 'thead':
-                               case 'tr':
-                                       if ( $this->endCell() ) {
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                       }
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'td':
-                               case 'th':
-                                       if ( $this->stack->inTableScope( $value ) ) {
-                                               $this->stack->generateImpliedEndTags();
-                                               $this->stack->popTag( $value );
-                                               $this->afe->clearToMarker();
-                                               $this->switchMode( 'inRowMode' );
-                                       }
-                                       return true;
-                               // OMITTED: <body>
-                               case 'caption':
-                               case 'col':
-                               case 'colgroup':
-                               // OMITTED: <html>
-                                       return true;
-
-                               case 'table':
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                               case 'tr':
-                                       if ( $this->stack->inTableScope( $value ) ) {
-                                               $this->stack->generateImpliedEndTags();
-                                               $this->stack->popTag( BalanceSets::$tableCellSet );
-                                               $this->afe->clearToMarker();
-                                               $this->switchMode( 'inRowMode' );
-                                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                       }
-               }
-               // Anything else:
-               return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-       }
-
-       private function inSelectMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' ) {
-                       $this->stack->insertText( $value );
-                       return true;
-               } elseif ( $token === 'eof' ) {
-                       return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               // OMITTED: <html>
-                               case 'option':
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-                               case 'optgroup':
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       $this->stack->insertHTMLElement( $value, $attribs );
-                                       return true;
-                               case 'select':
-                                       $this->inSelectMode( 'endtag', $value ); // treat it like endtag
-                                       return true;
-                               case 'input':
-                               case 'keygen':
-                               case 'textarea':
-                                       if ( !$this->stack->inSelectScope( 'select' ) ) {
-                                               return true; // ignore token (fragment case)
-                                       }
-                                       $this->inSelectMode( 'endtag', 'select' );
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-                               case 'script':
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'optgroup':
-                                       if (
-                                               $this->stack->currentNode->isHtmlNamed( 'option' ) &&
-                                               $this->stack->length() >= 2 &&
-                                               $this->stack->node( $this->stack->length() - 2 )->isHtmlNamed( 'optgroup' )
-                                       ) {
-                                               $this->stack->pop();
-                                       }
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       return true;
-                               case 'option':
-                                       if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
-                                               $this->stack->pop();
-                                       }
-                                       return true;
-                               case 'select':
-                                       if ( !$this->stack->inSelectScope( $value ) ) {
-                                               return true; // fragment case
-                                       }
-                                       $this->stack->popTag( $value );
-                                       $this->resetInsertionMode();
-                                       return true;
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-               } elseif ( $token === 'comment' ) {
-                       $this->stack->insertComment( $value );
-                       return true;
-               }
-               // anything else: just ignore the token
-               return true;
-       }
-
-       private function inSelectInTableMode( $token, $value, $attribs = null, $selfClose = false ) {
-               switch ( $value ) {
-                       case 'caption':
-                       case 'table':
-                       case 'tbody':
-                       case 'tfoot':
-                       case 'thead':
-                       case 'tr':
-                       case 'td':
-                       case 'th':
-                               if ( $token === 'tag' ) {
-                                       $this->inSelectInTableMode( 'endtag', 'select' );
-                                       return $this->insertToken( $token, $value, $attribs, $selfClose );
-                               } elseif ( $token === 'endtag' ) {
-                                       if ( $this->stack->inTableScope( $value ) ) {
-                                               $this->inSelectInTableMode( 'endtag', 'select' );
-                                               return $this->insertToken( $token, $value, $attribs, $selfClose );
-                                       }
-                                       return true;
-                               }
-               }
-               // anything else
-               return $this->inSelectMode( $token, $value, $attribs, $selfClose );
-       }
-
-       private function inTemplateMode( $token, $value, $attribs = null, $selfClose = false ) {
-               if ( $token === 'text' || $token === 'comment' ) {
-                       return $this->inBodyMode( $token, $value, $attribs, $selfClose );
-               } elseif ( $token === 'eof' ) {
-                       if ( $this->stack->indexOf( 'template' ) < 0 ) {
-                               $this->stopParsing();
-                       } else {
-                               $this->stack->popTag( 'template' );
-                               $this->afe->clearToMarker();
-                               array_pop( $this->templateInsertionModes );
-                               $this->resetInsertionMode();
-                               $this->insertToken( $token, $value, $attribs, $selfClose );
-                       }
-                       return true;
-               } elseif ( $token === 'tag' ) {
-                       switch ( $value ) {
-                               case 'base':
-                               case 'basefont':
-                               case 'bgsound':
-                               case 'link':
-                               case 'meta':
-                               case 'noframes':
-                               // OMITTED: <script>
-                               case 'style':
-                               case 'template':
-                               // OMITTED: <title>
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-
-                               case 'caption':
-                               case 'colgroup':
-                               case 'tbody':
-                               case 'tfoot':
-                               case 'thead':
-                                       return $this->switchModeAndReprocess(
-                                               'inTableMode', $token, $value, $attribs, $selfClose
-                                       );
-
-                               case 'col':
-                                       return $this->switchModeAndReprocess(
-                                               'inColumnGroupMode', $token, $value, $attribs, $selfClose
-                                       );
-
-                               case 'tr':
-                                       return $this->switchModeAndReprocess(
-                                               'inTableBodyMode', $token, $value, $attribs, $selfClose
-                                       );
-
-                               case 'td':
-                               case 'th':
-                                       return $this->switchModeAndReprocess(
-                                               'inRowMode', $token, $value, $attribs, $selfClose
-                                       );
-                       }
-                       return $this->switchModeAndReprocess(
-                               'inBodyMode', $token, $value, $attribs, $selfClose
-                       );
-               } elseif ( $token === 'endtag' ) {
-                       switch ( $value ) {
-                               case 'template':
-                                       return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-                       }
-                       return true;
-               } else {
-                       Assert::invariant( false, "Bad token type: $token" );
-               }
-       }
-}
diff --git a/includes/tidy/Html5Depurate.php b/includes/tidy/Html5Depurate.php
deleted file mode 100644 (file)
index c6acd66..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-namespace MediaWiki\Tidy;
-
-use MWHttpRequest;
-use Exception;
-
-class Html5Depurate extends TidyDriverBase {
-       public function __construct( array $config ) {
-               parent::__construct( $config + [
-                       'url' => 'http://localhost:4339/document',
-                       'timeout' => 10,
-                       'connectTimeout' => 0.5,
-               ] );
-       }
-
-       public function tidy( $text ) {
-               $wrappedtext = '<!DOCTYPE html><html>' .
-                       '<body>' . $text . '</body></html>';
-
-               $req = MWHttpRequest::factory( $this->config['url'],
-                       [
-                               'method' => 'POST',
-                               'timeout' => $this->config['timeout'],
-                               'connectTimeout' => $this->config['connectTimeout'],
-                               'postData' => [
-                                       'text' => $wrappedtext
-                               ]
-                       ] );
-               $status = $req->execute();
-               if ( !$status->isOK() ) {
-                       throw new Exception( "Error contacting depurate service: "
-                               . $status->getWikiText( false, false, 'en' ) );
-               } elseif ( $req->getStatus() !== 200 ) {
-                       throw new Exception( "Depurate returned error: " . $status->getWikiText( false, false, 'en' ) );
-               }
-               $result = $req->getContent();
-               $startBody = strpos( $result, "<body>" );
-               $endBody = strrpos( $result, "</body>" );
-               if ( $startBody !== false && $endBody !== false && $endBody > $startBody ) {
-                       $startBody += strlen( "<body>" );
-                       return substr( $result, $startBody, $endBody - $startBody );
-               } else {
-                       return $text . "\n<!-- Html5Depurate returned an invalid result -->";
-               }
-       }
-}
diff --git a/includes/tidy/Html5Internal.php b/includes/tidy/Html5Internal.php
deleted file mode 100644 (file)
index 4ad8200..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace MediaWiki\Tidy;
-
-class Html5Internal extends TidyDriverBase {
-       private $balancer;
-       public function __construct( array $config ) {
-               parent::__construct( $config + [
-                       'strict' => true,
-                       'tidyCompat' => true,
-               ] );
-               $this->balancer = new Balancer( $this->config );
-       }
-
-       public function tidy( $text ) {
-               return $this->balancer->balance( $text );
-       }
-}
index f102d49..6b8153c 100644 (file)
@@ -261,6 +261,15 @@ class BotPassword implements IDBAccessObject {
                }
        }
 
+       /**
+        * Whether the password is currently invalid
+        * @since 1.32
+        * @return bool
+        */
+       public function isInvalid() {
+               return $this->getPassword() instanceof InvalidPassword;
+       }
+
        /**
         * Save the BotPassword to the database
         * @param string $operation 'update' or 'insert'
@@ -491,7 +500,11 @@ class BotPassword implements IDBAccessObject {
                }
 
                // Check the password
-               if ( !$bp->getPassword()->equals( $password ) ) {
+               $passwordObj = $bp->getPassword();
+               if ( $passwordObj instanceof InvalidPassword ) {
+                       return Status::newFatal( 'botpasswords-needs-reset', $name, $appId );
+               }
+               if ( !$passwordObj->equals( $password ) ) {
                        return Status::newFatal( 'wrongpassword' );
                }
 
index 77b9375..9943212 100644 (file)
@@ -149,7 +149,7 @@ class Names {
                'en-gb' => 'British English', # British English
                'eo' => 'Esperanto', # Esperanto
                'es' => 'español', # Spanish
-               'es-formal' => 'español (formal)', # Spanish formal address
+               'es-formal' => "español (formal)\xE2\x80\x8E", # Spanish formal address
                'et' => 'eesti', # Estonian
                'eu' => 'euskara', # Basque
                'ext' => 'estremeñu', # Extremaduran
@@ -199,7 +199,7 @@ class Names {
                'hsb' => 'hornjoserbsce', # Upper Sorbian
                'ht' => 'Kreyòl ayisyen', # Haitian Creole French
                'hu' => 'magyar', # Hungarian
-               'hu-formal' => 'magyar (formal)', # Hungarian formal address
+               'hu-formal' => "magyar (formal)\xE2\x80\x8E", # Hungarian formal address
                'hy' => 'Հայերեն', # Armenian
                'hz' => 'Otsiherero', # Herero
                'ia' => 'interlingua', # Interlingua (IALA)
@@ -243,7 +243,7 @@ class Names {
                'km' => 'ភាសាខ្មែរ', # Khmer, Central
                'kn' => 'ಕನ್ನಡ', # Kannada
                'ko' => '한국어', # Korean
-               'ko-kp' => '한국어 (조선)', # Korean (DPRK)
+               'ko-kp' => '조선말', # Korean (DPRK), T190324
                'koi' => 'Перем Коми', # Komi-Permyak
                'kr' => 'Kanuri', # Kanuri, Central
                'krc' => 'къарачай-малкъар', # Karachay-Balkar
index d5cc237..df8fa58 100644 (file)
        "template-protected": "(محميه)",
        "template-semiprotected": "(نص حماية )",
        "hiddencategories": "{{PLURAL:$1|هاد الصفحة ما كايناش فل تصانف المخبّييين|هاد الصفحة كاينة في تصنيف مخبّي واحد|هاد الصفحة كاينة في زوج تاع الـتصانف المخبّيين|هاد الصفحة كاينة في $1 تصنيف مخبّي|هاد الصفحة كاينة في $1 تصنيف مخفبّي|هذه الصفحة كاينة في $1 تصنيف مخبّي}}:",
+       "permissionserrors": "مشكل فل مسموحات",
        "permissionserrorstext-withaction": "ما راكش اوتوريزى ل$2، لل{{PLURAL:$1||سبب هاذا|اسباب هاذي}}:",
        "recreate-moveddeleted-warn": "'''توليه: راك تعاود تصنع باحه اتمحات من قبل.'''\n\nلازم تتأكد بلى الباجه الا نصنعت ماهوش مشكل الا كملت الكتبه فبها.\nريجيستر المحو و النقل معروض هنا باش تراقب :",
        "moveddeleted-notice": "هاذ الباجه تمحات .\nريجيستر المحو والتنقال للباجه معروضين التحت كريفيرونس.",
        "yourrealname": "الاسم الحقاني:",
        "prefs-help-email": "لادريس نتع البريه الإلكترونيه بالخاطر، ولكن هي لازمه في حال نسيت كلمت السر نتاعك.",
        "prefs-help-email-others": "تقدر تاني تخلي لوخرين يتاصلو بيك في باجت نقاشك ولا في وصيله في باجت مستخدم نتاعك, اذا ارسلك واحد ما يبانش لادريس نتاعك , حتى اذ رديت عليه باش يبان لادريس نتاعك.",
+       "group-bot": "بوتات",
+       "grouppage-bot": "{{ns:project}}:بوتات",
        "right-edit": "تبدال الصفحات",
        "right-writeapi": "استعمل API للكتابه نتاع الويكي",
        "newuserlogpage": "ريجيستر صنعة حسابات المستخدمين",
        "rcfilters-filter-editsbyself-label": "التبدال نتاعك",
        "rcfilters-filter-minor-label": "تبديلة خفيفة",
        "rcfilters-filter-major-label": "ماشي تبديلة خفيفة",
-       "rcnotefrom": "التحت التبديلات من <strong>$2</strong> (إلى <strong>$1</strong> معروضة).",
+       "rcnotefrom": "التحت {{PLURAL:$5|تبديلة|التبديلات}}  من <strong>$4 ,$3</strong> (إلى <strong>$1</strong> معروضة).",
        "rclistfrom": "بين التبديلات البديه من $3 $2",
        "rcshowhideminor": "$1 التبديلات الصغير",
        "rcshowhideminor-show": "ورّي",
        "rc-enhanced-expand": "شوف التفاصيل",
        "rc-enhanced-hide": "خبي التفاصيل",
        "recentchangeslinked": "تبديلات مربوطه",
+       "recentchangeslinked-feed": "تبديلات مربوطه",
        "recentchangeslinked-toolbox": "تبديلات الباجات المرتبطه",
        "recentchangeslinked-title": "التبديلات المرتبطة ب \"$1\"",
        "recentchangeslinked-summary": "هاذي ليستة تع التبديلات اللي تمت هاذ الخطرة للباجات الموصولة من باجة معينة (ولا للأعضاء الداخلين في تصنيف معين).\nالصفحات في [[Special:Watchlist|ليستت مراقبة نتاعك]] '''مغلظه'''",
        "nmembers": "$1 اعضاء{{PLURAL:$1||s}}",
        "prefixindex": "كامل الباجات الباديه ب",
        "prefixindex-submit": "ورّي",
+       "listusers": "ليستا تاع المستخدمين",
        "usereditcount": "{{PLURAL:$1|تبديلة|تبديلات}}",
        "usercreated": "{{GENDER:$3|صنعه|صنعته}} في $1 الساعة $2",
        "newpages": "باجه جديده",
        "watchlistfor2": "ل$1 ($2)",
        "watch": "تبع",
        "unwatch": "ما تزيدش تعس",
-       "watchlist-details": "{{PLURAL:$1||باجه وحده|باجتين|$1 باجات|$1 باجه}} في ليستت مراقبتك، من غير اعتبار باجات النقاش هي باجات منفصله.",
+       "watchlist-details": "{{PLURAL:$1||باجه وحده|باجتين|$1 باجات|$1 باجه}} في ليستت مراقبتك، (زدلها باجات النقاش).",
        "wlshowlast": "بين آخر $1 سوايع $2 يامات",
        "watchlist-submit": "ورّي",
        "wlshowhideminor": "تبديلة خفيفة",
        "rollbacklinkcount": "رجّع {{PLURAL:$1|تعديل واحد|$1 تعديلات}}",
        "protectlogpage": "ريجيستر الحمايه",
        "protectedarticle": "راه حمى \"[[$1]]\"",
+       "protect-default": "خلي المستخدمين كامل",
        "restriction-edit": "بدل",
        "undeletelink": "شوف/رجع",
        "undeleteviewlink": "شوف",
        "sp-contributions-search": "تفتاش المشاركات",
        "sp-contributions-username": "عنوان أيبي والال اسم مستخدم:",
        "sp-contributions-toponly": "ما تورّي غير المشاركات التوالا تاع المقالات",
+       "sp-contributions-newonly": "ما تورّي غير المشاركات التوالا تاع المقالات",
        "sp-contributions-submit": "تفتاش",
        "whatlinkshere": "شنوّ يوصّل ل هنا",
        "whatlinkshere-title": "الباجات اللي تقين في \"$1\"",
        "tooltip-undo": "\"نحّي\" فاصي هاد الـمعاودة و حلّ تاقة تاع تبدال بشوفه قبلانيّه. تخلّي باش ترجع لل معاوده التاليه و تزيد الـسبّة علاش فل قابسه تاع الـحويصله.",
        "tooltip-summary": "دخل تلخيص صغير",
        "simpleantispam-label": "مسيّة ضدّ السبام.\nما تعمّرش هادا!",
+       "pageinfo-article-id": "id تاع الصفحة",
+       "pageinfo-robot-index": "تفوت",
        "pageinfo-lastuser": "لخر لي كتب",
        "pageinfo-lasttime": "تاريخ آخر تبديلة",
        "pageinfo-toolboxlink": "معلومات على هاد الصفحة",
        "exif-urgency-normal": "عادي ($1)",
        "namespacesall": "لكل",
        "monthsall": "لكل",
+       "imgmultipagenext": "الباجة الجاية ←",
+       "imgmultigo": "روح",
+       "imgmultigoto": "روح للاباجة $1",
        "watchlisttools-view": "اعرض التبديلات المرتابطه",
        "watchlisttools-edit": "اعرض قائمه المراقبه و عدلها",
        "watchlisttools-raw": "موديفي ليستت التبيعه الخام",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|تقرعيج]])",
        "duplicate-defaultsort": "'''توليه:''' مفتاح التستيف الافتراضي \"$2\" ديباسا مفتاح التستيف الافتراضي التالي\"$1\".",
        "version-no-ext-name": "[بلا اسم]",
+       "redirect-submit": "روح",
+       "redirect-lookup": "حوس في:",
+       "redirect-user": "id تاع المستخدم",
+       "redirect-page": "id تاع الصفحة",
        "specialpages": "الپاجات الخاصّين",
        "external_image_whitelist": " #<pre>خلى هاذ السطر كيما راه\n#حط منثورات التعبيرات المنتظمة (برك الجزء الي يروح بين //) بالتحت\n#هاذ يكون مطابقتها مع مسارات التصاوير البرانيه (الموصولة بصفه مباشره)\n#هاذي الي تشبهغادي تنعرض  كتصاور، خلاف هذا برك وصيلة للتصويرة غادي تنعرض\n#السطور اللي تبدأا ب# تعتبر تعليقات\n#هذا لا يتأثر بحالة الحروف\n\n#حط كامل منثورات التعبيرات المنتظمة فوق هذا السطر. خلي هاذ السطر سواسوا كيما هو</pre>",
        "tag-filter": "صفاية[[Special:Tags|الوشام]]:",
        "tags-active-yes": "إيه",
        "tags-active-no": "لالا",
        "tags-edit": "بدّل",
+       "tags-hitcount": " $1 {{PLURAL:$1|تبديله|تبديلات}}",
        "htmlform-no": "لالا",
        "htmlform-yes": "إيه",
        "logentry-delete-delete": "$1 {{GENDER:$2| راه محا|راهي محات}}الصفحة $3",
        "logentry-upload-upload": " {{GENDER:$2|نزّل|نزّلت}} $1 $3",
        "feedback-error2": "غلطة: تبديلتك ما صلحتش",
        "searchsuggest-search": "فتّش في {{SITENAME}}",
+       "duration-days": "$1 {{PLURAL:$1|يوم|يامات}}",
        "mediastatistics-header-bitmap": "تصويرة Bitmap"
 }
index 520c3ed..81b6d0f 100644 (file)
        "table_pager_limit_submit": "যাওক",
        "table_pager_empty": "ফলাফল নাই",
        "autosumm-blank": "পৃষ্ঠাটো খালী কৰা হ'ল",
-       "autosumm-replace": "পà§\84ষà§\8dঠাà¦\96নক \"$1\"ৰে সলনি কৰা হ'ল",
+       "autosumm-replace": "পà§\83ষà§\8dঠাà¦\9fà§\8bক \"$1\"ৰে সলনি কৰা হ'ল",
        "autoredircomment": "[[$1]]-লৈ পুনৰ্নিৰ্দেশ কৰা হ'ল",
        "autosumm-new": "\"$1\" দি পৃষ্ঠা সৃষ্টি কৰা হ'ল",
        "lag-warn-normal": "$1 {{PLURAL:$1|ছেকেণ্ড|ছেকেণ্ড}} তকৈ নতুন পৰিৱৰ্তনসমূহ এই তালিকাত দেখুওৱা নহবও পাৰে।",
index 313dca9..736af83 100644 (file)
        "createacct-emailoptional": "ইমেইল ঠিকানা (ঐচ্ছিক)",
        "createacct-email-ph": "আপনার ইমেইল ঠিকানা যোগ করুন",
        "createacct-another-email-ph": "আপনার ইমেইল ঠিকানা প্রবেশ করান",
-       "createaccountmail": "à¦\8fà¦\95à¦\9fি à¦°â\80\8cà§\8dযানà§\8dডম à¦ªà¦¾à¦¸à¦\93য়ারà§\8dড à¦¨à¦¿à¦°à§\8dবাà¦\9aন করুন এবং নির্ধারিত ইমেইল ঠিকানায় পাঠিয়ে দিন",
+       "createaccountmail": "সাময়িà¦\95ভাবà§\87 à¦\85à¦\9cানা à¦ªà¦¾à¦¸à¦\93য়ারà§\8dড à¦¬à§\8dযবহার করুন এবং নির্ধারিত ইমেইল ঠিকানায় পাঠিয়ে দিন",
        "createaccountmail-help": "পাসওয়ার্ড জানা ছাড়াই অন্য ব্যক্তির জন্য অ্যাকাউন্ট তৈরি করতে ব্যবহার করা যেতে পারে।",
        "createacct-realname": "আসল নাম (ঐচ্ছিক)",
        "createacct-reason": "কারণ",
index ac44fe2..598b9a4 100644 (file)
        "recentchangeslinked-feed": "Související změny",
        "recentchangeslinked-toolbox": "Související změny",
        "recentchangeslinked-title": "Související změny pro stránku „$1“",
-       "recentchangeslinked-summary": "Vložením názvu stránky uvidíte změny stránek, které na stránku odkazují nebo na které stránka odkazuje. (Pro stránky zařazené do kategorie vložte Kategorie:Název kategorie.) Vámi [[Special:Watchlist|sledované stránky]] jsou <strong>zvýrazněny</strong>.",
+       "recentchangeslinked-summary": "Vložením názvu stránky uvidíte změny stránek, které na stránku odkazují nebo na které stránka odkazuje. (Pro stránky zařazené do kategorie vložte {{ns:category}}:Název kategorie.) Vámi [[Special:Watchlist|sledované stránky]] jsou <strong>zvýrazněny</strong>.",
        "recentchangeslinked-page": "Název stránky:",
        "recentchangeslinked-to": "Zobrazit změny na stránkách odkazujících na zadanou stránku",
        "recentchanges-page-added-to-category": "Stránka [[:$1]] zařazena do kategorie",
        "uploadstash-file-not-found-no-object": "Nepodařilo se vytvořit objekt lokálního souboru pro náhled.",
        "uploadstash-file-not-found-no-remote-thumb": "Načtení náhledu se nepodařilo: $1\nURL = $2",
        "uploadstash-file-not-found-missing-content-type": "Chybí hlavička content-type.",
+       "uploadstash-file-not-found-not-exists": "Nelze najít cestu nebo nejde o soubor.",
        "uploadstash-file-too-large": "Nelze poskytnout soubor větší než $1 bajtů.",
        "uploadstash-not-logged-in": "Není přihlášen žádný uživatel, soubory musí patřit uživatelům.",
        "uploadstash-wrong-owner": "Tento soubor ($1) nepatří aktuálnímu uživateli.",
        "group-bot.css": "/* Zde uvedené CSS bude ovlivňovat pouze roboty */",
        "group-sysop.css": "/* Zde uvedené CSS bude ovlivňovat pouze správce */",
        "group-bureaucrat.css": "/* Zde uvedené CSS bude ovlivňovat pouze byrokraty */",
+       "common.json": "/* Zde uvedený JSON se načte pro všechny uživatele při načtení každé stránky. */",
        "common.js": "/* Zde uvedený JavaScript bude použit pro všechny uživatele při načtení každé stránky. */",
        "group-autoconfirmed.js": "/* Zde uvedený JavaScript bude použit pouze pro automaticky schválené uživatele */",
        "group-user.js": "/* Zde uvedený JavaScript bude použit pouze pro registrované uživatele */",
index 9790e70..2622727 100644 (file)
        "recentchangeslinked-feed": "Σχετικές αλλαγές",
        "recentchangeslinked-toolbox": "Σχετικές αλλαγές",
        "recentchangeslinked-title": "Αλλαγές σχετικές με το «$1»",
-       "recentchangeslinked-summary": "Εισαγάγετε ένα όνομα σελίδας για να δείτε τις αλλαγές σε σελίδες που συνδέονται ή από αυτή τη σελίδα. (Για να δείτε τα μέλη μιας κατηγορίας, εισαγάγετε Κατηγορία:Όνομα κατηγορίας.)\nΑλλαγές σε σελίδες στην [[Special:Watchlist|λίστα παρακολούθησής]] σας είναι <strong>έντονες</strong>.",
+       "recentchangeslinked-summary": "Εισαγάγετε ένα όνομα σελίδας για να δείτε τις αλλαγές σε σελίδες που συνδέονται ή από αυτή τη σελίδα. (Για να δείτε τα μέλη μιας κατηγορίας, εισαγάγετε {{ns:category}}:Όνομα κατηγορίας.)\nΑλλαγές σε σελίδες στην [[Special:Watchlist|λίστα παρακολούθησής]] σας είναι <strong>έντονες</strong>.",
        "recentchangeslinked-page": "Όνομα σελίδας:",
        "recentchangeslinked-to": "Εμφάνιση αλλαγών σε σελίδες συνδεδεμένες με την δεδομένη σελίδα αντί αυτής",
        "recentchanges-page-added-to-category": "Η σελίδα [[:$1]] προστέθηκε στην κατηγορία",
index 09b2d3e..4befdc5 100644 (file)
        "botpasswords-existing": "Existing bot passwords",
        "botpasswords-createnew": "Create a new bot password",
        "botpasswords-editexisting": "Edit an existing bot password",
+       "botpasswords-label-needsreset": "(password needs reset)",
        "botpasswords-label-appid": "Bot name:",
        "botpasswords-label-create": "Create",
        "botpasswords-label-update": "Update",
        "botpasswords-restriction-failed": "Bot password restrictions prevent this login.",
        "botpasswords-invalid-name": "The username specified does not contain the bot password separator (\"$1\").",
        "botpasswords-not-exist": "User \"$1\" does not have a bot password named \"$2\".",
+       "botpasswords-needs-reset": "The bot password for bot name \"$2\" of {{GENDER:$1|user}} \"$1\" must be reset.",
        "resetpass_forbidden": "Passwords cannot be changed",
        "resetpass_forbidden-reason": "Passwords cannot be changed: $1",
        "resetpass-no-info": "You must be logged in to access this page directly.",
        "prefs-watchlist-edits": "Maximum number of changes to show in watchlist:",
        "prefs-watchlist-edits-max": "Maximum number: 1000",
        "prefs-watchlist-token": "Watchlist token:",
+       "prefs-watchlist-managetokens": "Manage tokens",
        "prefs-misc": "Misc",
        "prefs-resetpass": "Change password",
        "prefs-changeemail": "Change or remove email address",
        "recentchangescount": "Number of edits to show in recent changes, page histories, and in logs, by default:",
        "prefs-help-recentchangescount": "Maximum number: 1000",
        "prefs-help-watchlist-token2": "This is the secret key to the web feed of your watchlist.\nAnyone who knows it will be able to read your watchlist, so do not share it.\nIf you need to, [[Special:ResetTokens|you can reset it]].",
+       "prefs-help-tokenmanagement": "You can see and reset the secret key for your account that can access the Web feed of your watchlist. Anyone who knows the key will be able to read your watchlist, so do not share it.",
        "savedprefs": "Your preferences have been saved.",
        "savedrights": "The user groups of {{GENDER:$1|$1}} have been saved.",
        "timezonelegend": "Time zone:",
index 18fd37e..7199df9 100644 (file)
        "recentchanges-label-newpage": "Esta edición creó una página",
        "recentchanges-label-minor": "Esta es una edición menor",
        "recentchanges-label-bot": "Esta edición fue realizada por un robot",
-       "recentchanges-label-unpatrolled": "Esta edición aún no ha sido verificada",
+       "recentchanges-label-unpatrolled": "Aún no se ha verificado esta edición",
        "recentchanges-label-plusminus": "El tamaño de la página cambió esta cantidad de bytes",
        "recentchanges-legend-heading": "<strong>Leyenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ver también la [[Special:NewPages|lista de páginas nuevas]])",
        "rcfilters-filter-user-experience-level-newcomer-label": "Recién llegados",
        "rcfilters-filter-user-experience-level-newcomer-description": "Usuarios registrados con menos de diez ediciones o cuatro días de actividad.",
        "rcfilters-filter-user-experience-level-learner-label": "Aprendices",
-       "rcfilters-filter-user-experience-level-learner-description": "Editores registrados cuya experiencia se ubica entre \"Recién Llegados\" y \"Usuarios experimentados\".",
+       "rcfilters-filter-user-experience-level-learner-description": "Editores registrados cuya experiencia se ubica entre «recién llegados» y «usuarios experimentados».",
        "rcfilters-filter-user-experience-level-experienced-label": "Usuarios experimentados",
        "rcfilters-filter-user-experience-level-experienced-description": "Editores registrados con más de 500 ediciones y 30 días de actividad.",
        "rcfilters-filtergroup-automated": "Contribuciones automatizadas",
        "rcfilters-filtergroup-lastRevision": "Últimas revisiones",
        "rcfilters-filter-lastrevision-label": "Última revisión",
        "rcfilters-filter-lastrevision-description": "Solo el cambio más reciente a una página.",
-       "rcfilters-filter-previousrevision-label": "No la última revisión",
-       "rcfilters-filter-previousrevision-description": "Todos los cambios que no son la \"última revisión\".",
+       "rcfilters-filter-previousrevision-label": "No la revisión más reciente",
+       "rcfilters-filter-previousrevision-description": "Todos los cambios que no son la «versión actual».",
        "rcfilters-filter-excluded": "Excluido",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>Estado:</strong> $1",
        "rcfilters-exclude-button-off": "Excluir los seleccionados",
index 58bd170..f06f2d4 100644 (file)
        "permissionserrorstext": "Vous n'avez pas la permission d'effectuer l'opération demandée pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "permissionserrorstext-withaction": "Vous ne pouvez pas $2, pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "contentmodelediterror": "Vous ne pouvez pas modifier cette révision car son modèle de contenu est <code>$1</code>, ce qui diffère du modèle de contenu actuel de la page <code>$2</code>.",
-       "recreate-moveddeleted-warn": "<strong>Attention : vous êtes en train de recréer une page qui a été précédemment supprimée.</strong>\n\nAssurez-vous qu'il est pertinent de poursuivre les modifications sur cette page. \nLe journal des suppressions et des déplacements pour cette page est affiché ci-dessous à titre d'information :",
-       "moveddeleted-notice": "Cette page a été supprimée. \nLes journaux des suppressions, protections et déplacements pour la page sont fournis ci-dessous pour référence.",
+       "recreate-moveddeleted-warn": "<strong>Attention : vous êtes en train de recréer une page qui a été précédemment supprimée.</strong>\n\nAssurez-vous qu'il est pertinent de poursuivre les modifications sur cette page.\nLes journaux des suppressions et déplacements pour cette page sont fournis ici pour information :",
+       "moveddeleted-notice": "Cette page a été supprimée.\nLes journaux des suppressions, protections et déplacements pour la page sont fournis ci-dessous pour référence.",
        "moveddeleted-notice-recent": "Désolé, cette page a été récemment supprimée (dans les dernières 24 heures).\nLes journaux des suppressions, protections et déplacements pour la page sont fournis ci-dessous pour référence.",
        "log-fulllog": "Voir le journal complet",
        "edit-hook-aborted": "Échec de la modification par une extension.\nAucune explication n’a été retournée.",
        "upload_directory_missing": "Le répertoire d’import de fichier ($1) est introuvable et n’a pas pu être créé par le serveur web.",
        "upload_directory_read_only": "Le serveur web n’a pas accès en écriture au répertoire d’import de fichier ($1).",
        "uploaderror": "Erreur lors de l’import",
-       "upload-recreate-warning": "<strong>Attention : Un fichier portant ce nom a été supprimé ou déplacé.</strong>\n\nLe journal des suppressions et celui des déplacements de cette page sont affichés ici pour informations :",
+       "upload-recreate-warning": "<strong>Attention : Un fichier portant ce nom a été supprimé ou déplacé.</strong>\n\nLes journaux des suppressions et déplacements pour cette page sont fournis ici pour information :",
        "uploadtext": "Utilisez ce formulaire pour téléverser des fichiers sur le serveur.\nPour voir ou rechercher des images précédemment envoyées, consultez la [[Special:FileList|liste des fichiers téléversés]]. Les envois multiples sont également tracés dans le [[Special:Log/upload|journal des téléversements]], et les suppressions dans le [[Special:Log/delete|journal des suppressions]].\n\nPour inclure un fichier dans une page, utilisez un lien ayant l'un des formats suivants :\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:fichier.jpg]]</nowiki></code></strong>, pour afficher le fichier en pleine résolution (dans le cas d’une image) ;\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:fichier.png|200px|thumb|left|texte descriptif]]</nowiki></code></strong> pour utiliser une miniature de 200 pixels de large dans une boîte à gauche avec « texte descriptif » comme description ;\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:fichier.ogg]]</nowiki></code></strong> pour relier directement le fichier sans l’afficher.",
        "upload-permitted": "{{PLURAL:$2|Format|Formats}} de fichiers {{PLURAL:$2|autorisé|autorisés}} : $1.",
        "upload-preferred": "{{PLURAL:$2|Format|Formats}} de fichiers {{PLURAL:$2|préféré|préférés}} : $1.",
index e8d6e3a..4a5a09d 100644 (file)
        "botpasswords-existing": "סיסמאות בוט קיימות",
        "botpasswords-createnew": "יצירת סיסמת בוט חדשה",
        "botpasswords-editexisting": "עריכת סיסמת בוט קיימת",
+       "botpasswords-label-needsreset": "(הסיסמה דורשת איפוס)",
        "botpasswords-label-appid": "שם הבוט:",
        "botpasswords-label-create": "יצירה",
        "botpasswords-label-update": "עדכון",
        "botpasswords-restriction-failed": "כניסה זו נמנעה בשל הגבלות על סיסמאות בוט.",
        "botpasswords-invalid-name": "שם המשתמש שניתן אינו מכיל את תו הפרדת סיסמאות הבוט (\"$1\").",
        "botpasswords-not-exist": "{{GENDER:$1|למשתמש|למשתמשת}} \"$1\" אין סיסמת בוט בשם \"$2\".",
+       "botpasswords-needs-reset": "נדרש איפוס של סיסמת הבוט עבור הבוט \"$2\" של {{GENDER:$1|המשתמש|המשתמשת}} \"$1\".",
        "resetpass_forbidden": "לא ניתן לשנות סיסמאות",
        "resetpass_forbidden-reason": "לא ניתן לשנות את הסיסמאות: $1",
        "resetpass-no-info": "נדרשת כניסה לחשבון כדי לגשת לדף זה באופן ישיר.",
        "listfiles_size": "גודל",
        "listfiles_description": "תיאור",
        "listfiles_count": "גרסאות",
-       "listfiles-show-all": "×\9b×\95×\9cל גרסאות ישנות של קבצים",
+       "listfiles-show-all": "×\9c×\9b×\9c×\95ל גרסאות ישנות של קבצים",
        "listfiles-latestversion": "גרסה נוכחית",
        "listfiles-latestversion-yes": "כן",
        "listfiles-latestversion-no": "לא",
        "linkstoimage": "{{PLURAL:$1|הדף הבא משתמש|הדפים הבאים משתמשים}} בקובץ זה:",
        "linkstoimage-more": "יותר {{PLURAL:$1|מדף אחד מקשר|מ־$1 דפים מקשרים}} לקובץ זה.\nהרשימה הבאה מראה רק את {{PLURAL:$1|הדף הראשון שמקשר|$1 הדפים הראשונים שמקשרים}} לקובץ זה.\nניתן לצפות ב[[Special:WhatLinksHere/$2|רשימה המלאה]].",
        "nolinkstoimage": "אין דפים המשתמשים בקובץ זה.",
-       "morelinkstoimage": "ר×\90×\95 [[Special:WhatLinksHere/$1|דפים נוספים]] שמשתמשים בקובץ זה.",
+       "morelinkstoimage": "×\99שנ×\9d [[Special:WhatLinksHere/$1|דפים נוספים]] שמשתמשים בקובץ זה.",
        "linkstoimage-redirect": "$1 (הפניה של קובץ) $2",
-       "duplicatesoffile": "{{PLURAL:$1|הקובץ הבא זהה|הקבצים הבאים זהים}} לקובץ זה ([[Special:FileDuplicateSearch/$2|לפרטים נוספים]]):",
+       "duplicatesoffile": "{{PLURAL:$1|הקובץ הבא זהה|$1 הקבצים הבאים זהים}} לקובץ זה ([[Special:FileDuplicateSearch/$2|לפרטים נוספים]]):",
        "sharedupload": "זהו קובץ מתוך $1 וניתן להשתמש בו גם במיזמים אחרים.",
-       "sharedupload-desc-there": "×\96×\94×\95 ×§×\95×\91×¥ ×\9eת×\95×\9a $1 ×\95× ×\99ת×\9f ×\9c×\94שת×\9eש ×\91×\95 ×\92×\9d ×\91×\9e×\99×\96×\9e×\99×\9d ×\90×\97ר×\99×\9d.\n×\9c×\9e×\99×\93×¢ × ×\95סף, ×¨×\90×\95 ×\90ת [$2 דף תיאור הקובץ].",
+       "sharedupload-desc-there": "×\96×\94×\95 ×§×\95×\91×¥ ×\9eת×\95×\9a $1 ×\95× ×\99ת×\9f ×\9c×\94שת×\9eש ×\91×\95 ×\92×\9d ×\91×\9e×\99×\96×\9e×\99×\9d ×\90×\97ר×\99×\9d.\n×\9c×\9e×\99×\93×¢ × ×\95סף, × ×\99ת×\9f ×\9c×¢×\99×\99×\9f ×\91[$2 דף תיאור הקובץ].",
        "sharedupload-desc-here": "זהו קובץ מתוך $1 וניתן להשתמש בו גם במיזמים אחרים.\nתיאורו ב[$2 דף תיאור הקובץ] שלו מוצג למטה.",
        "sharedupload-desc-edit": "זהו קובץ מתוך $1 וניתן להשתמש בו גם במיזמים אחרים.\nניתן לערוך את התקציר שלו ב[$2 דף תיאור הקובץ] שם.",
        "sharedupload-desc-create": "זהו קובץ מתוך $1 וניתן להשתמש בו גם במיזמים אחרים.\nניתן לערוך את התקציר שלו ב[$2 דף תיאור הקובץ] שם.",
        "filepage-nofile-link": "לא קיים קובץ בשם זה, אך באפשרותך [$1 להעלותו].",
        "uploadnewversion-linktext": "העלאת גרסה חדשה של קובץ זה",
        "shared-repo-from": "מתוך $1",
-       "shared-repo": "×\9eק×\95×\9d ×\90×\99×\97ס×\95×\9f ×\9eש×\95תף",
+       "shared-repo": "מקום אחסון משותף",
        "shared-repo-name-wikimediacommons": "ויקישיתוף",
        "filepage.css": "/* הסגנונות הנכתבים כאן יוכללו בדף תיאור הקובץ, כולל באתרי ויקי זרים */",
        "upload-disallowed-here": "אין באפשרותך לדרוס את הקובץ הזה.",
index f547bdc..b76598d 100644 (file)
        "special-characters-group-thai": "tajlandski (tajski)",
        "special-characters-group-lao": "laoski",
        "special-characters-group-khmer": "kmerski",
-       "special-characters-group-canadianaboriginal": "Kanadski domorodni",
+       "special-characters-group-canadianaboriginal": "kanadski domorodni",
        "special-characters-title-endash": "crtica",
        "special-characters-title-emdash": "dulja crtica",
        "special-characters-title-minus": "znak za minus",
index 735d477..80cf726 100644 (file)
        "tog-fancysig": "Кулг яздара ший йола вики-разметка (автоматически тIахьожаярг йоацаш)",
        "tog-uselivepreview": "Хьахьокха хьалххе бӀаргтохар оагӀув юха хьа а ца елаш",
        "tog-forceeditsummary": "ДIахьалхадаккха, нагахьа санна хувцама йоазонца сурт оттадара моттиг хьалъйизанза яле",
-       "tog-watchlisthideown": "Са Ð·ÐµÐ¼ Ð±Ð°Ñ\80а Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80 Ñ\87Ñ\83Ñ\80а Ñ\85Ñ\83вÑ\86амаÑ\88 къайладаха",
-       "tog-watchlisthidebots": "Ð\97ем Ð±Ð°Ñ\80а Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80 Ñ\87Ñ\83Ñ\80а Ð±Ð¾Ñ\82ий Ñ\85Ñ\83вÑ\86амаÑ\88 къайладаха",
-       "tog-watchlisthideminor": "Са Ð·ÐµÐ¼ Ð±Ð°Ñ\80а Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80 Ñ\87Ñ\83Ñ\80а Ð·Iамига Ñ\85Ñ\83вÑ\86амаÑ\88 къайладаха",
-       "tog-watchlisthideliu": "ШоаÑ\88 Ñ\85Ñ\8cабайзийÑ\82а Ð´Ð¾Ð°ÐºÑ\8aоÑ\88Ñ\85оÑ\88а Ñ\85Ñ\83вÑ\86амаÑ\88 ÐºÑ\8aайладаÑ\85а Ð·ÐµÐ¼ Ð±Ð°Ñ\80а Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80 чура",
+       "tog-watchlisthideown": "Ð\90з Ð´Ð°Ñ\8c Ñ\85Ñ\83вÑ\86амаÑ\88 Ð·Ã©Ð¼Ð° Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80а Ñ\87Ñ\83Ñ\80а къайладаха",
+       "tog-watchlisthidebots": "Ð\91оÑ\82аÑ\88 Ð´Ð°Ñ\8c Ñ\85Ñ\83вÑ\86амаÑ\88 Ð·Ã©Ð¼Ð° Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80а Ñ\87Ñ\83Ñ\80а къайладаха",
+       "tog-watchlisthideminor": "Ð\97Iамига Ñ\85Ñ\83вÑ\86амаÑ\88 Ð·Ã©Ð¼Ð° Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80а Ñ\87Ñ\83Ñ\80а къайладаха",
+       "tog-watchlisthideliu": "РажаÑ\87а Ñ\87Ñ\83баÑ\8cннабоаÑ\86аÑ\87а Ð´Ð¾Ð°ÐºÑ\8aоÑ\88Ñ\85оÑ\88а Ð´Ð°Ñ\8c Ñ\85Ñ\83вÑ\86амаÑ\88 ÐºÑ\8aайладаÑ\85а Ð·ÐµÐ¼Ð° Ñ\85Ñ\8cаÑ\8fзÑ\8aÑ\8fÑ\8cÑ\80а чура",
        "tog-watchlisthideanons": "ЦIийоацача доакъошхоша хувцамаш къайладаха зем бара хьаязъяьр чура",
        "tog-watchlisthidepatrolled": "Ха даь дола хувцамаш къайладаха зéма хьаязъяьра чу",
        "tog-watchlisthidecategorization": "ОагӀонашта оагIаташ тохар къайладаккха",
        "december-date": "Чан-тар $1",
        "period-am": "ДЦ",
        "period-pm": "ДТ",
-       "pagecategories": "{{PLURAL:$1|1=ОагIат|ОагIаташ}}",
-       "category_header": "«$1» яхача оагIата чура оагIонаш",
-       "subcategories": "КIалоагIаташ",
-       "category-media-header": "\"$1\" яхача оагIата чура файлаш",
-       "category-empty": "''Ер оагIат хӀанза яьсса я (цхьаккха оагIонаш е файлаш йоацаш).''",
-       "hidden-categories": "{{PLURAL:$1|1=Къайла оагIат|Къайла оагIаташ}}",
+       "pagecategories": "{{PLURAL:$1|1=ОагӀат|ОагӀаташ}}",
+       "category_header": "«$1» яхача оагӀата чура оагIонаш",
+       "subcategories": "КӀалоагӀаташ",
+       "category-media-header": "\"$1\" яхача оагӀата чура файлаш",
+       "category-empty": "''Ер оагӀат хӀанза яьсса я (цхьаккха оагIонаш е файлаш яц укхаза).''",
+       "hidden-categories": "{{PLURAL:$1|1=Къайла оагӀат|Къайла оагӀаташ}}",
        "hidden-category-category": "Къайла оагӀаташ",
        "category-subcat-count": "{{PLURAL:$2|Укх оагIата чу я алхха ер кIалоагIат.|Укх оагIата чу гуш я $2-нен юкъера $1 {{PLURAL:$1|кIалоагIат}} }}",
        "category-subcat-count-limited": "Укх категори чу {{PLURAL:$1|кIалхара категори|$1 кIалхара категореш}} я.",
        "category-article-count": "{{PLURAL:$2|Укх оагIата чу цаI мара оагIув яц.|Укх оагIата чу я $2 оагӀув, царех оагӀонгахьа {{PLURAL:$1|хьагойт $1 оагӀув}}}}",
        "category-article-count-limited": "Укх оагӀата чу {{PLURAL:$1|$1 оагӀув я|1=цхьа оагӀув мара яц}}.",
-       "category-file-count": "{{PLURAL:$2|Укх оагIата чу цаI мара файл яц.|Укх оагIата чу долча $2 файлах {{PLURAL:$1|1=хьагойт $1 файл}} }}",
+       "category-file-count": "{{PLURAL:$2|Укх оагӀата чу цаI мара файл яц.|Укх оагӀата чу йолча $2 файлах {{PLURAL:$1|1=хьагуш я $1 файл}} }}",
        "category-file-count-limited": "Укх категори чу {{PLURAL:$1|$1 файл|$1 файлаш|1=цаI мара файл яц}}.",
        "listingcontinuesabbrev": "(дIахо)",
        "index-category": "Индекс оттаеш оагIонаш",
        "nstab-mediawiki": "Хоамбар",
        "nstab-template": "Ло",
        "nstab-help": "Новкъостал",
-       "nstab-category": "ОагIат",
+       "nstab-category": "ОагӀат",
        "mainpage-nstab": "Керттера",
        "nosuchaction": "Цу тайпара ардам дац",
        "nosuchspecialpage": "Изза мо гIулакха оагӀув яц",
        "skin-preview": "Хьалххе бIаргтохар",
        "prefs-personal": "Доакъашхочун дараш",
        "prefs-rc": "Керда нийсдараш",
-       "prefs-watchlist": "Зем бара хьаязъяьр",
+       "prefs-watchlist": "Зéма хьаязъяьр",
        "prefs-watchlist-days": "Дéной дукхал:",
        "prefs-resetpass": "Хувца къайладIоагIа",
        "prefs-rendering": "ТIера куц",
        "right-createtalk": "дувца оттадара оагӀонаш кхоллар",
        "right-move": "оагIонай цIераш хувцар",
        "right-movefile": "файлай цӀераш хувцар",
-       "right-writeapi": "Ð\94IаÑ\8fздаÑ\80а Ð»Ð°Ñ\8cÑ\80Ñ\85Ñ\85Iа API Ð¿Ð°Ð¹Ð´Ð° Ñ\8dÑ\86аÑ\80",
+       "right-writeapi": "дIаÑ\8fздеÑ\88 Ð»ÐµÐ»Ð°Ðµ API",
        "newuserlogpage": "Доакъашхой дIаязбаь таптар",
        "rightslog": "Доакъашхочун бокъоний тéптар",
        "action-read": "ер оагӀув éшар",
        "recentchangeslinked-feed": "ВIашагIдувзаденна нийсдараш",
        "recentchangeslinked-toolbox": "ВIашагIдувзаденна хувцамаш",
        "recentchangeslinked-title": "$1ца вIашидувзаденна хувцамаш",
-       "recentchangeslinked-summary": "Ӏочуязъе оагӀон цӀи, цунна тӀатовжаш йолча оагӀонаш чу даь дола хувцамаш бӀаргагургдолаш (ОагӀата доакъашхой бӀаргагургболаш Ӏочуязъе Category:ОагӀата цӀи). \n[[Special:Watchlist|Хьа зема хьаязъяьра]] юкъейоагӀа оагӀонаш <strong>сомача лапӀазаца</strong> белгалаяьй.",
+       "recentchangeslinked-summary": "Ӏочуязъе оагӀон цӀи, цунна тӀатовжаш йолча оагӀонаш чу даь дола хувцамаш бӀаргагургдолаш (ОагӀата доакъашхой бӀаргагургболаш Ӏочуязъе {{ns:category}}:ОагӀата цӀи).\n[[Special:Watchlist|Хьа зéма хьаязъяьрá]] юкъейоагӀача оагӀонаш чу даь хувцамаш <strong>сомача лапӀазаца</strong> белгалдаь да.",
        "recentchangeslinked-page": "ОагIон цIи",
        "recentchangeslinked-to": "Вешта, белгаляьккха оагIон тIахьожавеш дола оагIонашта даь хувцамаш хьахьокха.",
        "upload": "Файл чуяккха",
        "nbytes": "$1 {{PLURAL:$1|байт}}",
        "nmembers": "$1 {{PLURAL:$1|объект}}",
        "prefixindex": "ОагӀоний цӀерий дешхьалхех хьахокхар",
+       "prefixindex-namespace": "ОагӀоний цӀераш шоай дешхьалхех хьахьокхар (цIерий моттиг «{{ns:$1}}»)",
        "shortpages": "Лоаца оагIонаш",
        "longpages": "ЙIаьха оагIонаш",
        "protectedpages-page": "ОагIув",
        "allarticles": "Еррига оагIонаш",
        "allpagessubmit": "Кхоачашде",
        "allpages-hide-redirects": "ДIакъайладаха дӀа-хьа хьожавераш",
-       "categories": "ОагIаташ",
+       "categories": "ОагӀаташ",
        "linksearch": "Арахьара тIахьожаяргаш лахар",
        "linksearch-ns": "ЦIерий аренаш:",
        "linksearch-ok": "Хьалаха",
        "tooltip-ca-nstab-mediawiki": "MediaWiki хоамбара оагIув",
        "tooltip-ca-nstab-template": "Лера оагIув",
        "tooltip-ca-nstab-help": "Новкъостала оагIув",
-       "tooltip-ca-nstab-category": "ОагIата оагӀув",
+       "tooltip-ca-nstab-category": "ОагӀата оагӀув",
        "tooltip-minoredit": "Ер хувцар кIезига дар санна белгалде",
        "tooltip-save": "Хьа хувцамаш лорадеш дIаязде",
        "tooltip-preview": "ОагIонна хьалххе бIаргтохар.\nДехар да, оагӀув дIаязъелехь хьажа из мишта я!",
-       "tooltip-diff": "Ð\94IадолалÑ\83 текстаца даь хувцамаш хьахьокха",
+       "tooltip-diff": "ЧÑ\83Ñ\85Ñ\8cнаÑ\85Ñ\8cаÑ\80а текстаца даь хувцамаш хьахьокха",
        "tooltip-compareselectedversions": "Укх оагIон хержа шин версешта юкъе йола башхалога хьажа.",
        "tooltip-watch": "ТIатоха ер оагIув хьа зем бара хьаязъяьра",
        "tooltip-rollback": "Цкъа пIелг тоIабаь дIадаха тIехьарча редакторо даь хувцамаш",
        "pageinfo-robot-noindex": "Могадаьдац",
        "pageinfo-watchers": "Зем беш болчар дуккхал",
        "pageinfo-few-watchers": "{{PLURAL:$1|Зем бер}} $1-ннел кIезигагIа ба",
-       "pageinfo-redirects-name": "УкÑ\85 Ð¾Ð°Ð³Ó\80онна Ð´Ó\80а-Ñ\81аÑ\85Ñ\8cожадарий дуккхал",
+       "pageinfo-redirects-name": "УкÑ\85 Ð¾Ð°Ð³Ó\80онна Ð´Ó\80а-Ñ\85Ñ\8cа Ñ\85Ñ\8cожадаÑ\8cрий дуккхал",
        "pageinfo-subpages-name": "Укх оагӀон кIалоагӀонаш",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|дIа-хьа хьожаяьр}}; $3 {{PLURAL:$3|кхыяр}})",
        "pageinfo-firstuser": "ОагӀув кхеллар",
        "pageinfo-recent-edits": "ТӀехьарча хана даь нийсдар (укх хана юкъе: $1)",
        "pageinfo-recent-authors": "ТӀехьарча хана бола башха автораш",
        "pageinfo-magic-words": "{{PLURAL:$1|1=Тамашийна дош|Тамашийна дешаш}} ($1)",
-       "pageinfo-hidden-categories": "{{PLURAL:$1|1=Къайла оагIат}} ($1)",
+       "pageinfo-hidden-categories": "{{PLURAL:$1|Къайла оагӀат|Къайла оагӀаташ}} ($1)",
        "pageinfo-templates": "{{PLURAL:$1|1=Ло|Лераш}} ($1)",
        "pageinfo-toolboxlink": "ОагIонах бола хоам",
        "pageinfo-contentpage": "Счётчико чулацаме оагIув санна лоархI",
        "pageinfo-contentpage-yes": "XIаа",
        "patrol-log-page": "ТӀахьожама тептар",
-       "previousdiff": "â\86\90 Ð¥Ñ\8cалÑ\85аÑ\80а Ð½Ð¸Ð¹Ñ\81дар",
-       "nextdiff": "ТIайоагIа Ð½Ð¸Ð¹Ñ\81Ñ\8aаÑ\80",
+       "previousdiff": "â\86\90 Ð\9aÑ\8aаÑ\8cнагIа Ð´Ð¾Ð»Ð° Ð½Ð¸Ð¹Ñ\81даÑ\8cр",
+       "nextdiff": "Ð\9aеÑ\80дагÓ\80а Ð´Ð¾Ð»Ð° Ð½Ð¸Ð¹Ñ\81даÑ\8cÑ\80 â\86\92",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|оагӀув}}",
        "file-info-size": "$1 × $2 {{PLURAL:$2|пиксель}}, файла боарам: $3, MIME-тайпа: $4",
        "file-info-size-pages": "$1 × $2 пиксель, файлан боарам: $3, MIME-тайп: $4, $5 {{PLURAL:$5|1=оагӀув}}",
-       "file-nohires": "УкÑ\85ал Ð´Ñ\83ккÑ\85агIа Ð´Ð¾ÐºÐºÑ\85ал Ð´Ð¾Ð»Ð°Ñ\88 Ð²ÐµÑ\80Ñ\81и Ñ\8fÑ\86",
+       "file-nohires": "Ð\9aÑ\85Ñ\8b Ð¹Ð¾ÐºÐºÑ\85агIа Ñ\8dÑ\80Ñ\88 Ñ\8fÑ\86.",
        "svg-long-desc": "SVG-файл, номинально $1 × $2 {{PLURAL:$2|пиксель}}, файлан боарам: $3",
-       "show-big-image": "Ð\94IадолалÑ\83 файл",
-       "show-big-image-preview": "Ð\91оаÑ\80ам Ñ\85Ñ\8cалÑ\85Ñ\85е Ð±IаÑ\80гÑ\82оÑ\85аÑ\87 Ñ\85ан: $1.",
-       "show-big-image-other": "{{PLURAL:$2|1=Кхыбола тIера боарам|Кхыбола тIера боарам}}: $1.",
+       "show-big-image": "Ð\9eÑ\80игиналÑ\8cни файл",
+       "show-big-image-preview": "Ð¥Ñ\8cалÑ\85Ñ\85е Ð±IаÑ\80гÑ\82оÑ\85аÑ\87а Ñ\85ана Ð±Ð¾Ð»Ð° Ð±Ð¾Ð°Ñ\80ам: $1.",
+       "show-big-image-other": "{{PLURAL:$2|1=Кхыбола тIера боарам}}: $1.",
        "show-big-image-size": "$1 × $2 пиксель",
        "noimages": "Суртaш дац.",
-       "ilsubmit": "Хьалáха",
-       "bad_image_list": "Формат хила еза иштта:\n\nЛоархIаш хургда алхха хьаязъяьра элементаш (укх * бехкама белгалонаца долалуш дола могIараш).\nМогIара цхьоаллагIа тIахьожаярг чуоттаде мегаш доаца сурта тIахьожавеш хила еза.\nЦу могIара тIехьайоагIа тIахьожаяргаш лоархIаш хургья эргамаш (исключения) санна, вешта аьлча, сурт чуоттаде мегаш йола статьяш санна.",
+       "ilsubmit": "Хьалаха",
+       "bad_image_list": "Формат хила езаш я иштта:\n\nЛоархIаш хургда алхха хьаязъяьра элементаш (укх * яхача хьаракаца дIадолалуш дола могIараш).\nМогIара хьалхара тIатовжам хила безаш ба хьачудаккха йиш йоацача суртá тIатовжаш.\nАмма цун тIехьадоагIа тIатовжамаш тIатовжаш хургда сурт чуоттаде йиш йолча статьяшта.",
        "metadata": "Мета-дараш",
-       "metadata-help": "Файло кхыдола дараш чулоаца, цифровой суртдоакхарго е сканеро тIатохаш дола. Нагахьа файл чуякхачул тIехьа хийца хинна дале, цхьаццайола параметраш хIанзара сурта тIара йоацаш хила мегаш я.",
+       "metadata-help": "Файло кхыдола дараш чулоац, цифрацара суртдоакхарго е сканеро тIатохаш дола. Нагахьа санна файл хьачуякхачул тIехьагIа хийца хинна яле, цхьаццайола параметраш хIанзарча сурта тIа йоацаш хила мег.",
        "metadata-expand": "Хьахьокха кхыдола дараш",
        "metadata-collapse": "ДIакъайладаха кхыдола дараш",
-       "metadata-fields": "Укх хьаяьзъяра чу дагaрадаь суртий метахоамий йистош, хьахьекха хургда сурта оагIон тIа, хьоарчадаь метахоамий ильг долаш. Юхедиса йистош къайла хургда.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
+       "metadata-fields": "Укх хьаязъяьра чу Iохьоахаяь сурта метадарий йистош хьахьекха хургья сурта оагIон тIа дIахьулъяьча метадарий таблица чу. Юхейиса йистош къайла хургья.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "exif-imagewidth": "Шерал",
        "exif-imagelength": "Лакхал",
        "exif-orientation": "Сурта белгало",
        "exif-datetimedigitized": "Оцифровк яь таьрахь а, ха а",
        "exif-writer": "Текста автор",
        "exif-languagecode": "Мотт",
-       "exif-iimcategory": "Ð\9aаÑ\82егоÑ\80и",
-       "exif-orientation-1": "гIаÑ\8cÑ\85Ñ\8cа",
+       "exif-iimcategory": "Ð\9eагÓ\80аÑ\82",
+       "exif-orientation-1": "Ð\9bеÑ\80Ñ\82Ó\8fа",
        "exif-exposureprogram-1": "Кара",
        "exif-scenecapturetype-1": "Ландшафт",
        "exif-scenecapturetype-2": "Сага сурт",
        "specialpages": "ЛаьрххIа йола оагIонаш",
        "specialpages-group-users": "Доакъашхойи бокъонаши",
        "specialpages-group-pages": "ОагIонай хьаязъяьраш",
-       "specialpages-group-pagetools": "ОагIонашта дола гIирсаш",
+       "specialpages-group-pagetools": "ОагIонашта эша кечалаш",
        "external_image_whitelist": "#Ер мугI ший болча тайпара бита<pre>\n#Укхаз оттаде кастта дувлача выражений фрагменташ (// юкъе дола дакъа)\n#арахьара суртий URL адресашца дIанийсалургда уш.\n#Мегаргдола сурташ санна хьахьекха хургда, дIаходараш, сурташта тIахьожаяргаш санна хьахьекха хургда.\n#Укханца # долалуш дола могIараш алараш санна лоархIаш да.\n#МогIараш регистраца кIаьда дац\n\n#Укх могIара лакхе оттаде кастта дувлача выражений фрагменташ. Ер мугI ший болча тайпара бита</pre>",
        "tag-filter": "[[Special:Tags|Белгалонай]] фильтр:",
        "tag-filter-submit": "Литта",
index 791e29f..0b18272 100644 (file)
        "recentchangeslinked-feed": "Modifiche correlate",
        "recentchangeslinked-toolbox": "Modifiche correlate",
        "recentchangeslinked-title": "Modifiche correlate a \"$1\"",
-       "recentchangeslinked-summary": "Inserisci il nome di una pagina per vedere le modifiche alle pagine che sono collegate o che collegano a quella pagine. (Per vedere i membri una categoria, inserisci Categoria:Nome della categoria). Le modifiche alle pagine contenute nella propria lista degli [[Special:Watchlist|osservati speciali]] sono evidenziate in <strong>grassetto</strong>.",
+       "recentchangeslinked-summary": "Inserisci il nome di una pagina per vedere le modifiche alle pagine che sono collegate o che collegano a quella pagine. (Per vedere i membri una categoria, inserisci {{ns:category}}:Nome della categoria). Le modifiche alle pagine contenute nella propria lista degli [[Special:Watchlist|osservati speciali]] sono evidenziate in <strong>grassetto</strong>.",
        "recentchangeslinked-page": "Nome della pagina:",
        "recentchangeslinked-to": "Mostra solo le modifiche alle pagine collegate a quella specificata",
        "recentchanges-page-added-to-category": "[[:$1]] aggiunta alla categoria",
index 6831398..17faeee 100644 (file)
        "version-skins": "Uzstādītās apdares",
        "version-specialpages": "Īpašās lapas",
        "version-variables": "Mainīgie",
+       "version-editors": "Redaktori",
        "version-antispam": "Spama aizsardzība",
        "version-other": "Cita",
        "version-hooks": "Aizķeres",
        "htmlform-cloner-create": "Pievienot vairāk",
        "htmlform-cloner-delete": "Noņemt",
        "htmlform-date-placeholder": "GGGG-MM-DD",
+       "htmlform-title-not-creatable": "\"$1\" nav izveidojams lapas nosaukums",
        "htmlform-title-not-exists": "$1 nepastāv.",
        "htmlform-user-not-exists": "<strong>$1</strong> nepastāv.",
        "htmlform-user-not-valid": "<strong>$1</strong> nav derīgs lietotājvārds.",
        "limitreport-templateargumentsize": "Veidnes argumenta izmērs",
        "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|baiti|baits|baiti}}",
        "limitreport-expensivefunctioncount": "Dārgo parsētāja funkciju skaits",
+       "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|baiti|baits|baiti}}",
+       "expandtemplates": "Izvērst veidnes",
        "expand_templates_output": "Rezultāts",
        "expand_templates_ok": "Labi",
        "expand_templates_remove_nowiki": "Cenzēt <nowiki> iezīmes rezultātā",
        "mediastatistics-header-video": "Video",
        "mediastatistics-header-total": "Visi faili",
        "json-error-syntax": "Sintakses kļūda",
+       "headline-anchor-title": "Saite uz šo sadaļu",
        "special-characters-group-latin": "Latīņu",
        "special-characters-group-latinextended": "Latīņu (papildus)",
        "special-characters-group-ipa": "IPA",
index 21dfa69..5f6a0a5 100644 (file)
@@ -88,7 +88,7 @@
        "tog-watchlisthideminor": "Skjul mindre endringer fra overvåkningslisten",
        "tog-watchlisthideliu": "Skjul endringer av innloggede brukere fra overvåkningslisten",
        "tog-watchlistreloadautomatically": "Oppdater oversiktslisten automatisk når et filter er endret (JavaScript kreves)",
-       "tog-watchlistunwatchlinks": "Legg til lenker for å overvåke/fjerne overvåking direkte i overvåkningslisten (JavaScript kreves)",
+       "tog-watchlistunwatchlinks": "Legg til lenker for å innføre/fjerne overvåking direkte i overvåkningslisten (JavaScript kreves)",
        "tog-watchlisthideanons": "Skjul endringer av anonyme brukere fra overvåkningslisten",
        "tog-watchlisthidepatrolled": "Skjul patruljerte endringer fra overvåkningslisten",
        "tog-watchlisthidecategorization": "Skjul kategorisering av sider",
        "prefs-dateformat": "Datoformat",
        "prefs-timeoffset": "Tidsforskyvning",
        "prefs-advancedediting": "Generelle valg",
+       "prefs-developertools": "Utviklerverktøy",
        "prefs-editor": "Tekstbehandling",
        "prefs-preview": "Forhåndsvisning",
        "prefs-advancedrc": "Avanserte alternativ",
        "rcfilters-filter-humans-label": "Menneske (ikke bot)",
        "rcfilters-filter-humans-description": "Redigeringer gjort av menneskelige brukere.",
        "rcfilters-filtergroup-reviewstatus": "Gjennomgangsstatus",
+       "rcfilters-filter-reviewstatus-unpatrolled-description": "Redigeringer som ikke er manuelt eller automatisk merket som patruljerte.",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Upatruljert",
+       "rcfilters-filter-reviewstatus-manual-description": "Redigeringer som er manuelt merket som patruljert.",
+       "rcfilters-filter-reviewstatus-manual-label": "Patruljert manuelt",
+       "rcfilters-filter-reviewstatus-auto-description": "Redigeringer utført av avanserte brukere på innhold som er automatisk merket som patruljert.",
+       "rcfilters-filter-reviewstatus-auto-label": "Autopatruljert",
        "rcfilters-filtergroup-significance": "Betydning",
        "rcfilters-filter-minor-label": "Mindre endringer",
        "rcfilters-filter-minor-description": "Redigeringer merket som mindre av brukeren.",
        "recentchangeslinked-feed": "Relaterte endringer",
        "recentchangeslinked-toolbox": "Relaterte endringer",
        "recentchangeslinked-title": "Endringer relatert til «$1»",
-       "recentchangeslinked-summary": "Skriv inn et sidenavn for å se endringer på sider som lenker til eller lenkes fra den siden. (For å se medlemmene av en kategori, skriv inn Kategori:Kategorinavn.) Endringer på sider som er på [[Special:Watchlist|overvåkningslista di]] er i <strong>fet skrift</strong>.",
+       "recentchangeslinked-summary": "Skriv inn et sidenavn for å se endringer på sider som lenker til eller lenkes fra den siden. (For å se medlemmene av en kategori, skriv inn Kategori:Kategorinavn.) Endringer på sider som er på din [[Special:Watchlist|overvåkningsliste]] er angitt med <strong>fet skrift</strong>.",
        "recentchangeslinked-page": "Sidenavn:",
        "recentchangeslinked-to": "Vis endringer på sider som lenker til den gitte siden istedet",
        "recentchanges-page-added-to-category": "[[:$1]] ble lagt til i kategorien",
        "deadendpages": "Blindveisider",
        "deadendpagestext": "Følgende sider lenker ikke til andre sider på {{SITENAME}}.",
        "protectedpages": "Beskyttede sider",
+       "protectedpages-filters": "Filtre:",
        "protectedpages-indef": "Kun beskyttelser på ubestemt tid",
        "protectedpages-summary": "Denne siden viser en liste av eksisterende sider som for tiden er beskyttet. For å se en liste av sider som er beskyttet mot opprettelse, se [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "Kun dypbeskyttelse",
        "version-specialpages": "Spesialsider",
        "version-parserhooks": "Parsertillegg",
        "version-variables": "Variabler",
+       "version-editors": "Bidragsytere",
        "version-antispam": "Søppelpostforebygging",
        "version-other": "Annet",
        "version-mediahandlers": "Mediehåndterere",
        "unlinkaccounts-success": "Kontoens lenking ble fjernet.",
        "authenticationdatachange-ignored": "Autentiseringsdataendringen ble ikke håndtert. Muligens ble ingen tilbyder konfigurert?",
        "userjsispublic": "Merk: JavaScript-undersidene bør ikke inneholde konfidensielle data, siden de kan ses av andre brukere.",
+       "userjsonispublic": "OBS: JSON-undersider bør ikke inneholde privat informasjon ettersom de kan leses av andre brukere.",
        "usercssispublic": "Merk: CSS-undersidene bør ikke inneholde konfidensielle data siden de kan ses av andre brukere.",
        "restrictionsfield-badip": "Ugyldig IP-adresse eller intervall: $1",
        "restrictionsfield-label": "Tillatte IP-intervaller:",
index 117f91c..b37e66a 100644 (file)
        "recentchangeslinked-feed": "Verwante wijzigingen",
        "recentchangeslinked-toolbox": "Verwante wijzigingen",
        "recentchangeslinked-title": "Wijzigingen verwant aan \"$1\"",
-       "recentchangeslinked-summary": "Voer een paginanaam in om bewerkingen te zien van pagina's waarheen vanaf die pagina verwezen wordt of die ernaar verwijzen. (Om leden van een categorie te zien, voert u <kbd>Categorie:''Naam van categorie''</kbd> in.) Bewerkingen van pagina's op [[Special:Watchlist|uw volglijst]] worden <strong>vet</strong> weergegeven.",
+       "recentchangeslinked-summary": "Voer een paginanaam in om bewerkingen te zien van pagina's waarheen vanaf die pagina verwezen wordt of die ernaar verwijzen. (Om leden van een categorie te zien, voert u <kbd>{{ns:category}}:''Naam van categorie''</kbd> in.) Bewerkingen van pagina's op [[Special:Watchlist|uw volglijst]] worden <strong>vet</strong> weergegeven.",
        "recentchangeslinked-page": "Paginanaam:",
        "recentchangeslinked-to": "Wijzigingen aan pagina's met koppelingen naar deze pagina bekijken",
        "recentchanges-page-added-to-category": "[[:$1]] aan categorie toegevoegd",
index 5676834..fcd18d5 100644 (file)
        "mw-widgets-dateinput-placeholder-month": "ÅÅÅÅ-MM",
        "mw-widgets-titleinput-description-new-page": "sida finst ikkje enno",
        "mw-widgets-titleinput-description-redirect": "omdirigering til $1",
+       "mw-widgets-usersmultiselect-placeholder": "Legg til fleire …",
        "date-range-from": "Frå dato:",
        "date-range-to": "Til dato:",
        "randomrootpage": "Tilfeldig rotside",
index 8a28548..4d71e75 100644 (file)
        "botpasswords-existing": "Form section label for the part of the form listing the user's existing bot passwords.",
        "botpasswords-createnew": "Form section label for the part of the form related to creating a new bot password.",
        "botpasswords-editexisting": "Form section label for the part of the form related to editing an existing bot password.",
+       "botpasswords-label-needsreset": "Indicator for when an existing bot password is invalid and needs to be reset.",
        "botpasswords-label-appid": "Form field label for the \"bot name\", internally known as the \"application ID\".",
        "botpasswords-label-create": "Button label for the button to create a new bot password.\n{{Identical|Create}}",
        "botpasswords-label-update": "Button label for the button to save changes to a bot password.\n{{Identical|Update}}",
        "botpasswords-restriction-failed": "Error message when login is rejected because the configured restrictions were not satisfied.",
        "botpasswords-invalid-name": "Error message when a username lacking the separator character is passed to BotPassword. Parameters:\n* $1 - The separator character.",
        "botpasswords-not-exist": "Error message when a username exists but does not a bot password for the given \"bot name\". Parameters:\n* $1 - username\n* $2 - bot name",
+       "botpasswords-needs-reset": "Error message when a bot password exists but needs to be reset. Parameters:\n* $1 - username\n* $2 - bot name",
        "resetpass_forbidden": "Used as error message in changing password. Maybe the external auth plugin won't allow local password changes.",
        "resetpass_forbidden-reason": "Like {{msg-mw|resetpass_forbidden}} but the auth provider gave a reason.\n\nParameters:\n* $1 - reason given by auth provider",
        "resetpass-no-info": "Error message for [[Special:ChangePassword]].\n\nParameters:\n* $1 (unused) - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description",
        "prefs-watchlist-edits": "Used in [[Special:Preferences]], tab \"Watchlist\".",
        "prefs-watchlist-edits-max": "Shown as hint in [[Special:Preferences]], tab \"Watchlist\"",
        "prefs-watchlist-token": "Used in [[Special:Preferences]], tab Watchlist.",
+       "prefs-watchlist-managetokens": "Label for the button to see and reset the user's private tokens",
        "prefs-misc": "Tab used on the [[Special:Preferences|user preferences]] special page.",
        "prefs-resetpass": "Button on user data tab in user preferences. When you click the button you go to the special page [[Special:ResetPass]].\n\n{{Identical|Change password}}",
        "prefs-changeemail": "Link on [[Special:Preferences]] to [[Special:ChangeEmail]]. [[Special:ChangeEmail]] also allows removing email address. \n\nSee also:\n* {{msg-mw|prefs-help-email-required|help}}\n* {{msg-mw|prefs-help-email|help}}\n* {{msg-mw|prefs-help-email-others|help}}\n* {{msg-mw|prefs-setemail|link title}}",
        "recentchangescount": "Used in [[Special:Preferences]], tab \"Recent changes\".",
        "prefs-help-recentchangescount": "Used in [[Special:Preferences]], tab \"Recent changes\".",
        "prefs-help-watchlist-token2": "Used in [[Special:Preferences]], tab Watchlist. (Formerly in {{msg-mw|prefs-help-watchlist-token}}.)",
+       "prefs-help-tokenmanagement": "Used in [[Special:Preferences]], Watchlist tab.",
        "savedprefs": "This message appears after saving changes to your user preferences.",
        "savedrights": "This message appears after saving the user groups on [[Special:UserRights]].\n* $1 - The user name of the user which groups was saved.",
        "timezonelegend": "{{Identical|Time zone}}",
index de172e5..841d011 100644 (file)
        "tog-watchlisthideminor": "Скрывать малые правки из списка наблюдения",
        "tog-watchlisthideliu": "Скрывать правки представившихся участников из списка наблюдения",
        "tog-watchlistreloadautomatically": "Обновлять список наблюдения автоматически всякий раз, когда изменяется фильтр (требуется JavaScript)",
-       "tog-watchlistunwatchlinks": "Ð\94обавиÑ\82Ñ\8c Ð² Ñ\81пиÑ\81ок Ð½Ð°Ð±Ð»Ñ\8eдениÑ\8f Ð¿Ñ\80Ñ\8fмÑ\8bе Ñ\81Ñ\81Ñ\8bлки Ð´Ð»Ñ\8f Ð¸Ñ\81клÑ\8eÑ\87ениÑ\8f Ð·Ð°Ð¿Ð¸Ñ\81ей (требуется JavaScript)",
+       "tog-watchlistunwatchlinks": "Ð\94обавиÑ\82Ñ\8c Ð¿Ñ\80Ñ\8fмÑ\8bе Ð¼Ð°Ñ\80кеÑ\80Ñ\8b Ð´Ð»Ñ\8f Ð²ÐºÐ»Ñ\8eÑ\87ениÑ\8f/иÑ\81клÑ\8eÑ\87ениÑ\8f Ð¸Ð· Ñ\81пиÑ\81ка Ð½Ð°Ð±Ð»Ñ\8eдениÑ\8f ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) Ð´Ð»Ñ\8f Ð½Ð°Ð±Ð»Ñ\8eдаемÑ\8bÑ\85 Ñ\81Ñ\82Ñ\80аниÑ\86 Ñ\81 Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ\8fми (длÑ\8f Ð¿ÐµÑ\80еклÑ\8eÑ\87ениÑ\8f Ñ\84Ñ\83нкÑ\86ий требуется JavaScript)",
        "tog-watchlisthideanons": "Скрывать правки анонимных участников из списка наблюдения",
        "tog-watchlisthidepatrolled": "Скрывать отпатрулированные правки из списка наблюдения",
        "tog-watchlisthidecategorization": "Скрывать категоризацию страниц",
        "action-undelete": "восстановление страниц",
        "action-suppressrevision": "просмотр и восстановление скрытых версий страниц",
        "action-suppressionlog": "просмотр частного журнала",
-       "action-block": "блокировку участника",
+       "action-block": "блокировка участника",
        "action-protect": "изменение уровня защиты этой страницы",
        "action-rollback": "быстрый откат изменений",
        "action-import": "импорт страниц из другой вики",
        "recentchangeslinked-feed": "Связанные правки",
        "recentchangeslinked-toolbox": "Связанные правки",
        "recentchangeslinked-title": "Связанные правки для «$1»",
-       "recentchangeslinked-summary": "Введите имя страницы, чтобы увидеть изменения на страницах, ссылающихся на эту страницу или, наоборот, c неё. (Чтобы увидеть членов категории, введите Category:Название категории). Изменения на страницах [[Special:Watchlist|вашего списка наблюдения]] отмечены <strong>жирным шрифтом</strong>.",
+       "recentchangeslinked-summary": "Введите имя страницы, чтобы увидеть изменения на страницах, ссылающихся на эту страницу или, наоборот, c неё. (Чтобы увидеть членов категории, введите {{ns:category}}:Название категории). Изменения на страницах [[Special:Watchlist|вашего списка наблюдения]] отмечены <strong>жирным шрифтом</strong>.",
        "recentchangeslinked-page": "Название страницы:",
        "recentchangeslinked-to": "Наоборот, показать изменения на страницах, которые ссылаются на указанную страницу",
        "recentchanges-page-added-to-category": "[[:$1]] добавлена в категорию",
index 7649054..170c9b8 100644 (file)
        "sitecsspreview": "<strong>Smekti belli aql-ak tɛerḍeḍ asebter CSS agi inek kan.\nMazal ur yettusmekti ara!</strong>",
        "sitejsonpreview": "<strong>Smekti belli aql-ak tɛerḍeḍ asebter JSON config agi inek kan.\nMazal ur yettusmekti ara!</strong>",
        "sitejspreview": "<strong>Smekti belli aql-ak tɛerḍeḍ asebter JavaScript inek kan.\nMazal ur yettusmekti ara!</strong>",
+       "userinvalidconfigtitle": "<strong>Aɣtal:</strong> Aglim \"$1\" ulac-it. Ur tettuḍ ara belli isebtar \".css\" d \".json u .js\" i txedmeḍ sseqdacen azwel i yesɛan isekkilen imecṭuḥen, s umedya: {{ns:user}}:Foo/vector.css akk d {{ns:user}}:Foo/Vector.css.",
+       "updated": "(Yettubeddel)",
+       "note": "<strong>Tamawt:</strong>",
+       "previewnote": "<strong>Ttagi d azar-timeẓriwt kan, ibeddlen mazal ur ttusmektin ara!</strong>\n\nCfut, ttagi d azar-timeẓriwt kan.\nIbeddlen mazal ur ttusmektin ara!",
+       "continue-editing": "Ṛuḥ ar taɣzut n ubeddel",
+       "previewconflict": "Pre-timeẓriwt-agi tesskan aḍris i yellan deg usawen lemmer tebɣiḍ a tt-tesmektiḍ.",
+       "session_fail_preview": "Suref-aɣ! ur nezmir ara a nesmekti abeddil inek axaṭer yella ugur.\nG leɛnayek ɛreḍ tikelt nniḍen. Lukan mazal yella ugur, ffeɣ umbeɛd kcem [[Special:UserLogout|logging out]].",
+       "session_fail_preview_html": "Ur nezmer ara an aklas ibeddilen inek/inem acku yella asṛuḥu n tilɣa deg taɣimit inek/inem.\n\n<em>Acku {{SITENAME}} i sermed azar n HTML, azaraskan yeseggelmes iwakken ur t-illint ara tinṭagin s Javascript.</em>\n\n<strong>Lukan abeddel agi d-aḥeqqani, ɛered tikkelt nniḍen.</strong>\nLukan yella ugur, [[Special:UserLogout|Senser]] dɣa qqen.",
+       "token_suffix_mismatch": "<strong>Abeddel inek/inem ur yeɣbel ara acku iminig inek/inem ur yesettengel ara s umellil isekkilen n uqqa deg asulay n ubeddel.</strong>\nTiririt agi telaq i usḍiqqef n usgufsu n uḍris deg usebter.\nUgur agi, yetilli tikwal mi seqdeceḍ aqeddac Proxy warisem yellan ɣef Web.",
+       "edit_form_incomplete": "<strong>Kra n iḥricen n tiferkit n ubeddel ur gweḍen ara ar uqeddac, ilaq ad selkeneḍ ma ibeddilen ur erẓen ara dɣa ɛreḍ tikkelt nniḍen.</strong>",
        "editing": "Aglaf n $1",
+       "creating": "Asnulfu n $1",
        "editingsection": "Aglaf n $1 (tagzemt)",
+       "editingcomment": "Abeddel n $1 (tigezmi tamaynut)",
+       "editconflict": "Amennuɣ deg ubeddel: $1",
+       "explainconflict": "Amdan nniḍen ibeddel asebter-agi asmi telliḍ tettbeddileḍ.\nAḍris deg usawen yesɛa asebter am yella tura.\nIbeddlen inek ahaten deg ukessar.\nYesfek ad txelṭeḍ ibeddlen inek akk d usebter i yellan.\n<strong>Ala</strong> aḍris deg usawen i yettusmekta asmi twekkiḍ \"$1\".",
+       "yourtext": "Aḍris inek",
+       "storedversion": "Tasiwelt yettusmketen",
+       "editingold": "<strong>AƔTAL</strong> Aqlak tettbeddileḍ tasiwelt taqdimt n usebter-agi.\nMa ara t-tesmektiḍ, akk ibeddlen i yexdmen seg tasiwelt-agi ruḥen.",
+       "yourdiff": "Imgerraden",
+       "copyrightwarning": "Ssen belli akk tikkin deg {{SITENAME}} hatent ttwaznen seddaw $2 (Ẓer $1 akken ad tessneḍ kter). Lukan ur tebɣiḍ ara aru inek yettubeddel neɣ yettwazen u yettwaru deg imkanen nniḍen, ihi ur t-tazneḍ ara dagi.<br />\nAqlak teggaleḍ belli tureḍ wagi d kečč, neɣ teddmiḍ-t seg taɣult azayez neɣ iɣbula tilelliyin.\n<strong>UR TEFKIḌ ARA AXDAM S COPYRIGHT MEBLA TURAGT!</strong>",
+       "copyrightwarning2": "Ssen belli akk tikkin deg {{SITENAME}} zemren ad ttubeddlen neɣ ttumḥan sɣur imdanen wiyaḍ. Lukan ur tebɣiḍ ara aru inek yettubeddel neɣ yettwazen u yettwaru deg imkanen nniḍen, ihi ur t-tazneḍ ara dagi.<br />\nAqlak teggaleḍ belli tureḍ wagi d kečč, neɣ teddmiḍ-t seg taɣult azayez neɣ iɣbula tilelliyin (ẓer $1 akken ad tessneḍ kter).\n<strong>UR TEFKIḌ ARA AXDAM S COPYRIGHT MEBLA TURAGT!</strong>",
+       "longpageerror": "<strong>Anezri : Aḍris i sekcemeḍ yeɛbeṛ {{PLURAL:$1|yiwen kilobyte|$1 kilobytes}}, tiddi-yagi kter n talast yellan af {{PLURAL:$2|yiwen kilobyte|$1 kilobytes}}.</strong>\nUr yezmer ara ad yetwaḥrez.",
+       "readonlywarning": "<strong>ƔUR-WET : taffa n isefka t-sekkweṛ i timhelin n ibeddi. Ur tzemreḍ ara ad ḥrezeḍ  ibeddilen tura.</strong>\nTzemreḍ ad nɣeleḍ aḍris ik/im deg ufaylu iwakken ad tesqedceḍ sakin.\n\nAnedbal i sekkweṛen taffa n isefka agi, yefka-d taɣẓint agi : $1",
+       "protectedpagewarning": "<strong>ƔUR-WET : Asebter-agi yettwaḥrez, inedbalen kan i zemren a t-beddlen.</strong>\nAsekcem aneggaru n uɣmis yella ddaw-agi:",
+       "semiprotectedpagewarning": "<strong>Tamawt:</strong> Asebter-agi yettwaḥrez, iseqdacen yesɛan amiḍan kan i zemren a t-beddlen.\nAsekcem aneggaru n uɣmis yella ddaw-agi:",
+       "cascadeprotectedwarning": "<strong>ƔUR-WET:</strong> Asebter-agi yettwaḥrez, inedbalen kan i zemren a t-beddlen. Yettwaḥrez acku yettwassekcem  deg {{PLURAL:$1|asebter i ḥerzen agi yesɛan|isebtar i ḥerzen agi yesɛan}} « amesten s uceṛcuṛ » i sermeden :",
+       "titleprotectedwarning": "<strong>ƔUR-WET: Asebter agi yemesten, dɣa ilaq ad sɛuḍ [[Special:ListGroupRights|izerfan usligen]] iwakken at id snulfuḍ.</strong> Asekcem aneggaru n uɣmis yebeqqeḍ ddaw agi:",
+       "templatesused": "{{PLURAL:$1|Talɣa i seqdacen|Tilɣatin i seqdacen}} deg usebter agi:",
+       "templatesusedpreview": "{{PLURAL:$1|Talɣa i seqdacen|Tilɣatin i seqdacen}} deg azaraskan agi:",
+       "templatesusedsection": "{{PLURAL:$1|Talɣa i seqdacen|Tilɣatin i seqdacen}} deg tigezmi agi:",
+       "template-protected": "(yettwaḥrez)",
+       "template-semiprotected": "(azin-yettwaḥrez)",
+       "hiddencategories": "Asebter agi yella deg {{PLURAL:$1|Taggayt i ffren|Tiggayin i ffren}} agi:",
+       "nocreatetext": "{{SITENAME}} yekref iẓubaẓ n usnulfu n isebtar imaynuten.\nTzemreḍ ad uɣaleḍ ar deffir dɣa ad beddeleḍ asebter yellan yakan, naɣ [[Special:UserLogin|ad qqeneḍ naɣ ad snulfuḍ amiḍan]].",
+       "nocreate-loggedin": "Ur tesɛiḍ ara turagt i usnulfu n isebtar imaynuten.",
+       "sectioneditnotsupported-title": "Abeddel n tigezmi agi ur yezmer ara",
+       "sectioneditnotsupported-text": "Abeddel n tigezmi ur yezmer ara deg usebtar agi n ubeddel.",
+       "permissionserrors": "Agul n turagt",
+       "permissionserrorstext": "Ur tesɛiḍ ara turagt iwakken ad xedmeḍ wayagi i {{PLURAL:$1|taɣẓint|tiɣẓinin}} agi:",
+       "permissionserrorstext-withaction": "Ur sɛiḍ ara ttesriḥ af $2, i {{PLURAL:$1|taɣẓint|tiɣẓinin}} agi:",
+       "recreate-moveddeleted-warn": "<strong>Ɣur-wet : asebter agi i tebɣam ad snulfum, yetwekkes uqbel.</strong>\n\nIlaq ad snulfum asebter agi haca ma i xater. Aɣmis n isebtaren i twekkesen yella ddaw-agi :",
+       "moveddeleted-notice": "Asebter-a yettwakkes. \nAɣmis n tukksa, ammesten neɣ asenkez n usebter yettwammel-d ddaw-a i uwelleh.",
+       "moveddeleted-notice-recent": "Nesḥissef, imi melmi kan i yettwakkes usebter-a (deg 24 n yisragen ineggura). Iɣmisen n tukksa, n ummesten, akked usnifel n yisem i usebter ttunefken-d ddaw-a i uwelleh.",
+       "log-fulllog": "Ẓeṛ aɣmis ummid",
+       "edit-hook-aborted": "Abrir n ubeddel s usiɣzef.\nTamentilt warisem",
+       "edit-gone-missing": "Ur yezmer ara ad yemucceḍ asebter agi.\nAhat yetwemḥa.",
+       "edit-conflict": "Amgirred n ubeddel.",
+       "edit-no-change": "Abeddel inek/inem ur yetwexdam ara acku ur di ban ara abeddel deg uḍris.",
+       "postedit-confirmation-created": "Asebter ittwarna.",
+       "postedit-confirmation-restored": "Asebter yuɣal-d.",
+       "postedit-confirmation-saved": "Abeddel inek/inem yetwakles.",
+       "edit-already-exists": "Asebter amaynut ur d yesnufu ara.\nYella yakan.",
+       "defaultmessagetext": "Izen s lexṣas",
+       "content-failed-to-parse": "Tasleṭ n ugbur n $2 i talɣa $1 texseṛ : $3",
+       "invalid-content-data": "Isefka n ugbur ur ɣbelen ara",
+       "content-not-allowed-here": "Agbur \"$1\" ur yesɛa ara turagt ɣef usebter [[$2]]",
+       "editwarning-warning": "Ma ad teffeɣeḍ seg usebter-agi ad tesṛuḥeḍ akk ibeddilen i tegiḍ.\nMa teqqeneḍ, tzemreḍ ad tsenseḍ alɣu-agi deg tigezmi \"{{int:prefs-editing}}\"  n ismenyifen-ik(im).",
+       "editpage-invalidcontentmodel-title": "Taneɣruft n ugbur ur tettwasefrak ara",
+       "editpage-invalidcontentmodel-text": "Taneɣruft n ugbur \"$1\" ur tettwasefrak ara.",
+       "editpage-notsupportedcontentformat-title": "Amasal n ugbur ur d-yetwarfed ara",
+       "editpage-notsupportedcontentformat-text": "Amasal n ugbur $1 ur d-yetwarfed ara sɣur talɣa n ugbur $2.",
+       "content-model-wikitext": "wikiaḍris",
+       "content-model-text": "aḍris afraray",
+       "content-model-javascript": "JavaScript",
+       "content-json-empty-object": "Asentel d ilem",
+       "content-json-empty-array": "Talfelwit d tilemt",
+       "deprecated-self-close-category": "Asebter iseqdacen yir tiṛekkizin HTM tumdilin tiwurmanin",
+       "expensive-parserfunction-warning": "'''Ɣur-wet :''' Asebter agi yesɛa aṭas n tiɣriwin ar tiseɣnin ɣlayen n umsisleḍ taseddast.\nIlaq ad i sɛu ddaw n  $2 {{PLURAL:$2|tiɣri|tiɣriwin }}, wannag tura {{PLURAL:$1|tella $1 tiɣri|llant $1 tiɣriwin}}.",
+       "expensive-parserfunction-category": "Isebtar yesɛan aṭas tiɣriwin ɣlayen n tiseeɣnin n umsisleḍ taseddast",
+       "post-expand-template-inclusion-warning": "<strong>Ɣur-wet:</strong> Asebter agi yesɛa aṭas tilɣatin. Kra n tilɣatin ur zemrent ara ad seqdacent.",
+       "post-expand-template-inclusion-category": "Isebtaren i sɛan aṭas tilɣatin",
+       "post-expand-template-argument-warning": "<strong>Ɣur-wet</strong>: Asebter agi yesɛa tuccḍa deg aɣewwar n yiwet talɣa.",
+       "post-expand-template-argument-category": "Isebtaren i sɛan iɣewwaren n talɣa ur skazelen ara",
+       "parser-template-loop-warning": "N-ufad talɣa s tineddict: [[$1]]",
+       "parser-template-recursion-depth-warning": "Talast n lqay n tiɣriwin n tilɣatin tefel ($1)",
+       "language-converter-depth-warning": "Talast n lqay n uselkat n tutlayt tefel ($1)",
+       "node-count-exceeded-category": "Isebtar anda amḍa n tikerwas yefel",
+       "node-count-exceeded-category-desc": "Asebter iɛedda amḍan attas afellay n tkerras",
+       "node-count-exceeded-warning": "Asebter iɛedda amḍan afellay n tkerras",
+       "expansion-depth-exceeded-category": "Isebtar anda lqay n uderrec yefel",
+       "expansion-depth-exceeded-category-desc": "Asebter iɛedda talqayt n temɣer tafellayt.",
+       "expansion-depth-exceeded-warning": "Isebtar yefelen lqay n uderrec",
+       "parser-unstrip-loop-warning": "Tifin n tineddict ur nezmer ara an sentuter",
+       "unstrip-depth-warning": "Talast n usniles ur nezmer ara an sentuter tefel ($1)",
+       "converter-manual-rule-error": "Tifin n unezri deg alugen awfus n uselket n tutlayt",
+       "undo-success": "Tzemreḍ ad tessefsuḍ abeddil. Ssenqed asidmer akken ad tessneḍ ayen tebɣiḍ ad txdmeḍ d ṣṣeḥ, umbeɛd smekti ibeddlen u tkemmleḍ ad tessefsuḍ abeddil.",
+       "undo-failure": "Ur yezmir ara ad issefu abeddel axaṭer yella amennuɣ abusari deg ubeddel.",
+       "undo-norev": "Abeddel ur yezmer ara ad yetwekkes acku ulac-itt naɣ tetwekkes yakan",
+       "undo-nochange": "Ad yettban d akken abeddel yettwasefsex yakan.",
+       "undo-summary": "Ssefsu tasiwelt $1 sɣur [[Special:Contributions/$2|$2]] ([[User talk:$2|Meslay]])",
+       "undo-summary-username-hidden": "Semmewet tacaggart $1 sɣur amseqdac yeffren",
+       "cantcreateaccount-text": "Asnulfu n umiḍan seg tansa IP (<b>$1</b>) tekyef sɣur [[User:$3|$3]].\n\nTaɣẓint n $3 : <em>$2</em>",
+       "cantcreateaccount-range-text": "Asnulfu n umiḍan seg tansiwin IP deg tagrumma <strong>$1</strong>, i sseddan tansa inek/inem IP (<strong>$4</strong>), twawḥelen sɣur [[User:$3|$3]].\n\nTaɣẓint i-d yefka/tefka $3 : <em>$2</em>",
+       "viewpagelogs": "Ẓer aɣmis n usebter-agi",
+       "nohistory": "Ulac amezruy n yibeddlen i usebter-agi.",
+       "currentrev": "Tasiwelt n tura",
+       "currentrev-asof": "Azmez n lqem taneggarut d  $1",
+       "revisionasof": "Tasiwelt n wass $1",
+       "revision-info": "Aceggir-agi yettwag di $1 sɣuṛ {{GENDER:$6|$2}}$7",
+       "previousrevision": "←Tasiwelt taqdimt",
+       "nextrevision": "Tasiwelt tamaynut→",
+       "currentrevisionlink": "Tasiwelt n tura",
+       "cur": "tura",
+       "next": "ameḍfir",
+       "last": "amgirred",
+       "page_first": "amezwaru",
+       "page_last": "aneggaru",
+       "histlegend": "Axtiri n umgerrad: rcem tankulin akken ad teẓreḍ imgerraden ger tisiwal u wekki ɣef enter/entrée neɣ ɣef taqeffalt deg ukessar.<br />\nTabadut: (tura) = amgirred akk d tasiwelt n tura,\n(amgirred) = amgirred akk d tasiwelt ssabeq, M = abeddel afessas.",
+       "history-fieldset-title": "Nadi iceggiren",
+       "history-show-deleted": "Aceggir yettwakksen kan",
+       "histfirst": "tiqdimin",
+       "histlast": "timaynutin",
+       "historysize": "({{PLURAL:$1|1 atamḍan|$1 itamḍanen}})",
+       "historyempty": "(amecluc)",
+       "history-feed-title": "Amezruy n tsiwelt",
+       "history-feed-description": "Amezruy n tsiwelt n usebter-agi deg wiki",
+       "history-feed-item-nocomment": "$1 deg $2",
+       "history-feed-empty": "Asebter i tebɣiḍ ulac-it.\nAhat yettumḥa neɣ yettbeddel isem-is.\nƐreḍ [[Special:Search|ad tnadiḍ deg wiki]] ɣef isebtar imaynuten.",
+       "history-edit-tags": "Ẓreg tirekkizin n ileqman yettwafernen",
+       "rev-deleted-comment": "(agzul n taẓrigt yettwakes)",
+       "rev-deleted-user": "(isem n wemseqdac yettwakes)",
+       "rev-deleted-event": "(talqayt n umazray tettwakkes)",
+       "rev-deleted-user-contribs": "[isem n useqdac naɣ tansa IP yetwemḥa - abeddel yeffer deg tiwsitin]",
+       "rev-deleted-text-permission": "Lqem n usebter agi <strong>tetwesfeḍ</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].",
+       "rev-deleted-text-unhide": "Lqem n usebter agi <strong>tetwesfeḍ</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].\nTzemreḍ meqqar [$1 ad ẓṛeḍ lqem agi]  ma tebɣiḍ",
+       "rev-suppressed-text-unhide": "Lqem n usebter agi <strong>tetwekkes</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n umḥu].\nTzemreḍ meqqar [$1 ad ẓṛeḍ lqem agi]  ma tebɣiḍ",
+       "rev-deleted-text-view": "Lqem n usebter agi <strong>tetwesfeḍ</strong>.\nTzemreḍ att ẓṛeḍ ; tilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].",
+       "rev-suppressed-text-view": "Lqem n usebter agi <strong>tetwekkes</strong>.\nTzemreḍ att ẓṛeḍ ; tilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n umḥu].",
+       "rev-deleted-no-diff": "Ur tzemreḍ ara ad ẓṛeḍ \"diff\" agi acku yiwet n lqem-is <strong>tetwesfeḍ</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].",
+       "rev-suppressed-no-diff": "Ur tzemreḍ ara ad ẓṛeḍ \"diff\" agi acku yiwet n lqem-is <strong>tetwekkes</strong>.",
+       "rev-deleted-unhide-diff": "Yiwen lqem n tameẓla agi <strong>yetwesfeḍ</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].\nTzemreḍ meqqar [$1 ad ẓṛeḍ tameẓla agi] ma tebɣiḍ",
+       "rev-suppressed-unhide-diff": "Yiwen lqem n tameẓla agi <strong>yetwekkes</strong>.\nTilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n umḥu].\nTzemreḍ meqqar [$1 ad ẓṛeḍ tameẓla agi] ma tebɣiḍ",
+       "rev-deleted-diff-view": "Yiwen lqem n \"diff\" agi <strong>yetwekkes</strong>.\nTzemreḍ att ẓṛeḍ ; tilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n umḥu].",
+       "rev-suppressed-diff-view": "Yiwen lqem n \"diff\" agi <strong>yetwesfeḍ</strong>.\nTzemreḍ att ẓṛeḍ ; tilɣa llant deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n usfeḍ].",
+       "rev-delundel": "ssken/ffer",
+       "rev-showdeleted": "Ssken",
+       "revisiondelete": "Mḥu/kkes amḥay tisiwal",
+       "revdelete-nooldid-title": "Lqem asaḍas ur i ɣbel ara",
+       "revdelete-nooldid-text": "Ur textareḍ ara lqem nnican akken ad txedmeḍ tawuri fell-as.",
+       "revdelete-no-file": "Afaylu id ssefruḍ ur yella ara.",
+       "revdelete-show-file-confirm": "Tebɣriḍ ad mḥuḍ tacaggart n ufaylu « <nowiki>$1</nowiki> » n $2 af $3 ?",
+       "revdelete-show-file-submit": "Ih",
+       "logdelete-selected": "{{PLURAL:$1|Tamirt n uɣmis tettwafren|Isallen n uɣmis ttwafernen}}:",
+       "revdelete-confirm": "Sergeg ma tebɣiḍ ad xedmeḍ tigawt agi, fehmeḍ inalkamen, dɣa temtawiḍ s [[{{MediaWiki:Policy-url}}|ilugan]].",
+       "revdelete-suppress-text": "Ilaq tukksa att illi <strong>kan</strong> deg tijṛa agi:\n* Tilɣa ahat tinergamin\n* Tilɣa ur sɛant ara amkan d-agi\n*: <em>tansa, uḍḍun n tilifun, uḍḍun n taɣellist tamettit, …</em>",
+       "revdelete-legend": "Sbebd akref n tamuɣli",
+       "revdelete-hide-text": "Aḍris n tacaggart",
+       "revdelete-hide-image": "Ffer ayen yellan deg ufaylu",
+       "revdelete-hide-name": "Ffer iberdan d iɣewwaṛen",
+       "revdelete-hide-comment": "Beddel agzul",
+       "revdelete-hide-user": "Isem n umseqdac/Tansa IP n umaẓrag",
+       "revdelete-hide-restricted": "Mḥu isefka agi i inedbalen d yimdanen wiyaḍ",
+       "revdelete-radio-same": "(ur beddel ara)",
+       "revdelete-radio-set": "Udrig",
+       "revdelete-radio-unset": "Yeban",
+       "revdelete-suppress": "Kkes talɣut seg inedbalen d yimdanen wiyaḍ",
+       "revdelete-unsuppress": "Kkes icekkilen ɣef tisiwal i yuɣalen-d",
+       "revdelete-log": "Ayɣer:",
+       "revdelete-submit": "Snes {{PLURAL:$1|i tacaggart i tettwafren|i ticggarin i tettwafren}}",
+       "revdelete-success": "Asekkud n ileqman yemucce war uguren.",
+       "revdelete-failure": "Iẓṛi n lqem ur yemucceḍ ara:\n$1",
+       "logdelete-success": "Asekkud n tamirt yettuxdem.",
+       "logdelete-failure": "Iẓṛi n uɣmis ur yezmer ara ad yesbadu:\n$1",
+       "revdel-restore": "beddel timezrit",
+       "pagehist": "Amezruy n usebter",
+       "deletedhist": "Amezruy yemḥa",
+       "revdelete-hide-current": "Yella anezri imi nemḥa aferdis yezemzen ass n $1 af $2 : d lqem aneggaru.\nUr yezmer ara ad yemḥu.",
+       "revdelete-show-no-access": "Yella anezri imi n beqqeḍ aferdis yezemzen ass n $1 af $2 : yecreḍ am \"ukrif\".\nUr tesɛiḍ ara izerfan n wadduf.",
+       "revdelete-modify-no-access": "Yella anezri imi nebeddel aferdis yezemzen ass n $1 af $2 : yecreḍ am \"ukrif\".\nUr tesɛiḍ ara izerfan n wadduf.",
+       "revdelete-modify-missing": "Yella anezri imi nebeddel aferdis yesɛan ID $1 : Ulac-it deg taffa n isefka !",
+       "revdelete-no-change": "<strong>Ɣur-wet:</strong> Aferdis yezemzen ass n $1 af $2 yesɛa yakan iɣewwaren n iẓṛi i tebɣiḍ.",
+       "revdelete-concurrent-change": "Yella anezri imi nebeddel aferdis yezemzen ass n $1 af $2: aẓayeris yetwebeddel sɣur amḍan nniḍen mi tbeddeleḍ\nẒeṛ iɣmisen.",
+       "revdelete-only-restricted": "Yella anezri imi nemḥa asekcem yezemzen ass n $1 af $2 : ur tzemreḍ ara ad mḥuḍ iferdisen agi i inedbalen war ad fruḍ tixtiṛiyin nniḍen n umḥu.",
+       "revdelete-reason-dropdown": "Tiɣẓinin timiranin n umḥu :\n** Akukel n izerfan umeskar (copyright) ;\n** Iwenniten naɣ tilɣa n yiwen ur yezgan ara ;\n** Tilɣa i zemren ad rgemen.",
+       "revdelete-otherreason": "Taɣẓint nniḍen/taɣzint tamarnant:",
+       "revdelete-reasonotherlist": "Taɣẓint nniḍen",
+       "revdelete-edit-reasonlist": "Beddel tiɣẓinin n umḥu i-d-yettuɣalen",
+       "revdelete-offender": "Ameskar n tacaggart:",
+       "suppressionlog": "Aɣmis n isfaḍen",
+       "suppressionlogtext": "Ddaw-agi, umuɣ n tukksiwin d ikyafen yellan ɣef ugbur yeffren i inedbalen.\nẒeṛ [[Special:BlockList|umuɣ ikyafen]] i umuɣ n tiririyin d ikyafen yellan d imahlanen.",
+       "mergehistory": "Zdi amezruy n isebtar",
+       "mergehistory-header": "Asebtar agi aken yeǧǧ ad tesduklem ileqman n umezruy n usebtar unṣib γer usebtar amaynut.\nSenked d akken tamhelt agi ad eǧǧ amezruy n usebtar ad ikemmel.",
+       "mergehistory-box": "Zdi lqem n sin isebtar",
+       "mergehistory-from": "Azar n usebter:",
+       "mergehistory-into": "Aserken n usebter:",
+       "mergehistory-list": "Amezruy n ibeddilen i nezmer an zdi",
+       "mergehistory-merge": "Ileqman id iteddun n [[:$1]] zemren ad twasduklen d [[:$2]]. Seqdec tigejdit n tqeffalt ṛadyu iwakken ad tesdukleḍ ala ileqman yettwasnulfan seg tazwara armi d azmez yettwamlan. Ẓeṛ d akken aseqdec n iseγwan n tunigin ad iwennez tigejdit agi.",
+       "mergehistory-go": "Ẓeṛ ibeddilen i nezmer an zdi",
+       "mergehistory-submit": "Azday n ileqman",
+       "mergehistory-empty": "Ulac lqem i nezmer an zdi.",
+       "mergehistory-done": "$3 {{PLURAL:$3|lqem|ileqman}} n $1 {{PLURAL:$3|yezdukel|zdukelen}} deg [[:$2]].",
+       "mergehistory-fail": "Ulamek an zdukel imezruyen. Fru tikkelt nniḍen asebter d iɣewwaren is n uzmez.",
+       "mergehistory-fail-invalid-source": "Asebter aɣbalu d arameɣtu.",
+       "mergehistory-no-source": "Azar n usebter $1 ulac-it.",
+       "mergehistory-no-destination": "Aserken n usebter $1 ulac-it",
+       "mergehistory-invalid-source": "Azar n usebter ilaq ad i sɛu azwel i ɣbelen.",
+       "mergehistory-invalid-destination": "Aserken n usebter ilaq ad i sɛu azwel i ɣbelen.",
+       "mergehistory-autocomment": "[[:$1]] yezdukel s [[:$2]]",
+       "mergehistory-comment": "[[:$1]] yezdukel s [[:$2]]: $3",
+       "mergehistory-same-destination": "Asebter n azar d usebter n userken ur zemren ara ad illin d yiwen",
+       "mergehistory-reason": "Ayɣer:",
+       "mergelog": "Aɣmis n izdayen",
+       "revertmerge": "Fru",
+       "mergelogpagetext": "Attan tebdart n wesdukel umezruy usebtar deg win n usebtar nniḍen amaynut.",
+       "history-title": "Tiẓṛi tiss sint umezruy n \"$1\"",
+       "difference-title": "$1 : Tameẓla gar ileqman",
+       "difference-title-multipage": "Timeẓliwin gar isebtar \"$1\" d \"$2\"",
+       "difference-multipage": "(Tameẓla gar isebtar)",
+       "lineno": "Ajerriḍ $1:",
+       "compareselectedversions": "Ẓer imgerraden ger tisiwal i textareḍ",
+       "showhideselectedversions": "Ssken/Ffer ileqman i xtiṛen",
+       "editundo": "ssefsu",
+       "diff-empty": "(Ulac amgerrad)",
+       "diff-multi-sameuser": "({{PLURAL:$1|Yiwen n uceggir askudan sɣuṛ aseqdac iman-is  ur d-yettwasken ara|$1 N iceggiren iskudanen sɣuṛ aseqdac iman-is ur d-ttwaseknen ara}})",
+       "diff-multi-otherusers": "({{PLURAL:$1|Yiwen n uceggir agrawan|$1 n iceggiren igrawanen}} sɣur {{PLURAL:$2|yiwen n useqdac-nniḍen|$2 n iseqdacen}} ur {{PLURAL:$1|d-yettwasken ara|d-ttwaseknen ara}})",
+       "diff-multi-manyusers": "({{PLURAL:$1|Yiwen lqem agrawan|$1 ileqman igrawanen}} af {{PLURAL:$2|aseqdac|$2 iseqdacen}} {{PLURAL:$1|yeffer|ffren}})",
+       "difference-missing-revision": "{{PLURAL:$1|Yiwet tacaggart|$1 ticaggartin}} n tameẓla agi ($1) {{PLURAL:$2|ur tella ara (ulac)|ur llant ara (ulac)}}.\n\nAcku azday n tameẓla, ɣef wayen tsennedeḍ, d-aqbur. Asebter yemḥa.\nTzemreḍ ad affeḍ tilɣa deg [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} uɣmis n isebtar yekksen].",
+       "searchresults": "Igmad n unadi",
+       "searchresults-title": "Igmad n unadi i \"$1\"",
+       "titlematches": "Ayen yecban azwel n umegrad",
+       "textmatches": "Ayen yecban azwel n usebter",
+       "notextmatches": "ulac ayen yecban azwel n usebter",
+       "prevn": "{{PLURAL:$1|$1}} ssabeq",
+       "nextn": "{{PLURAL:$1|$1}} ameḍfir",
+       "prev-page": "Asebter yezrin",
+       "next-page": "Asebter d-iteddun",
+       "prevn-title": "$1 {{PLURAL:$1|agmud n uqbel|igmad n uqbel}}",
+       "nextn-title": "$1 {{PLURAL:$1|agmud n sakin|igmad n sakin}}",
+       "shown-title": "Beqqeḍ $1 {{PLURAL:$1|agmud|igmad}} s usebter",
+       "viewprevnext": "Ẓer ($1 {{int:pipe-separator}} $2) ($3).",
+       "searchmenu-exists": "<strong>Yella asebter s isem \"[[:$1]]\" deg wiki agi.</strong> {{PLURAL:$2|0=|See also the other search results found.}}",
+       "searchmenu-new": "<strong>Rnu asebter \"[[:$1]]\" ɣef uwiki-agi!</strong> {{PLURAL:$2|0=|Wali daɣen asebter yettwafen s unadi-ik.|Wali daɣen igmaḍ n unadi yettwafen.}}",
+       "searchprofile-articles": "Isebtar n ugbur",
+       "searchprofile-images": "Agetmedia",
+       "searchprofile-everything": "Akk",
+       "searchprofile-advanced": "Anadi anemhal",
+       "searchprofile-articles-tooltip": "Nadi deg $1",
+       "searchprofile-images-tooltip": "Nadi  ifuyla agetmedia",
+       "searchprofile-everything-tooltip": "Nadi deg akk usmel (ula deg isebtaren n umyannan)",
+       "searchprofile-advanced-tooltip": "Fren ideggen n isemawen i unadi",
        "search-result-size": "$1 ({{PLURAL:$2|1 awal|$2 awalen}})",
+       "search-result-category-size": "$1 {{PLURAL:$1|amseqdac|imseqdacen}} $2 ({{PLURAL:$2|adu-taggayt|adu-tiggayin}}, $3 {{PLURAL:$3|afaylu|ifuyla}})",
+       "search-redirect": "(awelleh seg $1)",
        "search-section": "(tagzemt $1)",
+       "search-category": "(tigezmi $1)",
+       "search-file-match": "(yzega i ugbur n ufaylu)",
+       "search-suggest": "D awal $1 i tnadiḍ ?",
+       "search-rewritten": "Igmaḍ yettwaseknen i $1. Anadi n $2 deg umḍiq-is.",
+       "search-interwiki-caption": "Igmaḍ n isenfaṛen atmaten",
+       "search-interwiki-default": "Igmaḍ si $1:",
+       "search-interwiki-more": "(ugar)",
+       "search-interwiki-more-results": "ugar n igmaḍ",
+       "search-relatedarticle": "Amassaɣ",
+       "searchrelated": "amassaɣ",
+       "searchall": "akk",
+       "showingresults": "Tamuli n {{PLURAL:$1|<strong>Yiwen</strong> wegmud|<strong>$1</strong> n yigmad}} seg  #<strong>$2</strong>.",
+       "search-showingresults": "{{PLURAL:$4|Agmuḍ <strong>$1</strong> si <strong>$3</strong>|Igmaḍ <strong>$1 - $2</strong> si <strong>$3</strong>}}",
+       "search-nonefound": "Ulac igmad i usuter agi.",
+       "search-nonefound-thiswiki": "ulac Agmuḍ l site",
+       "powersearch-legend": "Anadi amahlan",
+       "powersearch-ns": "Nadi deg tallunin n isemawen",
+       "powersearch-togglelabel": "Ɛellem:",
+       "powersearch-toggleall": "akk",
+       "powersearch-togglenone": "ulac",
+       "powersearch-remember": "Cfu ɣef ufran n yinadiyen d-iteddun",
+       "search-external": "Anadi yeffɣen",
+       "searchdisabled": "Anadi deg {{SITENAME}} yettwakkes. Tzemreḍ ad tnadiḍ s Google. Meɛna ur tettuḍ ara, tasmult n google taqdimt.",
+       "search-error": "Tella tuccḍa deg unadi n : $1",
+       "search-warning": "Alɣu yettwammel degu unadi n: $1",
+       "preferences": "Ismenyifen",
+       "mypreferences": "Ismenyifen",
+       "prefs-edits": "Amḍan n ibeddlilen :",
+       "prefsnologintext2": "Ttxilek(m) qqen aken ad snifleḍ ismenyifen inek(m).",
+       "prefs-skin": "Aglim",
+       "skin-preview": "azar-asekdan",
+       "datedefault": "Ur sɛiɣ ara asemyifi",
+       "prefs-labs": "Tiseɣnin « labs »",
+       "prefs-user-pages": "Isebtar n useqdac",
+       "prefs-personal": "Profile n wemseqdac",
+       "prefs-rc": "Ibeddilen imaynuten",
+       "prefs-watchlist": "Tabdart n uḍfaṛ",
+       "prefs-editwatchlist": "Ẓreg tabdart n uḍfaṛ",
+       "prefs-editwatchlist-label": "Ẓreg inekcam ɣef tedbart n uḍfaṛ:",
+       "prefs-editwatchlist-edit": "Wali sakin kkes izwal n tebdart-ik n uḍfaṛ",
+       "prefs-editwatchlist-raw": "Ẓreg tabdart n uḍfaṛ deg uskar arewway",
+       "prefs-editwatchlist-clear": "Sfeḍ tabdart-ik n uḍfaṛ",
+       "prefs-watchlist-days": "Amḍan n ussan i ubeqqeḍ deg umuɣ n uɛassi:",
+       "prefs-watchlist-days-max": "Afellay $1 {{PLURAL:$1|ass|ussan}}",
+       "prefs-watchlist-edits": "Amḍan afellay n ubeddel ara d-ibabnen deg tebdart n uḍfaṛ:",
+       "prefs-watchlist-edits-max": "Amḍan afellay : 1000",
+       "prefs-watchlist-token": "Tiddest  umuɣ n uɛassi:",
+       "prefs-misc": "Ismenyifien-nniḍen",
+       "prefs-resetpass": "Beddel awal n uɛaddi",
+       "prefs-changeemail": "Beddel neɣ kkes tansan n yimayl",
+       "prefs-setemail": "Sbadu yiwet tansa e-mail",
+       "prefs-email": "Tixtiṛiyin n tira",
+       "prefs-rendering": "Tummant",
+       "saveprefs": "Smekti",
+       "restoreprefs": "Err akkw iɣewwaren s lexṣas (deg akkw tigezmiwin)",
+       "prefs-editing": "Aglaf",
        "searchresultshead": "Iruzzi",
+       "stub-threshold": "Talast i umasal n iseɣwan n isumar ($1):",
+       "stub-threshold-sample-link": "amedya",
+       "stub-threshold-disabled": "Yensa",
+       "recentchangesdays": "Amḍan n ussan an beqqeḍ deg ibeddilen ineggura.",
+       "recentchangesdays-max": "Afellay $1 {{PLURAL:$1|ass|ussan}}",
+       "recentchangescount": "Amḍan n ibeddilen i ubeqqeḍ s lexṣas:",
+       "prefs-help-recentchangescount": "Wagi yesɛa deg-es ibeddilen ineggura, isebtar n umezruy d iɣmisen.",
+       "prefs-help-watchlist-token2": "Hattan tasarut tufurt n usuddem Web n umuɣ inek/inem n uḍfar.\nAkkw amḍan yesɛan tasarut agi, ad yezmer ad i ɣer umuɣ inek/inem n uḍfar, ur d-sselɣu ara tasarut agi ihi.\n[[Special:ResetTokens|Nqer d-agi ma tebɣiḍ ad wennezeḍ tasarut agi]].",
+       "savedprefs": "Ismenyifen-ik ttwaskelsen.",
+       "savedrights": "Izerfan n useqdac n {{GENDER:$1|$1}}  ttwaskelsen.",
+       "timezonelegend": "Iẓḍi n ukud:",
+       "localtime": "Asrag adigan :",
+       "timezoneuseserverdefault": "Seqdec azal s lexṣas n wiki ($1)",
+       "timezoneuseoffset": "Nniḍen (ssefru asekḥer)",
+       "servertime": "Asrag n uqeddac:",
+       "guesstimezone": "Sseqdec azal n iminig",
+       "timezoneregion-africa": "Tafriqt",
+       "timezoneregion-america": "Tamrikt",
+       "timezoneregion-antarctica": "Antarktik",
+       "timezoneregion-arctic": "Arktik",
+       "timezoneregion-asia": "Asya",
+       "timezoneregion-atlantic": "Agaraw At'lasi",
+       "timezoneregion-australia": "Usṭralya",
+       "timezoneregion-europe": "Turuft",
+       "timezoneregion-indian": "Agaraw Ahendi",
+       "timezoneregion-pacific": "Agaraw Amelwi",
+       "allowemail": "Eǧǧ imseqdacen wiyaḍ a k-aznen email",
        "prefs-searchoptions": "Iruzzi",
+       "prefs-namespaces": "Talluntin n isemawen",
+       "default": "ameslugen",
+       "prefs-files": "ifuyla",
+       "prefs-custom-css": "CSS asagen",
+       "prefs-custom-json": "JSON asagen",
+       "prefs-custom-js": "JavaScript asagen",
+       "prefs-common-config": "JavaScript  d CSS azduklan i akkw lebsa:",
+       "prefs-reset-intro": "Tzemreḍ ad seqdeceḍ asebter agi iwakken ad erreḍ iɣewwaren inek/inem ar azalen n lexṣas n usmel.\nWagi ur yezmer ara ad yetwekkes.",
+       "prefs-emailconfirm-label": "Aragag n tirawt:",
+       "youremail": "E-mail:",
+       "username": "{{GENDER:$1|Isem n umseqdac|Isem n tamseqdact}}:",
+       "prefs-memberingroups": "{{GENDER:$2|Aεeggal|Taɛggalt}} n {{PLURAL:$1|ugraw|igrawen}}:",
+       "group-membership-link-with-expiry": "$1 (arams d $2)",
+       "prefs-registration": "Azmez n tiggezt:",
+       "yourrealname": "Isem n ṣṣeḥ:",
+       "yourlanguage": "Tutlayt:",
+       "yourvariant": "Lqem nniḍen n tutlayt n ugbur:",
+       "prefs-help-variant": "Lqem naɣ inun inek/inem iwakken an beqqeḍ agbur n wiki agi.",
+       "yournick": "Azmul amaynut:",
+       "prefs-help-signature": "Iwenniten ɣef isebtar n umeslay ilaq ad illin zmelen s « <nowiki>~~~~</nowiki> », sakin ad i sɛu aselkat ɣer azmul inek/inem dɣa azmez d usrag.",
+       "badsig": "Azmul mačči d ṣaḥiḥ; \nSsenqed tags n HTML.",
+       "badsiglength": "Azmul inek/inem, teɣwzi-s tameqqṛant aṭas.\nUr ilaq ara ad i sɛu ugar n $1 {{PLURAL:$1|asekkil|isekkilen}}.",
+       "yourgender": "Amek i tebɣiḍ ad n-ini fellak(m) ?",
+       "gender-unknown": "Ur bɣiɣ ara ad iniɣ",
+       "gender-male": "Yebeddel isebtar n wiki",
+       "gender-female": "Tebeddel isebtar n wiki",
+       "prefs-help-gender": "Sbadu asmenyif agi d-afrayan.\nAseɣẓan agi yetseqdac azal-is iwakken ad yemeslay s kečč/kem dɣa ad yefk isem-ik/im i wiyaḍ nniḍen s useqdac n tawsit tajeṛṛumant.\nTalɣut agi attili d-tazayezt.",
+       "email": "E-mail",
+       "prefs-help-realname": "Isem n tidet d anufran.\nma tefkeḍ-t-id, ad yettuseqdac iwaken ad ak(m)-d ttwanefkent tebzirin inek(m).",
+       "prefs-help-email": "E-mail (am tebɣiḍ): Teǧǧi imseqdacen wiyaḍ a k-aznen email mebla ma ẓren tansa email inek.",
+       "prefs-help-email-others": "Zemreḍ ad eǧǧeḍ wiyeḍ nniḍen ak(akem) cceqɛen izen deg usebter-ik (im) n umyannan war ad effekeḍ tamagit-ik (im).",
+       "prefs-help-email-required": "Tansa e-mail tesḍulli.",
+       "prefs-info": "Tilɣa n udasil",
+       "prefs-i18n": "Asagraɣlan",
+       "prefs-signature": "Azmul",
+       "prefs-dateformat": "Amasal n izemzan",
+       "prefs-timeoffset": "Asekḥer n usrag",
+       "prefs-advancedediting": "Tixtiṛiyin timuta",
+       "prefs-editor": "Amaẓrag",
+       "prefs-preview": "azar-asekdan",
+       "prefs-advancedrc": "Tixtiṛiyin timahlanin",
+       "prefs-advancedrendering": "Tixtiṛiyin timahlanin",
+       "prefs-advancedsearchoptions": "Tixtiṛiyin timahlanin",
+       "prefs-advancedwatchlist": "Tixtiṛiyin timahlanin",
+       "prefs-displayrc": "Tixtiṛiyin n ubeqqeḍ",
+       "prefs-displaywatchlist": "Tixtiṛiyin n ubeqqeḍ",
+       "prefs-tokenwatchlist": "Tiddest",
+       "prefs-diffs": "Timeẓliwin",
+       "prefs-help-prefershttps": "Asmenyif agi, ad yelḥu ar tuqqna ay d-yetteddun.",
+       "prefs-tabs-navigation-hint": "Taxbalut: Tzemreḍ ad seqdeceḍ tineccabin n uzelmaḍ d uyeffus iwakken ad ssileleḍ gar iccaren.",
+       "userrights": "Izerfan n useqdac",
+       "userrights-lookup-user": "Fren aseqdac",
+       "userrights-user-editname": "Sekcem isem n useqdac",
+       "editusergroup": "Sali-d igrawen n iseqdacen",
+       "editinguser": "Abeddel n izerfan n {{GENDER:$1|useqdac|tseqdact}} <strong>[[User:$1|$1]]</strong> $2",
+       "viewinguserrights": "Askan n izefan iseqdacen n {{GENDER:$1|useqdac|tseqdact}} <strong>[[User:$1|$1]]</strong> $2",
+       "userrights-editusergroup": "Snifel izerfan n {{GENDER:$1|useqdsac|tseqdact}}",
+       "userrights-viewusergroup": "Sken igrawen n {{GENDER:$1|useqdac|tseqdact}}",
+       "saveusergroups": "Sekles igrawen n {{GENDER:$1|useqdac|tseqdact}}",
+       "userrights-groupsmember": "Amaslad deg:",
+       "userrights-groupsmember-auto": "Aεeggal udrig n:",
+       "userrights-groups-help": "Tzemreḍ ad beddeleḍ igrawen anda yella aseqdac agi :\n* Taxxamt i tekkin : aseqdac yella deg ugraw agi.\n* Taxxamt ur tekkin ara : aseqdac ur yella ara deg ugraw agi\n* Titrit (*) : ur tzemreḍ ara ad ekkeseḍ agraw agi sakin i tid ernuḍ, naɣ bis-bersa.",
+       "userrights-reason": "Ayɣer:",
+       "userrights-no-interwiki": "Ur tesɛiḍ ara turagt iwakken ad beddeleḍ izerfan n iseqdacen ɣef wiki nniḍen.",
+       "userrights-nodatabase": "Taffa n isefka $1 ulac itt naɣ mačči d tadigant.",
+       "userrights-changeable-col": "Igrawen i tzemreḍ ad beddeleḍ",
+       "userrights-unchangeable-col": "Igrawen ur tzemreḍ ara ad beddeleḍ",
+       "userrights-expiry-current": "Ad ifat di $1",
+       "userrights-expiry-none": "Ur yettfat ara",
+       "userrights-expiry": "Ad ifat di:",
+       "userrights-expiry-existing": "Azemz n ufati yellan: $3,  $2",
+       "userrights-expiry-othertime": "Akud-nniḍen:",
+       "userrights-expiry-options": "1 ass:1 day,1 amalas:1 week,1 aggur:1 month,3 agguren:3 montghs,6 agguren:6 month,1 aseggas:1 year",
+       "userrights-invalid-expiry": "Azemz n tagara i ugraw \"$1\" mačči d ameɣtu.",
+       "userrights-expiry-in-past": "Azemz n tagara i ugraw \"$1\" yezri.",
+       "userrights-conflict": "Ccwal n ubeddel n izerfan n umseqdac ! Ilaq ad ɛzemeḍ tikelt nniḍen dɣa ad sergegeḍ ibeddilen.",
+       "group": "Adrum:",
+       "group-user": "Iseqdacen",
+       "group-autoconfirmed": "Iseqdacen i rgegen",
+       "group-bot": "Iṛubuten",
+       "group-sysop": "Inedbalen",
+       "group-bureaucrat": "Imsifellura",
+       "group-suppress": "Inemdayen",
+       "group-all": "(akk)",
+       "group-user-member": "{{GENDER:$1|aseqdac|taseqdact}}",
+       "group-autoconfirmed-member": "{{GENDER:$1|manrgeg aseqdac|manrgeg taseqdact}}",
+       "group-bot-member": "{{GENDER:$1|aṛubut}}",
+       "group-sysop-member": "{{GENDER:$1|anedbal|tanedbalt}}",
+       "group-bureaucrat-member": "{{GENDER:$1|amsfellaru}}",
+       "group-suppress-member": "{{GENDER:$1|anemday|tanemdayt}}",
+       "grouppage-user": "{{ns:project}}:Iseqdacen",
+       "grouppage-autoconfirmed": "{{ns:project}}:Iseqdacen i rgegen",
+       "grouppage-bot": "{{ns:project}}:Iṛubuten",
+       "grouppage-sysop": "{{ns:project}}:Inedbalen",
+       "grouppage-bureaucrat": "{{ns:project}}:Imsfelluran",
+       "grouppage-suppress": "{{ns:project}}:Suppress",
+       "right-read": "Ɣeṛ isebtar",
+       "right-edit": "Beddel isebtar",
+       "right-createpage": "Snulfud isebtar (mačči d-isebtar n umeslay)",
+       "right-createtalk": "Snulfud isebtar n umeslay",
+       "right-createaccount": "Snulfud imiḍanen n iseqdacen",
+       "right-autocreateaccount": "Tuqqna tawurmant s umiḍan n useqdac azɣaray",
+       "right-minoredit": "Ffer ibeddilen yellan d-imectuḥen",
+       "right-move": "Beddel isem n isebtar",
+       "right-move-subpages": "Beddel isem n isebtar d adu-isebtar nsen",
+       "right-move-rootuserpages": "Beddel isem n usebtar amenzawi n useqdac",
+       "right-move-categorypages": "Ugar n isebtar n taggayin",
+       "right-movefile": "Beddel isem n ifuyla",
+       "right-suppressredirect": "Ur snulfu ara asemmimeḍ seg azwel amezwaru s ubeddel n isem usebter",
+       "right-upload": "Azen ifuyla",
+       "right-reupload": "Sefxes afaylu yellan",
+       "right-reupload-own": "Sefxes afaylu id n-azen.",
+       "right-reupload-shared": "Ɛefes deg udigan afaylu yellan ɣef azadur azduklan",
+       "right-upload_by_url": "Kter afaylu seg tansa URL",
+       "enhancedrc-history": "amazray",
+       "recentchanges-submit": "Ssken",
+       "rcshowhideminor-show": "Ssken",
+       "rcshowhideminor-hide": "Ffer",
+       "rcshowhidebots": "$1 Iṛubuten",
+       "rcshowhidebots-show": "Ssken",
+       "rcshowhidebots-hide": "Ffer",
+       "rcshowhideliu-show": "Ssken",
+       "rcshowhideliu-hide": "Ffer",
+       "rcshowhideanons-show": "Ssken",
+       "rcshowhideanons-hide": "Ffer",
+       "rcshowhidepatr-show": "Ssken",
+       "rcshowhidemine-show": "Ssken",
+       "rcshowhidemine-hide": "Ffer",
+       "rcshowhidecategorization-show": "Ssken",
+       "hide": "Ffer",
+       "show": "Ssken",
+       "upload": "Azen afaylu",
+       "imgfile": "Afaylu",
+       "file-anchor-link": "Afaylu",
+       "withoutinterwiki-submit": "Ssken",
        "nbytes": "$1 {{PLURAL:$1|byte|bytes}}",
+       "prefixindex-submit": "Ssken",
+       "newpages-submit": "Ssken",
+       "newpages-username": "Isem n useqdac:",
+       "logeventslist-submit": "Ssken",
+       "categories-submit": "Ssken",
        "linksearch-ok": "Iruzzi",
+       "listusers-submit": "Ssken",
+       "emailusername": "Isem n useqdac:",
+       "watchlist-submit": "Ssken",
+       "historyaction-submit": "Ssken",
+       "restriction-edit": "Glef",
        "undelete-search-submit": "Iruzzi",
+       "namespace": "Talluntin n isemawen",
        "sp-contributions-submit": "Iruzzi",
        "ipblocklist-submit": "Iruzzi",
+       "tooltip-pt-logout": "Ffeɣ",
        "tooltip-search": "Iruzzi {{SITENAME}}",
+       "tooltip-t-upload": "Azen ifuyla",
+       "tooltip-ca-nstab-category": "Ẓer asebter n taggayin",
+       "pageinfo-toolboxlink": "Tilɣa n udasil",
+       "pageinfo-contentpage-yes": "Ih",
        "show-big-image-size": "$1 × $2 pixels",
        "ilsubmit": "Iruzzi",
        "days": "{{PLURAL:$1|$1 ass|$1 ussan}}",
+       "namespacesall": "akk",
+       "monthsall": "akk",
        "fileduplicatesearch-submit": "Iruzzi",
-       "searchsuggest-search": "Iruzzi",
+       "specialpages": "Asebter uslig",
+       "tags-active-yes": "Ih",
+       "tags-active-no": "Uhu",
+       "searchsuggest-search": "Iruzzi {{SITENAME}}",
        "duration-days": "$1 {{PLURAL:$1|ass|ussan}}"
 }
index 3b53fb4..0ce9aa3 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Korean (한국어(조선))
+/** Korean (Democratic People's Republic of Korea) (조선말)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index 7b779a6..e9048ec 100644 (file)
@@ -9,7 +9,6 @@
     "selenium-test": "wdio ./tests/selenium/wdio.conf.js"
   },
   "devDependencies": {
-    "bluebird": "3.5.1",
     "deepmerge": "1.3.2",
     "eslint": "4.9.0",
     "eslint-config-wikimedia": "0.5.0",
     "karma-firefox-launcher": "1.0.1",
     "karma-mocha-reporter": "2.2.5",
     "karma-qunit": "2.0.1",
-    "mwbot": "1.0.10",
     "postcss-less": "1.1.5",
     "qunit": "2.5.0",
     "stylelint": "9.2.0",
     "stylelint-config-wikimedia": "0.4.3",
     "wdio-junit-reporter": "0.2.0",
+    "wdio-mediawiki": "file:tests/selenium/wdio-mediawiki",
     "wdio-mocha-framework": "0.5.8",
     "wdio-sauce-service": "0.3.1",
     "wdio-spec-reporter": "0.0.5",
index 160aeb3..e8a5e24 100644 (file)
@@ -2167,7 +2167,7 @@ return [
                'scripts' => [
                        'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
                ],
@@ -2184,6 +2184,35 @@ return [
                ],
        ],
        'mediawiki.special.preferences.styles' => [
+               'targets' => [ 'desktop', 'mobile' ],
+               'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css',
+       ],
+       'mediawiki.special.preferences.ooui' => [
+               'targets' => [ 'desktop', 'mobile' ],
+               'scripts' => [
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+               ],
+               'messages' => [
+                       'prefs-tabs-navigation-hint',
+                       'prefswarning-warning',
+                       'saveprefs',
+                       'savedprefs',
+               ],
+               'dependencies' => [
+                       'mediawiki.language',
+                       'mediawiki.confirmCloseWindow',
+                       'mediawiki.notification.convertmessagebox',
+                       'oojs-ui-widgets',
+                       'mediawiki.widgets.SelectWithInputWidget',
+                       'mediawiki.editfont.styles',
+               ],
+       ],
+       'mediawiki.special.preferences.styles.ooui' => [
                'targets' => [ 'desktop', 'mobile' ],
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
        ],
index 503e3a6..e355196 100644 (file)
         *   expand the element. Default: the 'data-expandtext' attribute of the
         *   collapsible element or the content of 'collapsible-expand' message.
         * @param {boolean} [options.collapsed] Whether to collapse immediately. By default
-        *   collapse only if the elements has the 'mw-collapsible' class.
+        *   collapse only if the element has the 'mw-collapsed' class.
         * @param {jQuery} [options.$customTogglers] Elements to be used as togglers
         *   for this collapsible element. By default, if the collapsible element
         *   has an id attribute like 'mw-customcollapsible-XXX', elements with a
index 2572b52..b95a436 100644 (file)
@@ -215,6 +215,7 @@ table.toc td {
 }
 
 /* preference page with js-genrated toc */
+/* TODO: Delete #preftoc when Special:Preference's non-OOUI mode is disabled */
 #preftoc {
        float: left;
        margin: 1em 1em 1em 1em;
index 1476241..244154b 100644 (file)
@@ -4,9 +4,12 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var allowCloseWindow;
+               var allowCloseWindow, saveButton, restoreButton,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
 
-               // Check if all of the form values are unchanged
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
                function isPrefsChanged() {
                        var inputs = $( '#mw-prefs-form :input[name]' ),
                                input, $input, inputType,
                        return false;
                }
 
-               // Disable the button to save preferences unless preferences have changed
-               // Check if preferences have been changed before JS has finished loading
-               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-               $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
+               if ( oouiEnabled ) {
+                       saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+                       restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
+                       saveButton.setDisabled( !isPrefsChanged() );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
+                               saveButton.setDisabled( !isPrefsChanged() );
+                       } );
+               } else {
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
                        $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-               } );
+                       $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
+                               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+                       } );
+               }
 
                // Set up a message to notify users if they try to leave the page without
                // saving.
                        message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
                        namespace: 'prefswarning'
                } );
-               $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
-               $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
+               $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
+               if ( oouiEnabled ) {
+                       restoreButton.on( 'click', function () {
+                               allowCloseWindow.release();
+                               // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                               // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                               location.href = restoreButton.getHref();
+                       } );
+               } else {
+                       $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
+               }
        } );
 }( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js
new file mode 100644 (file)
index 0000000..fe48886
--- /dev/null
@@ -0,0 +1,32 @@
+/*!
+ * JavaScript for Special:Preferences: editfont field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var widget, lastValue;
+
+               try {
+                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       return;
+               }
+
+               // Style options
+               widget.dropdownWidget.menu.items.forEach( function ( item ) {
+                       item.$label.addClass( 'mw-editfont-' + item.getData() );
+               } );
+
+               function updateLabel( value ) {
+                       // Style selected item label
+                       widget.dropdownWidget.$label
+                               .removeClass( 'mw-editfont-' + lastValue )
+                               .addClass( 'mw-editfont-' + value );
+                       lastValue = value;
+               }
+
+               widget.on( 'change', updateLabel );
+               updateLabel( widget.getValue() );
+
+       } );
+}( mediaWiki, jQuery ) );
index 33b630a..2310377 100644 (file)
@@ -1,27 +1,29 @@
 /* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
        border: 1px solid #fde29b;
        background-color: #fdf1d1;
        color: #000;
+       padding: 0.5em;
 }
 /* Authenticated email field has its own class too. Unstyled by default */
 /*
-.mw-email-authenticated .mw-input { }
+.mw-email-authenticated .oo-ui-labelWidget { }
 */
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
 
-#preferences > fieldset table {
-       width: 100%;
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
 }
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
+
+.mw-prefs-buttons {
+       margin-top: 1em;
 }
 
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+#prefcontrol {
+       margin-right: 0.5em;
+}
 
 /*
  * Hide, but keep accessible for screen-readers.
        zoom: 1;
 }
 
-.client-nojs #preftoc {
-       display: none;
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       border-color: #c8ccd1;
+       border-width: 1px 0 0;
+       border-radius: 0;
+       padding-left: 0;
+       padding-right: 0;
+       box-shadow: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
 }
 
-.client-js #preferences > fieldset {
-       display: none;
+.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
+       margin-bottom: 0;
+       border: 0;
+       padding-top: 0;
 }
 
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
+}
+
+/* Make the "Basic information" section more compact */
+/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
+       width: 20%;
+       display: inline-block;
+       vertical-align: middle;
+       padding: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
+       margin-right: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+       width: 80%;
+       display: inline-block;
+       vertical-align: middle;
+}
+
+/* Expand the dropdown and textfield of "Time zone" field to the */
+/* usual maximum width and display them on separate lines. */
+#wpTimeCorrection .oo-ui-dropdownInputWidget,
+#wpTimeCorrection .oo-ui-textInputWidget {
        display: block;
+       max-width: 50em;
+}
+
+#wpTimeCorrection .oo-ui-textInputWidget {
+       margin-top: 0.5em;
 }
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css
new file mode 100644 (file)
index 0000000..33b630a
--- /dev/null
@@ -0,0 +1,47 @@
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .mw-input,
+.mw-email-none .mw-input {
+       border: 1px solid #fde29b;
+       background-color: #fdf1d1;
+       color: #000;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .mw-input { }
+*/
+/* This breaks due to nolabel styling */
+#preferences > fieldset td.mw-label {
+       width: 20%;
+}
+
+#preferences > fieldset table {
+       width: 100%;
+}
+#preferences > fieldset table.mw-htmlform-matrix {
+       width: auto;
+}
+
+/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+       overflow: hidden;
+       height: 0;
+       zoom: 1;
+}
+
+.client-nojs #preftoc {
+       display: none;
+}
+
+.client-js #preferences > fieldset {
+       display: none;
+}
+
+/* Only the 1st tab is shown by default in JS mode */
+.client-js #preferences #mw-prefsection-personal {
+       display: block;
+}
index 0d97d68..c948ff0 100644 (file)
@@ -3,29 +3,10 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+               var $preferences, tabs, wrapper, previousTab;
 
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
                $preferences = $( '#preferences' );
 
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
                // Make sure the accessibility tip is selectable so that screen reader users take notice,
                // but hide it per default to reduce interface clutter. Also make sure it becomes visible
                // when selected. Similar to jquery.mw-jump
                                } else {
                                        $( this ).css( 'height', 'auto' );
                                }
-                       } ).insertBefore( $preftoc );
+                       } ).prependTo( '#mw-content-text' );
 
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
                        // Handle hash manually to prevent jumping,
                        // therefore save and restore scrollTop to prevent jumping.
                        scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
                        }
                        $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
                }
 
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
                        }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
                        }
-               } );
+               }
 
                // Jump to correct section as indicated by the hash.
                // This function is called onload and onhashchange.
                }
 
                $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       var value = tabs.getCurrentTabPanelName();
                        mw.storage.session.set( 'mwpreferences-prevTab', value );
                } );
 
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js
new file mode 100644 (file)
index 0000000..0d97d68
--- /dev/null
@@ -0,0 +1,143 @@
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+
+               labelFunc = function () {
+                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
+               };
+
+               $preftoc = $( '#preftoc' );
+               $preferences = $( '#preferences' );
+
+               $fieldsets = $preferences.children( 'fieldset' )
+                       .attr( {
+                               role: 'tabpanel',
+                               'aria-labelledby': labelFunc
+                       } );
+               $fieldsets.not( '#mw-prefsection-personal' )
+                       .hide()
+                       .attr( 'aria-hidden', 'true' );
+
+               // T115692: The following is kept for backwards compatibility with older skins
+               $preferences.addClass( 'jsprefs' );
+               $fieldsets.addClass( 'prefsection' );
+               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
+
+               // Make sure the accessibility tip is selectable so that screen reader users take notice,
+               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+               // when selected. Similar to jquery.mw-jump
+               $( '<div>' ).addClass( 'mw-navigation-hint' )
+                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+                       .attr( 'tabIndex', 0 )
+                       .on( 'focus blur', function ( e ) {
+                               if ( e.type === 'blur' || e.type === 'focusout' ) {
+                                       $( this ).css( 'height', '0' );
+                               } else {
+                                       $( this ).css( 'height', 'auto' );
+                               }
+                       } ).insertBefore( $preftoc );
+
+               /**
+                * It uses document.getElementById for security reasons (HTML injections in $()).
+                *
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to surpress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       var $tab, scrollTop;
+                       // Handle hash manually to prevent jumping,
+                       // therefore save and restore scrollTop to prevent jumping.
+                       scrollTop = $( window ).scrollTop();
+                       if ( mode !== 'noHash' ) {
+                               location.hash = '#mw-prefsection-' + name;
+                       }
+                       $( window ).scrollTop( scrollTop );
+
+                       $preftoc.find( 'li' ).removeClass( 'selected' )
+                               .find( 'a' ).attr( {
+                                       tabIndex: -1,
+                                       'aria-selected': 'false'
+                               } );
+
+                       $tab = $( document.getElementById( 'preftab-' + name ) );
+                       if ( $tab.length ) {
+                               $tab.attr( {
+                                       tabIndex: 0,
+                                       'aria-selected': 'true'
+                               } ).focus()
+                                       .parent().addClass( 'selected' );
+
+                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
+                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
+                       }
+               }
+
+               // Enable keyboard users to use left and right keys to switch tabs
+               $preftoc.on( 'keydown', function ( event ) {
+                       var keyLeft = 37,
+                               keyRight = 39,
+                               $el;
+
+                       if ( event.keyCode === keyLeft ) {
+                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
+                       } else if ( event.keyCode === keyRight ) {
+                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
+                       } else {
+                               return;
+                       }
+                       if ( $el.length > 0 ) {
+                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       }
+               } );
+
+               // Jump to correct section as indicated by the hash.
+               // This function is called onload and onhashchange.
+               function detectHash() {
+                       var hash = location.hash,
+                               matchedElement, parentSection;
+                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+                               mw.storage.session.remove( 'mwpreferences-prevTab' );
+                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+                               matchedElement = document.getElementById( hash.slice( 1 ) );
+                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+                               if ( parentSection.length ) {
+                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+                                       // Switch to proper tab and scroll to selected item.
+                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+                                       matchedElement.scrollIntoView();
+                               }
+                       }
+               }
+
+               $( window ).on( 'hashchange', function () {
+                       var hash = location.hash;
+                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
+                               detectHash();
+                       } else if ( hash === '' ) {
+                               switchPrefTab( 'personal', 'noHash' );
+                       }
+               } )
+                       // Run the function immediately to select the proper tab on startup.
+                       .trigger( 'hashchange' );
+
+               // Restore the active tab after saving the preferences
+               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+               if ( previousTab ) {
+                       switchPrefTab( previousTab, 'noHash' );
+                       // Deleting the key, the tab states should be reset until we press Save
+                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+               }
+
+               $( '#mw-prefs-form' ).on( 'submit', function () {
+                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       mw.storage.session.set( 'mwpreferences-prevTab', value );
+               } );
+
+       } );
+}( mediaWiki, jQuery ) );
index 03656ee..a6ffae9 100644 (file)
@@ -3,14 +3,25 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var
-                       $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+               var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
 
                // Timezone functions.
                // Guesses Timezone from browser and updates fields onchange.
 
-               $tzSelect = $( '#mw-input-wptimecorrection' );
-               $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               if ( oouiEnabled ) {
+                       // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+                       try {
+                               timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+                       } catch ( err ) {
+                               // This preference could theoretically be disabled ($wgHiddenPrefs)
+                               timezoneWidget = null;
+                       }
+               } else {
+                       $tzSelect = $( '#mw-input-wptimecorrection' );
+                       $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               }
+
                $localtimeHolder = $( '#wpLocalTime' );
                servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
 
 
                function updateTimezoneSelection() {
                        var minuteDiff, localTime,
-                               type = $tzSelect.val();
+                               type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
+                               val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
 
                        if ( type === 'other' ) {
                                // User specified time zone manually in <input>
                                // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( $tzTextbox.val() );
+                               minuteDiff = hoursToMinutes( val );
                        } else {
                                // Time zone not manually specified by user
                                if ( type === 'guess' ) {
                                        // Get browser timezone & fill it in
                                        minuteDiff = -( new Date().getTimezoneOffset() );
-                                       $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                       $tzSelect.val( 'other' );
+                                       if ( oouiEnabled ) {
+                                               timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                               timezoneWidget.dropdowninput.setValue( 'other' );
+                                       } else {
+                                               $tzTextbox.val( minutesToHours( minuteDiff ) );
+                                               $tzSelect.val( 'other' );
+                                       }
                                } else {
-                                       // Grab data from the $tzSelect value
+                                       // Grab data from the dropdown value
                                        minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
                                }
                        }
                        $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
                }
 
-               if ( $tzSelect.length && $tzTextbox.length ) {
-                       $tzSelect.change( updateTimezoneSelection );
-                       $tzTextbox.blur( updateTimezoneSelection );
-                       updateTimezoneSelection();
+               if ( oouiEnabled ) {
+                       if ( timezoneWidget ) {
+                               timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                               timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
+               } else {
+                       if ( $tzSelect.length && $tzTextbox.length ) {
+                               $tzSelect.change( updateTimezoneSelection );
+                               $tzTextbox.blur( updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
                }
 
        } );
index a8c82b1..b1e1da3 100644 (file)
                         *
                         * @private
                         * @param {string} src URL to script, will be used as the src attribute in the script tag
-                        * @return {jQuery.Promise}
+                        * @param {Function} [callback] Callback to run after request resolution
                         */
-                       function addScript( src ) {
-                               return $.ajax( {
-                                       url: src,
-                                       dataType: 'script',
-                                       // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
-                                       // XHR for a same domain request instead of <script>, which changes the request
-                                       // headers (potentially missing a cache hit), and reduces caching in general
-                                       // since browsers cache XHR much less (if at all). And XHR means we retrieve
-                                       // text, so we'd need to $.globalEval, which then messes up line numbers.
-                                       crossDomain: true,
-                                       cache: true
-                               } );
+                       function addScript( src, callback ) {
+                               var script = document.createElement( 'script' );
+                               script.src = src;
+                               script.onload = script.onerror = function () {
+                                       if ( script.parentNode ) {
+                                               script.parentNode.removeChild( script );
+                                       }
+                                       script = null;
+                                       if ( callback ) {
+                                               callback();
+                                               callback = null;
+                                       }
+                               };
+                               document.head.appendChild( script );
                        }
 
                        /**
                         *
                         * @private
                         * @param {string} src URL of the script
-                        * @param {string} [moduleName] Name of currently executing module
-                        * @return {jQuery.Promise}
+                        * @param {string} moduleName Name of currently executing module
+                        * @param {Function} callback Callback to run after addScript() resolution
                         */
-                       function queueModuleScript( src, moduleName ) {
-                               var r = $.Deferred();
-
+                       function queueModuleScript( src, moduleName, callback ) {
                                pendingRequests.push( function () {
-                                       if ( moduleName && hasOwn.call( registry, moduleName ) ) {
+                                       if ( hasOwn.call( registry, moduleName ) ) {
                                                // Emulate runScript() part of execute()
                                                window.require = mw.loader.require;
                                                window.module = registry[ moduleName ].module;
                                        }
-                                       addScript( src ).always( function () {
+                                       addScript( src, function () {
                                                // 'module.exports' should not persist after the file is executed to
                                                // avoid leakage to unrelated code. 'require' should be kept, however,
                                                // as asynchronous access to 'require' is allowed and expected. (T144879)
                                                delete window.module;
-                                               r.resolve();
-
+                                               callback();
                                                // Start the next one (if any)
                                                if ( pendingRequests[ 0 ] ) {
                                                        pendingRequests.shift()();
                                        handlingPendingRequests = true;
                                        pendingRequests.shift()();
                                }
-                               return r.promise();
                        }
 
                        /**
                                                        return;
                                                }
 
-                                               queueModuleScript( arr[ i ], module ).always( function () {
+                                               queueModuleScript( arr[ i ], module, function () {
                                                        nestedAddScript( arr, callback, i + 1 );
                                                } );
                                        };
index 0416bcf..f19be3b 100644 (file)
@@ -2,8 +2,10 @@
 
 namespace MediaWiki\Tests\Storage;
 
+use InvalidArgumentException;
 use MediaWiki\Storage\MutableRevisionSlots;
 use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\RevisionSlots;
 use MediaWiki\Storage\SlotRecord;
 use WikitextContent;
 
@@ -12,6 +14,33 @@ use WikitextContent;
  */
 class MutableRevisionSlotsTest extends RevisionSlotsTest {
 
+       /**
+        * @param SlotRecord[] $slots
+        * @return RevisionSlots
+        */
+       protected function newRevisionSlots( $slots = [] ) {
+               return new MutableRevisionSlots( $slots );
+       }
+
+       public function provideConstructorFailue() {
+               yield 'array or the wrong thing' => [
+                       [ 1, 2, 3 ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailue
+        * @param $slots
+        *
+        * @covers \MediaWiki\Storage\RevisionSlots::__construct
+        * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
+        */
+       public function testConstructorFailue( $slots ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new MutableRevisionSlots( $slots );
+       }
+
        public function testSetMultipleSlots() {
                $slots = new MutableRevisionSlots();
 
index b9f833c..95bba47 100644 (file)
@@ -2,10 +2,12 @@
 
 namespace MediaWiki\Tests\Storage;
 
+use InvalidArgumentException;
 use MediaWiki\Storage\RevisionAccessException;
 use MediaWiki\Storage\RevisionSlots;
 use MediaWiki\Storage\SlotRecord;
 use MediaWikiTestCase;
+use TextContent;
 use WikitextContent;
 
 class RevisionSlotsTest extends MediaWikiTestCase {
@@ -18,6 +20,28 @@ class RevisionSlotsTest extends MediaWikiTestCase {
                return new RevisionSlots( $slots );
        }
 
+       public function provideConstructorFailue() {
+               yield 'not an array or callable' => [
+                       'foo'
+               ];
+               yield 'array of the wrong thing' => [
+                       [ 1, 2, 3 ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailue
+        * @param $slots
+        *
+        * @covers \MediaWiki\Storage\RevisionSlots::__construct
+        * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
+        */
+       public function testConstructorFailue( $slots ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new RevisionSlots( $slots );
+       }
+
        /**
         * @covers \MediaWiki\Storage\RevisionSlots::getSlot
         */
@@ -94,6 +118,40 @@ class RevisionSlotsTest extends MediaWikiTestCase {
                $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
        }
 
+       /**
+        * @covers \MediaWiki\Storage\RevisionSlots::getInheritedSlots
+        */
+       public function testGetInheritedSlots() {
+               $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newInherited(
+                       SlotRecord::newSaved(
+                               7, 7, 'foo',
+                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+                       )
+               );
+               $slotsArray = [ $mainSlot, $auxSlot ];
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\RevisionSlots::getTouchedSlots
+        */
+       public function testGetTouchedSlots() {
+               $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newInherited(
+                       SlotRecord::newSaved(
+                               7, 7, 'foo',
+                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+                       )
+               );
+               $slotsArray = [ $mainSlot, $auxSlot ];
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertEquals( [ 'main' => $mainSlot ], $slots->getTouchedSlots() );
+       }
+
        public function provideComputeSize() {
                yield [ 1, [ 'A' ] ];
                yield [ 2, [ 'AA' ] ];
@@ -136,4 +194,34 @@ class RevisionSlotsTest extends MediaWikiTestCase {
                $this->assertSame( $expected, $slots->computeSha1() );
        }
 
+       public function provideHasSameContent() {
+               $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
+               $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
+               $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
+               $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
+               $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
+
+               $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
+               $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
+               $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
+
+               yield 'same instance' => [ $a, $a, true ];
+               yield 'same slots' => [ $a, $a2, true ];
+               yield 'same content' => [ $a, $a3, true ];
+
+               yield 'different roles' => [ $a, $b, false ];
+               yield 'different content' => [ $a, $c, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        * @covers \MediaWiki\Storage\RevisionSlots::hasSameContent
+        */
+       public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) {
+               $this->assertSame( $same, $a->hasSameContent( $b ) );
+               $this->assertSame( $same, $b->hasSameContent( $a ) );
+       }
+
 }
diff --git a/tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php b/tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php
new file mode 100644 (file)
index 0000000..5b392c8
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use MediaWiki\Storage\RevisionSlots;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\SlotRecord;
+use MediaWikiTestCase;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Storage\RevisionSlotsUpdate
+ */
+class RevisionSlotsUpdateTest extends MediaWikiTestCase {
+
+       public function provideNewFromRevisionSlots() {
+               $slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
+               $slotB = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B' ) );
+               $slotC = SlotRecord::newUnsaved( 'C', new WikitextContent( 'C' ) );
+
+               $slotB2 = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B2' ) );
+
+               $parentSlots = new RevisionSlots( [
+                       'A' => $slotA,
+                       'B' => $slotB,
+                       'C' => $slotC,
+               ] );
+
+               $newSlots = new RevisionSlots( [
+                       'A' => $slotA,
+                       'B' => $slotB2,
+               ] );
+
+               yield [ $newSlots, null, [ 'A', 'B' ], [] ];
+               yield [ $newSlots, $parentSlots, [ 'B' ], [ 'C' ] ];
+       }
+
+       /**
+        * @dataProvider provideNewFromRevisionSlots
+        *
+        * @param RevisionSlots $newSlots
+        * @param RevisionSlots $parentSlots
+        * @param $modified
+        * @param $removed
+        */
+       public function testNewFromRevisionSlots(
+               RevisionSlots $newSlots,
+               RevisionSlots $parentSlots = null,
+               array $modified = [],
+               array $removed = []
+       ) {
+               $update = RevisionSlotsUpdate::newFromRevisionSlots( $newSlots, $parentSlots );
+
+               $this->assertEquals( $modified, $update->getModifiedRoles() );
+               $this->assertEquals( $removed, $update->getRemovedRoles() );
+
+               foreach ( $modified as $role ) {
+                       $this->assertSame( $newSlots->getSlot( $role ), $update->getModifiedSlot( $role ) );
+               }
+       }
+
+       public function testConstructor() {
+               $update = new RevisionSlotsUpdate();
+
+               $this->assertEmpty( $update->getModifiedRoles() );
+               $this->assertEmpty( $update->getRemovedRoles() );
+
+               $slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
+               $update = new RevisionSlotsUpdate( [ 'A' => $slotA ] );
+
+               $this->assertEquals( [ 'A' ], $update->getModifiedRoles() );
+               $this->assertEmpty( $update->getRemovedRoles() );
+
+               $update = new RevisionSlotsUpdate( [ 'A' => $slotA ], [ 'X' ] );
+
+               $this->assertEquals( [ 'A' ], $update->getModifiedRoles() );
+               $this->assertEquals( [ 'X' ], $update->getRemovedRoles() );
+       }
+
+       public function testModifySlot() {
+               $slots = new RevisionSlotsUpdate();
+
+               $this->assertSame( [], $slots->getModifiedRoles() );
+               $this->assertSame( [], $slots->getRemovedRoles() );
+
+               $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
+               $slots->modifySlot( $slotA );
+               $this->assertTrue( $slots->isModifiedSlot( 'some' ) );
+               $this->assertFalse( $slots->isRemovedSlot( 'some' ) );
+               $this->assertSame( $slotA, $slots->getModifiedSlot( 'some' ) );
+               $this->assertSame( [ 'some' ], $slots->getModifiedRoles() );
+               $this->assertSame( [], $slots->getRemovedRoles() );
+
+               $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
+               $slots->modifySlot( $slotB );
+               $this->assertTrue( $slots->isModifiedSlot( 'other' ) );
+               $this->assertFalse( $slots->isRemovedSlot( 'other' ) );
+               $this->assertSame( $slotB, $slots->getModifiedSlot( 'other' ) );
+               $this->assertSame( [ 'some', 'other' ], $slots->getModifiedRoles() );
+               $this->assertSame( [], $slots->getRemovedRoles() );
+
+               // modify slot A again
+               $slots->modifySlot( $slotA );
+               $this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() );
+
+               // remove modified slot
+               $slots->removeSlot( 'some' );
+               $this->assertSame( [ 'other' ], $slots->getModifiedRoles() );
+               $this->assertSame( [ 'some' ], $slots->getRemovedRoles() );
+
+               // modify removed slot
+               $slots->modifySlot( $slotA );
+               $this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() );
+               $this->assertSame( [], $slots->getRemovedRoles() );
+       }
+
+       public function testRemoveSlot() {
+               $slots = new RevisionSlotsUpdate();
+
+               $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
+               $slots->modifySlot( $slotA );
+
+               $this->assertSame( [ 'main' ], $slots->getModifiedRoles() );
+
+               $slots->removeSlot( 'main' );
+               $slots->removeSlot( 'other' );
+               $this->assertSame( [], $slots->getModifiedRoles() );
+               $this->assertSame( [ 'main', 'other' ], $slots->getRemovedRoles() );
+               $this->assertTrue( $slots->isRemovedSlot( 'main' ) );
+               $this->assertTrue( $slots->isRemovedSlot( 'other' ) );
+               $this->assertFalse( $slots->isModifiedSlot( 'main' ) );
+
+               // removing the same slot again should not trigger an error
+               $slots->removeSlot( 'main' );
+
+               // getting a slot after removing it should fail
+               $this->setExpectedException( RevisionAccessException::class );
+               $slots->getModifiedSlot( 'main' );
+       }
+
+       public function testGetModifiedRoles() {
+               $slots = new RevisionSlotsUpdate( [], [ 'xyz' ] );
+
+               $this->assertSame( [], $slots->getModifiedRoles() );
+
+               $slots->modifyContent( 'main', new WikitextContent( 'A' ) );
+               $slots->modifyContent( 'foo', new WikitextContent( 'Foo' ) );
+               $this->assertSame( [ 'main', 'foo' ], $slots->getModifiedRoles() );
+
+               $slots->removeSlot( 'main' );
+               $this->assertSame( [ 'foo' ], $slots->getModifiedRoles() );
+       }
+
+       public function testGetRemovedRoles() {
+               $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
+               $slots = new RevisionSlotsUpdate( [ $slotA ] );
+
+               $this->assertSame( [], $slots->getRemovedRoles() );
+
+               $slots->removeSlot( 'main', new WikitextContent( 'A' ) );
+               $slots->removeSlot( 'foo', new WikitextContent( 'Foo' ) );
+
+               $this->assertSame( [ 'main', 'foo' ], $slots->getRemovedRoles() );
+
+               $slots->modifyContent( 'main', new WikitextContent( 'A' ) );
+               $this->assertSame( [ 'foo' ], $slots->getRemovedRoles() );
+       }
+
+       public function provideHasSameUpdates() {
+               $fooX = SlotRecord::newUnsaved( 'x', new WikitextContent( 'Foo' ) );
+               $barZ = SlotRecord::newUnsaved( 'z', new WikitextContent( 'Bar' ) );
+
+               $a = new RevisionSlotsUpdate();
+               $a->modifySlot( $fooX );
+               $a->modifySlot( $barZ );
+               $a->removeSlot( 'Q' );
+
+               $a2 = new RevisionSlotsUpdate();
+               $a2->modifySlot( $fooX );
+               $a2->modifySlot( $barZ );
+               $a2->removeSlot( 'Q' );
+
+               $b = new RevisionSlotsUpdate();
+               $b->modifySlot( $barZ );
+               $b->removeSlot( 'Q' );
+
+               $c = new RevisionSlotsUpdate();
+               $c->modifySlot( $fooX );
+               $c->modifySlot( $barZ );
+
+               yield 'same instance' => [ $a, $a, true ];
+               yield 'same udpates' => [ $a, $a2, true ];
+
+               yield 'different modified' => [ $a, $b, false ];
+               yield 'different removed' => [ $a, $c, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameUpdates
+        */
+       public function testHasSameUpdates( RevisionSlotsUpdate $a, RevisionSlotsUpdate $b, $same ) {
+               $this->assertSame( $same, $a->hasSameUpdates( $b ) );
+               $this->assertSame( $same, $b->hasSameUpdates( $a ) );
+       }
+
+}
index 8f26494..feeb538 100644 (file)
@@ -295,4 +295,104 @@ class SlotRecordTest extends MediaWikiTestCase {
                SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
        }
 
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
 }
index f45bb93..62ab44c 100644 (file)
@@ -117,7 +117,7 @@ class DefaultPreferencesFactoryTest extends MediaWikiTestCase {
                $configMock = new HashConfig( [
                        'HiddenPrefs' => []
                ] );
-               $form = $this->getMockBuilder( PreferencesForm::class )
+               $form = $this->getMockBuilder( PreferencesFormLegacy::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
diff --git a/tests/phpunit/includes/tidy/BalancerTest.php b/tests/phpunit/includes/tidy/BalancerTest.php
deleted file mode 100644 (file)
index 8a4f662..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-<?php
-
-class BalancerTest extends MediaWikiTestCase {
-
-       /**
-        * Anything that needs to happen before your tests should go here.
-        */
-       protected function setUp() {
-               // Be sure to do call the parent setup and teardown functions.
-               // This makes sure that all the various cleanup and restorations
-               // happen as they should (including the restoration for setMwGlobals).
-               parent::setUp();
-       }
-
-       /**
-        * @covers MediaWiki\Tidy\Balancer
-        * @covers MediaWiki\Tidy\BalanceSets
-        * @covers MediaWiki\Tidy\BalanceElement
-        * @covers MediaWiki\Tidy\BalanceStack
-        * @covers MediaWiki\Tidy\BalanceMarker
-        * @covers MediaWiki\Tidy\BalanceActiveFormattingElements
-        * @dataProvider provideBalancerTests
-        */
-       public function testBalancer( $description, $input, $expected, $useTidy ) {
-               $balancer = new MediaWiki\Tidy\Balancer( [
-                       'strict' => false, /* not strict */
-                       'allowedHtmlElements' => null, /* no sanitization */
-                       'tidyCompat' => $useTidy, /* standard parser */
-                       'allowComments' => true, /* comment parsing */
-               ] );
-               $output = $balancer->balance( $input );
-
-               // Ignore self-closing tags
-               $output = preg_replace( '/\s*\/>/', '>', $output );
-
-               $this->assertEquals( $expected, $output, $description );
-       }
-
-       public static function provideBalancerTests() {
-               // Get the tests from html5lib-tests.json
-               $json = json_decode( file_get_contents(
-                       __DIR__ . '/html5lib-tests.json'
-               ), true );
-               // Munge this slightly into the format phpunit expects
-               // for providers, and filter out HTML constructs which
-               // the balancer doesn't support.
-               $tests = [];
-               $okre = "~ \A
-                       (?i:<!DOCTYPE\ html>)?
-                       <html><head></head><body>
-                       .*
-                       </body></html>
-               \z ~xs";
-               foreach ( $json as $filename => $cases ) {
-                       foreach ( $cases as $case ) {
-                               $html = $case['document']['html'];
-                               if ( !preg_match( $okre, $html ) ) {
-                                       // Skip tests which involve stuff in the <head> or
-                                       // weird doctypes.
-                                       continue;
-                               }
-                               // We used to do this:
-                               //   $html = substr( $html, strlen( $start ), -strlen( $end ) );
-                               // But now we use a different field in the test case,
-                               // which reports how domino would parse this case in a
-                               // no-quirks <body> context.  (The original test case may
-                               // have had a different context, or relied on quirks mode.)
-                               $html = $case['document']['noQuirksBodyHtml'];
-                               // Normalize case of SVG attributes.
-                               $html = str_replace( 'foreignObject', 'foreignobject', $html );
-                               // Normalize case of MathML attributes.
-                               $html = str_replace( 'definitionURL', 'definitionurl', $html );
-
-                               if (
-                                       isset( $case['document']['props']['comment'] ) &&
-                                       preg_match( ',<!--[^>]*<,', $html )
-                               ) {
-                                       // Skip tests which include HTML comments containing
-                                       // the < character, which we don't support.
-                                       continue;
-                               }
-                               if ( strpos( $case['data'], '<![CDATA[' ) !== false ) {
-                                       // Skip tests involving <![CDATA[ ]]> quoting.
-                                       continue;
-                               }
-                               if (
-                                       stripos( $case['data'], '<!DOCTYPE' ) !== false &&
-                                       stripos( $case['data'], '<!DOCTYPE html>' ) === false
-                               ) {
-                                       // Skip tests involving unusual doctypes.
-                                       continue;
-                               }
-                               $literalre = "~ <rdar: | < /? (
-                                       html | head | body | frame | frameset | plaintext
-                               ) > ~xi";
-                               if ( preg_match( $literalre, $case['data'] ) ) {
-                                       // Skip tests involving some literal tags, which are
-                                       // unsupported but don't show up in the expected output.
-                                       continue;
-                               }
-                               if (
-                                       isset( $case['document']['props']['tags']['iframe'] ) ||
-                                       isset( $case['document']['props']['tags']['noembed'] ) ||
-                                       isset( $case['document']['props']['tags']['noscript'] ) ||
-                                       isset( $case['document']['props']['tags']['script'] ) ||
-                                       isset( $case['document']['props']['tags']['svg script'] ) ||
-                                       isset( $case['document']['props']['tags']['svg title'] ) ||
-                                       isset( $case['document']['props']['tags']['title'] ) ||
-                                       isset( $case['document']['props']['tags']['xmp'] )
-                               ) {
-                                       // Skip tests with unsupported tags which *do* show
-                                       // up in the expected output.
-                                       continue;
-                               }
-                               if (
-                                       $filename === 'entities01.dat' ||
-                                       $filename === 'entities02.dat' ||
-                                       preg_match( '/&([a-z]+|#x[0-9A-F]+);/i', $case['data'] ) ||
-                                       preg_match( '/^(&|&#|&#X|&#x|&#45|&x-test|&AMP)$/', $case['data'] )
-                               ) {
-                                       // Skip tests involving entity encoding.
-                                       continue;
-                               }
-                               if (
-                                       isset( $case['document']['props']['tagWithLt'] ) ||
-                                       isset( $case['document']['props']['attrWithFunnyChar'] ) ||
-                                       preg_match( ':^(</b test|<di|<foo bar=qux/>)$:', $case['data'] ) ||
-                                       preg_match( ':</p<p>:', $case['data'] ) ||
-                                       preg_match( ':<b &=&amp>|<p/x/y/z>:', $case['data'] )
-                               ) {
-                                       // Skip tests with funny tag or attribute names,
-                                       // which are really tests of the HTML tokenizer, not
-                                       // the tree builder.
-                                       continue;
-                               }
-                               if (
-                                       preg_match( ':encoding=" text/html "|type=" hidden":', $case['data'] )
-                               ) {
-                                       // The Sanitizer normalizes whitespace in attribute
-                                       // values, which makes this test case invalid.
-                                       continue;
-                               }
-                               if ( $filename === 'plain-text-unsafe.dat' ) {
-                                       // Skip tests with ASCII null, etc.
-                                       continue;
-                               }
-                               $data = preg_replace(
-                                       '~<!DOCTYPE html>~i', '', $case['data']
-                               );
-                               $tests[] = [
-                                       $filename, # use better description?
-                                       $data,
-                                       $html,
-                                       false # strict HTML5 compat mode, no tidy
-                               ];
-                       }
-               }
-
-               # Some additional tests for mediawiki-specific features
-               $tests[] = [
-                       'Round-trip serialization for <pre>/<listing>/<textarea>',
-                       "<pre>\n\na</pre><listing>\n\nb</listing><textarea>\n\nc</textarea>",
-                       "<pre>\n\na</pre><listing>\n\nb</listing><textarea>\n\nc</textarea>",
-                       true # use the tidy-compatible mode
-               ];
-
-               return $tests;
-       }
-}
index 274eb14..a7c9aa6 100644 (file)
@@ -25,36 +25,32 @@ environment variable to any value:
 
     DISPLAY=1 npm run selenium
 
-To run only one file (for example page.js), you first need to spawn the chromedriver:
+To run only one test (for example specs/page.js), you first need to start Chromedriver:
 
     chromedriver --url-base=wd/hub --port=4444
 
-Then in another terminal:
+Then, in another terminal:
 
-    cd tests/selenium
-    ../../node_modules/.bin/wdio --spec specs/page.js
+    npm run selenium-test -- --spec tests/selenium/specs/page.js
 
-To run only one test (name contains string 'preferences'):
+You can also filter specific cases, for ones that contain the string 'preferences':
 
-    ../../node_modules/.bin/wdio --spec specs/user.js --mochaOpts.grep preferences
+    npm run selenium-test -- tests/selenium/specs/user.js --mochaOpts.grep preferences
 
-The runner reads the config file `wdio.conf.js` and runs the spec listed in
-`page.js`.
-
-The defaults in the configuration files aim are targeting a MediaWiki-Vagrant
-installation on http://127.0.0.1:8080 with a user Admin and
-password 'vagrant'.  Those settings can be overridden using environment
+The runner reads the configuration from `wdio.conf.js`. The defaults target
+a MediaWiki-Vagrant installation on `http://127.0.0.1:8080` with a user "Admin"
+and password "vagrant".  Those settings can be overridden using environment
 variables:
 
-`MW_SERVER`: to be set to the value of your $wgServer
-`MW_SCRIPT_PATH`: ditto with $wgScriptPath
-`MEDIAWIKI_USER`: username of an account that can create users on the wiki
-`MEDIAWIKI_PASSWORD`: password for above user
+`MW_SERVER`: to be set to the value of your $wgServer
+`MW_SCRIPT_PATH`: ditto with $wgScriptPath
+`MEDIAWIKI_USER`: username of an account that can create users on the wiki
+`MEDIAWIKI_PASSWORD`: password for above user
 
 Example:
 
     MW_SERVER=http://example.org MW_SCRIPT_PATH=/dev/w npm run selenium
 
-## Links
+## Further reading
 
 - [Selenium/Node.js](https://www.mediawiki.org/wiki/Selenium/Node.js)
index 105f409..2bcef13 100644 (file)
@@ -1,8 +1,7 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' ),
+       Api = require( 'wdio-mediawiki/Api' );
 
 class CreateAccountPage extends Page {
-
        get username() { return browser.element( '#wpName2' ); }
        get password() { return browser.element( '#wpPassword2' ); }
        get confirmPassword() { return browser.element( '#wpRetype' ); }
@@ -10,7 +9,7 @@ class CreateAccountPage extends Page {
        get heading() { return browser.element( '#firstHeading' ); }
 
        open() {
-               super.open( 'Special:CreateAccount' );
+               super.openTitle( 'Special:CreateAccount' );
        }
 
        createAccount( username, password ) {
@@ -21,29 +20,10 @@ class CreateAccountPage extends Page {
                this.create.click();
        }
 
+       // @deprecated Use wdio-mediawiki/Api#createAccount() instead.
        apiCreateAccount( username, password ) {
-
-               const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot
-                       Promise = require( 'bluebird' );
-               let bot = new MWBot();
-
-               return Promise.coroutine( function* () {
-                       yield bot.loginGetCreateaccountToken( {
-                               apiUrl: `${browser.options.baseUrl}/api.php`,
-                               username: browser.options.username,
-                               password: browser.options.password
-                       } );
-                       yield bot.request( {
-                               action: 'createaccount',
-                               createreturnurl: browser.options.baseUrl,
-                               createtoken: bot.createaccountToken,
-                               username: username,
-                               password: password,
-                               retype: password
-                       } );
-               } ).call( this );
-
+               return Api.createAccount( username, password );
        }
-
 }
+
 module.exports = new CreateAccountPage();
index d43cb9f..1218818 100644 (file)
@@ -1,39 +1,26 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' ),
+       Api = require( 'wdio-mediawiki/Api' );
 
 class DeletePage extends Page {
-
        get reason() { return browser.element( '#wpReason' ); }
        get watch() { return browser.element( '#wpWatch' ); }
        get submit() { return browser.element( '#wpConfirmB' ); }
        get displayedContent() { return browser.element( '#mw-content-text' ); }
 
-       open( name ) {
-               super.open( name + '&action=delete' );
+       open( title ) {
+               super.openTitle( title, { action: 'delete' } );
        }
 
-       delete( name, reason ) {
-               this.open( name );
+       delete( title, reason ) {
+               this.open( title );
                this.reason.setValue( reason );
                this.submit.click();
        }
 
+       // @deprecated Use wdio-mediawiki/Api#delete() instead.
        apiDelete( name, reason ) {
-
-               const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot
-                       Promise = require( 'bluebird' );
-               let bot = new MWBot();
-
-               return Promise.coroutine( function* () {
-                       yield bot.loginGetEditToken( {
-                               apiUrl: `${browser.options.baseUrl}/api.php`,
-                               username: browser.options.username,
-                               password: browser.options.password
-                       } );
-                       yield bot.delete( name, reason );
-               } ).call( this );
-
+               return Api.delete( name, reason );
        }
-
 }
+
 module.exports = new DeletePage();
index 33a27f0..8bc7dc6 100644 (file)
@@ -1,15 +1,14 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' ),
+       Api = require( 'wdio-mediawiki/Api' );
 
 class EditPage extends Page {
-
        get content() { return browser.element( '#wpTextbox1' ); }
        get displayedContent() { return browser.element( '#mw-content-text' ); }
        get heading() { return browser.element( '#firstHeading' ); }
        get save() { return browser.element( '#wpSave' ); }
 
-       openForEditing( name ) {
-               super.open( name + '&action=edit' );
+       openForEditing( title ) {
+               super.openTitle( title, { action: 'edit' } );
        }
 
        edit( name, content ) {
@@ -18,22 +17,10 @@ class EditPage extends Page {
                this.save.click();
        }
 
+       // @deprecated Use wdio-mediawiki/Api#edit() instead.
        apiEdit( name, content ) {
-
-               const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot
-                       Promise = require( 'bluebird' );
-               let bot = new MWBot();
-
-               return Promise.coroutine( function* () {
-                       yield bot.loginGetEditToken( {
-                               apiUrl: `${browser.options.baseUrl}/api.php`,
-                               username: browser.options.username,
-                               password: browser.options.password
-                       } );
-                       yield bot.edit( name, content, `Created page with "${content}"` );
-               } ).call( this );
-
+               return Api.edit( name, content );
        }
-
 }
+
 module.exports = new EditPage();
index 869484e..acaf3ea 100644 (file)
@@ -1,13 +1,11 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' );
 
 class HistoryPage extends Page {
-
        get comment() { return browser.element( '#pagehistory .comment' ); }
 
-       open( name ) {
-               super.open( name + '&action=history' );
+       open( title ) {
+               super.openTitle( title, { action: 'history' } );
        }
-
 }
+
 module.exports = new HistoryPage();
index 77bb1f4..f159990 100644 (file)
@@ -1,8 +1,12 @@
-// From http://webdriver.io/guide/testrunner/pageobjects.html
-'use strict';
-class Page {
+const Page = require( 'wdio-mediawiki/Page' );
+
+/**
+ * @deprecated Use wdio-mediawiki/Page and openTitle() instead.
+ */
+class LegacyPage extends Page {
        open( path ) {
                browser.url( browser.options.baseUrl + '/index.php?title=' + path );
        }
 }
-module.exports = Page;
+
+module.exports = LegacyPage;
index 98b87fe..64fd582 100644 (file)
@@ -1,13 +1,11 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' );
 
 class PreferencesPage extends Page {
-
        get realName() { return browser.element( '#mw-input-wprealname' ); }
        get save() { return browser.element( '#prefcontrol' ); }
 
        open() {
-               super.open( 'Special:Preferences' );
+               super.openTitle( 'Special:Preferences' );
        }
 
        changeRealName( realName ) {
@@ -15,6 +13,6 @@ class PreferencesPage extends Page {
                this.realName.setValue( realName );
                this.save.click();
        }
-
 }
+
 module.exports = new PreferencesPage();
index 071f7f9..47ad145 100644 (file)
@@ -1,21 +1,19 @@
-'use strict';
-const Page = require( './page' );
+const Page = require( 'wdio-mediawiki/Page' );
 
 class RestorePage extends Page {
-
        get reason() { return browser.element( '#wpComment' ); }
        get submit() { return browser.element( '#mw-undelete-submit' ); }
        get displayedContent() { return browser.element( '#mw-content-text' ); }
 
-       open( name ) {
-               super.open( 'Special:Undelete/' + name );
+       open( subject ) {
+               super.openTitle( 'Special:Undelete/' + subject );
        }
 
-       restore( name, reason ) {
-               this.open( name );
+       restore( subject, reason ) {
+               this.open( subject );
                this.reason.setValue( reason );
                this.submit.click();
        }
-
 }
+
 module.exports = new RestorePage();
index 0061d0c..971e21b 100644 (file)
@@ -1,27 +1,6 @@
-'use strict';
-const Page = require( './page' );
+const LoginPage = require( 'wdio-mediawiki/LoginPage' );
 
-class UserLoginPage extends Page {
-
-       get username() { return browser.element( '#wpName1' ); }
-       get password() { return browser.element( '#wpPassword1' ); }
-       get loginButton() { return browser.element( '#wpLoginAttempt' ); }
-       get userPage() { return browser.element( '#pt-userpage' ); }
-
-       open() {
-               super.open( 'Special:UserLogin' );
-       }
-
-       login( username, password ) {
-               this.open();
-               this.username.setValue( username );
-               this.password.setValue( password );
-               this.loginButton.click();
-       }
-
-       loginAdmin() {
-               this.login( browser.options.username, browser.options.password );
-       }
-
-}
-module.exports = new UserLoginPage();
+/**
+ * @deprecated Use wdio-mediawiki/LoginPage instead.
+ */
+module.exports = LoginPage;
index 376dce5..a1fd480 100644 (file)
@@ -1,5 +1,5 @@
-'use strict';
 const assert = require( 'assert' ),
+       Api = require( 'wdio-mediawiki/Api' ),
        DeletePage = require( '../pageobjects/delete.page' ),
        RestorePage = require( '../pageobjects/restore.page' ),
        EditPage = require( '../pageobjects/edit.page' ),
@@ -7,7 +7,6 @@ const assert = require( 'assert' ),
        UserLoginPage = require( '../pageobjects/userlogin.page' );
 
 describe( 'Page', function () {
-
        var content,
                name;
 
@@ -28,14 +27,12 @@ describe( 'Page', function () {
        } );
 
        it( 'should be creatable', function () {
-
                // create
                EditPage.edit( name, content );
 
                // check
                assert.equal( EditPage.heading.getText(), name );
                assert.equal( EditPage.displayedContent.getText(), content );
-
        } );
 
        it( 'should be re-creatable', function () {
@@ -43,12 +40,12 @@ describe( 'Page', function () {
 
                // create
                browser.call( function () {
-                       return EditPage.apiEdit( name, initialContent );
+                       return Api.edit( name, initialContent );
                } );
 
                // delete
                browser.call( function () {
-                       return DeletePage.apiDelete( name, 'delete prior to recreate' );
+                       return Api.delete( name, 'delete prior to recreate' );
                } );
 
                // create
@@ -57,14 +54,12 @@ describe( 'Page', function () {
                // check
                assert.equal( EditPage.heading.getText(), name );
                assert.equal( EditPage.displayedContent.getText(), content );
-
        } );
 
        it( 'should be editable', function () {
-
                // create
                browser.call( function () {
-                       return EditPage.apiEdit( name, content );
+                       return Api.edit( name, content );
                } );
 
                // edit
@@ -73,30 +68,26 @@ describe( 'Page', function () {
                // check
                assert.equal( EditPage.heading.getText(), name );
                assert.equal( EditPage.displayedContent.getText(), content );
-
        } );
 
        it( 'should have history', function () {
-
                // create
                browser.call( function () {
-                       return EditPage.apiEdit( name, content );
+                       return Api.edit( name, content );
                } );
 
                // check
                HistoryPage.open( name );
                assert.equal( HistoryPage.comment.getText(), `(Created page with "${content}")` );
-
        } );
 
        it( 'should be deletable', function () {
-
                // login
                UserLoginPage.loginAdmin();
 
                // create
                browser.call( function () {
-                       return EditPage.apiEdit( name, content );
+                       return Api.edit( name, content );
                } );
 
                // delete
@@ -107,22 +98,20 @@ describe( 'Page', function () {
                        DeletePage.displayedContent.getText(),
                        '"' + name + '" has been deleted. See deletion log for a record of recent deletions.\nReturn to Main Page.'
                );
-
        } );
 
        it( 'should be restorable', function () {
-
                // login
                UserLoginPage.loginAdmin();
 
                // create
                browser.call( function () {
-                       return EditPage.apiEdit( name, content );
+                       return Api.edit( name, content );
                } );
 
                // delete
                browser.call( function () {
-                       return DeletePage.apiDelete( name, content + '-deletereason' );
+                       return Api.delete( name, content + '-deletereason' );
                } );
 
                // restore
@@ -130,7 +119,5 @@ describe( 'Page', function () {
 
                // check
                assert.equal( RestorePage.displayedContent.getText(), name + ' has been restored\nConsult the deletion log for a record of recent deletions and restorations.' );
-
        } );
-
 } );
index 3f3872d..10bf05d 100644 (file)
@@ -1,11 +1,10 @@
-'use strict';
 const assert = require( 'assert' ),
        CreateAccountPage = require( '../pageobjects/createaccount.page' ),
        PreferencesPage = require( '../pageobjects/preferences.page' ),
-       UserLoginPage = require( '../pageobjects/userlogin.page' );
+       UserLoginPage = require( 'wdio-mediawiki/LoginPage' ),
+       Api = require( 'wdio-mediawiki/Api' );
 
 describe( 'User', function () {
-
        var password,
                username;
 
@@ -22,20 +21,17 @@ describe( 'User', function () {
        } );
 
        it( 'should be able to create account', function () {
-
                // create
                CreateAccountPage.createAccount( username, password );
 
                // check
                assert.equal( CreateAccountPage.heading.getText(), `Welcome, ${username}!` );
-
        } );
 
        it( 'should be able to log in', function () {
-
                // create
                browser.call( function () {
-                       return CreateAccountPage.apiCreateAccount( username, password );
+                       return Api.createAccount( username, password );
                } );
 
                // log in
@@ -43,16 +39,14 @@ describe( 'User', function () {
 
                // check
                assert.equal( UserLoginPage.userPage.getText(), username );
-
        } );
 
        it( 'should be able to change preferences', function () {
-
                var realName = Math.random().toString();
 
                // create
                browser.call( function () {
-                       return CreateAccountPage.apiCreateAccount( username, password );
+                       return Api.createAccount( username, password );
                } );
 
                // log in
@@ -63,7 +57,5 @@ describe( 'User', function () {
 
                // check
                assert.equal( PreferencesPage.realName.getValue(), realName );
-
        } );
-
 } );
diff --git a/tests/selenium/wdio-mediawiki/.eslintrc.json b/tests/selenium/wdio-mediawiki/.eslintrc.json
new file mode 100644 (file)
index 0000000..a49d096
--- /dev/null
@@ -0,0 +1,10 @@
+{
+       "extends": "wikimedia",
+       "env": {
+               "es6": true,
+               "node": true
+       },
+       "globals": {
+               "browser": false
+       }
+}
diff --git a/tests/selenium/wdio-mediawiki/Api.js b/tests/selenium/wdio-mediawiki/Api.js
new file mode 100644 (file)
index 0000000..40bce32
--- /dev/null
@@ -0,0 +1,77 @@
+const MWBot = require( 'mwbot' );
+
+// TODO: Once we require Node 7 or later, we can use async-await.
+
+module.exports = {
+       /**
+        * Shortcut for `MWBot#edit( .. )`.
+        *
+        * @since 1.0.0
+        * @see <https://www.mediawiki.org/wiki/API:Edit>
+        * @param {string} title
+        * @param {string} content
+        * @return {Object} Promise for API action=edit response data.
+        */
+       edit( title, content ) {
+               let bot = new MWBot();
+
+               return bot.loginGetEditToken( {
+                       apiUrl: `${browser.options.baseUrl}/api.php`,
+                       username: browser.options.username,
+                       password: browser.options.password
+               } ).then( function () {
+                       return bot.edit( title, content, `Created page with "${content}"` );
+               } );
+       },
+
+       /**
+        * Shortcut for `MWBot#delete( .. )`.
+        *
+        * @since 1.0.0
+        * @see <https://www.mediawiki.org/wiki/API:Delete>
+        * @param {string} title
+        * @param {string} reason
+        * @return {Object} Promise for API action=delete response data.
+        */
+       delete( title, reason ) {
+               let bot = new MWBot();
+
+               return bot.loginGetEditToken( {
+                       apiUrl: `${browser.options.baseUrl}/api.php`,
+                       username: browser.options.username,
+                       password: browser.options.password
+               } ).then( function () {
+                       return bot.delete( title, reason );
+               } );
+       },
+
+       /**
+        * Shortcut for `MWBot#request( { acount: 'createaccount', .. } )`.
+        *
+        * @since 1.0.0
+        * @see <https://www.mediawiki.org/wiki/API:Account_creation>
+        * @param {string} username
+        * @param {string} password
+        * @return {Object} Promise for API action=createaccount response data.
+        */
+       createAccount( username, password ) {
+               let bot = new MWBot();
+
+               // Log in as admin
+               return bot.loginGetCreateaccountToken( {
+                       apiUrl: `${browser.options.baseUrl}/api.php`,
+                       username: browser.options.username,
+                       password: browser.options.password
+               } ).then( function () {
+                       // Create the new account
+                       return bot.request( {
+                               action: 'createaccount',
+                               createreturnurl: browser.options.baseUrl,
+                               createtoken: bot.createaccountToken,
+                               username: username,
+                               password: password,
+                               retype: password
+                       } );
+               } );
+       }
+};
diff --git a/tests/selenium/wdio-mediawiki/BlankPage.js b/tests/selenium/wdio-mediawiki/BlankPage.js
new file mode 100644 (file)
index 0000000..ed99bd4
--- /dev/null
@@ -0,0 +1,11 @@
+const Page = require( 'wdio-mediawiki/Page' );
+
+class BlankPage extends Page {
+       get heading() { return browser.element( '#firstHeading' ); }
+
+       open() {
+               super.openTitle( 'Special:BlankPage', { uselang: 'en' } );
+       }
+}
+
+module.exports = new BlankPage();
diff --git a/tests/selenium/wdio-mediawiki/CHANGELOG.md b/tests/selenium/wdio-mediawiki/CHANGELOG.md
new file mode 100644 (file)
index 0000000..bfce387
--- /dev/null
@@ -0,0 +1,8 @@
+# Notable changes
+
+## [Unreleased]
+
+* Api: Added initial version.
+* Page: Added initial version.
+* BlankPage: Added initial version.
+* LoginPage: Added initial version.
diff --git a/tests/selenium/wdio-mediawiki/LICENSE b/tests/selenium/wdio-mediawiki/LICENSE
new file mode 100644 (file)
index 0000000..ad55501
--- /dev/null
@@ -0,0 +1,21 @@
+Copyright 2018 Željko Filipin
+Copyright 2018 Timo Tijhof
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/tests/selenium/wdio-mediawiki/LoginPage.js b/tests/selenium/wdio-mediawiki/LoginPage.js
new file mode 100644 (file)
index 0000000..d07934b
--- /dev/null
@@ -0,0 +1,25 @@
+const Page = require( 'wdio-mediawiki/Page' );
+
+class LoginPage extends Page {
+       get username() { return browser.element( '#wpName1' ); }
+       get password() { return browser.element( '#wpPassword1' ); }
+       get loginButton() { return browser.element( '#wpLoginAttempt' ); }
+       get userPage() { return browser.element( '#pt-userpage' ); }
+
+       open() {
+               super.openTitle( 'Special:UserLogin' );
+       }
+
+       login( username, password ) {
+               this.open();
+               this.username.setValue( username );
+               this.password.setValue( password );
+               this.loginButton.click();
+       }
+
+       loginAdmin() {
+               this.login( browser.options.username, browser.options.password );
+       }
+}
+
+module.exports = new LoginPage();
diff --git a/tests/selenium/wdio-mediawiki/Page.js b/tests/selenium/wdio-mediawiki/Page.js
new file mode 100644 (file)
index 0000000..48620e6
--- /dev/null
@@ -0,0 +1,23 @@
+const querystring = require( 'querystring' );
+
+/**
+ * Based on http://webdriver.io/guide/testrunner/pageobjects.html
+ */
+class Page {
+
+       /**
+        * Navigate the browser to a given page.
+        *
+        * @since 1.0.0
+        * @see <http://webdriver.io/api/protocol/url.html>
+        * @param {string} title Page title
+        * @param {Object} [query] Query parameter
+        * @return {void} This method runs a browser command.
+        */
+       openTitle( title, query = {} ) {
+               query.title = title;
+               browser.url( browser.options.baseUrl + '/index.php?' + querystring.stringify( query ) );
+       }
+}
+
+module.exports = Page;
diff --git a/tests/selenium/wdio-mediawiki/README.md b/tests/selenium/wdio-mediawiki/README.md
new file mode 100644 (file)
index 0000000..260dc77
--- /dev/null
@@ -0,0 +1,53 @@
+# wdio-mediawiki
+
+A plugin for [WebdriverIO](http://webdriver.io/) providing utilities to simplify testing of MediaWiki features.
+
+## Getting Started
+
+### Page
+
+The `Page` class is a base class for following the [Page Objects Pattern](http://webdriver.io/guide/testrunner/pageobjects.html).
+
+* `openTitle( title [, Object query ] )`
+
+The convention is for implementations to extend this class and provide an `open()` method
+that calls `super.openTitle()`, as well as add various getters for elements on the page.
+
+See [BlankPage](./BlankPage.js) and [specs/BlankPage](./specs/BlankPage.js) for an example.
+
+### Api
+
+Utilities to interact with the MediaWiki API. Uses the [mwbot](https://github.com/Fannon/mwbot) library.
+
+Actions are performed logged-in using `browser.options.username` and `browser.options.password`,
+which typically come from `MEDIAWIKI_USER` and `MEDIAWIKI_PASSWORD` environment variables.
+
+* `edit(title, content)`
+* `delete(title, reason)`
+* `createAccount(username, password)`
+
+## Versioning
+
+This package follows [Semantic Versioning guidelines](https://semver.org/) for its releases. In
+particular, its major version must be bumped when compatibility is removed for a previous of
+MediaWiki.
+
+It is the expectation that this module will only support a single version of MediaWiki at any
+given time, and that tests in older branches of MediaWiki-related projects naturally use the older
+release line of this package.
+
+In order to allow for smooth and decentralised upgrades, it is recommended that the only type of
+breaking change made to this package is a change that removes something. Thus, in order to change
+something, it must either be backwards-compatible, or must be introduced as a new method that
+co-exists with its deprecated equivalent for at least one release.
+
+## Issue tracker
+
+Please report issues to [Phabricator](https://phabricator.wikimedia.org/tag/mediawiki-core-tests/).
+
+## Contributing
+
+This module is maintained in the MediaWiki core repository and published from there as a
+package to npmjs.org. To simplify development and to ensure changes are verified
+automatically, MediaWiki core itself uses this module directly from the working copy
+using [npm Local Paths](https://docs.npmjs.com/files/package.json#local-paths).
diff --git a/tests/selenium/wdio-mediawiki/index.js b/tests/selenium/wdio-mediawiki/index.js
new file mode 100644 (file)
index 0000000..d3171be
--- /dev/null
@@ -0,0 +1,26 @@
+const fs = require( 'fs' );
+
+module.exports = {
+       /**
+        * Based on <https://github.com/webdriverio/webdriverio/issues/269#issuecomment-306342170>
+        *
+        * @since 1.0.0
+        * @param {string} title Description (will be sanitised and used as file name)
+        * @return {string} File path
+        */
+       saveScreenshot( title ) {
+               var filename, filePath;
+               // Create sane file name for current test title
+               filename = encodeURIComponent( title.replace( /\s+/g, '-' ) );
+               filePath = `${browser.options.screenshotPath}/${filename}.png`;
+               // Ensure directory exists, based on WebDriverIO#saveScreenshotSync()
+               try {
+                       fs.statSync( browser.options.screenshotPath );
+               } catch ( err ) {
+                       fs.mkdirSync( browser.options.screenshotPath );
+               }
+               // Create and save screenshot
+               browser.saveScreenshot( filePath );
+               return filePath;
+       }
+};
diff --git a/tests/selenium/wdio-mediawiki/package.json b/tests/selenium/wdio-mediawiki/package.json
new file mode 100644 (file)
index 0000000..be7ed33
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "name": "wdio-mediawiki",
+  "version": "0.1.0",
+  "description": "WebdriverIO plugin for testing a MediaWiki site.",
+  "homepage": "https://gerrit.wikimedia.org/g/mediawiki/core/+/master/tests/selenium/wdio-mediawiki/",
+  "license": "MIT",
+  "keywords": [
+    "mediawiki",
+    "wdio-plugin"
+  ],
+  "files": [
+    "*.js",
+    "specs/"
+  ],
+  "engines": {
+    "node" : ">=6.0"
+  },
+  "dependencies": {
+    "mwbot": "1.0.10"
+  }
+}
diff --git a/tests/selenium/wdio-mediawiki/specs/BlankPage.js b/tests/selenium/wdio-mediawiki/specs/BlankPage.js
new file mode 100644 (file)
index 0000000..f84ae90
--- /dev/null
@@ -0,0 +1,11 @@
+const assert = require( 'assert' ),
+       BlankPage = require( 'wdio-mediawiki/BlankPage' );
+
+describe( 'BlankPage', function () {
+       it( 'should have its title', function () {
+               BlankPage.open();
+
+               // check
+               assert.equal( BlankPage.heading.getText(), 'Blank page' );
+       } );
+} );
index ca9f846..f785d36 100644 (file)
@@ -1,8 +1,7 @@
-'use strict';
-
 const fs = require( 'fs' ),
        path = require( 'path' ),
-       logPath = process.env.LOG_DIR || './log/';
+       saveScreenshot = require( 'wdio-mediawiki' ).saveScreenshot,
+       logPath = process.env.LOG_DIR || __dirname + '/log';
 
 function relPath( foo ) {
        return path.resolve( __dirname, '../..', foo );
@@ -13,33 +12,43 @@ exports.config = {
        // Custom WDIO config specific to MediaWiki
        // ======
        // Use in a test as `browser.options.<key>`.
-
-       // Configure wiki admin user/pass via env
        // Defaults are for convenience with MediaWiki-Vagrant
+
+       // Wiki admin
        username: process.env.MEDIAWIKI_USER || 'Admin',
        password: process.env.MEDIAWIKI_PASSWORD || 'vagrant',
 
+       // Base for browser.url() and Page#openTitle()
+       baseUrl: ( process.env.MW_SERVER || 'http://127.0.0.1:8080' ) + (
+               process.env.MW_SCRIPT_PATH || '/w'
+       ),
+
        // ======
        // Sauce Labs
        // ======
+       // See http://webdriver.io/guide/services/sauce.html
+       // and https://docs.saucelabs.com/reference/platforms-configurator
        services: [ 'sauce' ],
        user: process.env.SAUCE_USERNAME,
        key: process.env.SAUCE_ACCESS_KEY,
 
+       // Default timeout in milliseconds for Selenium Grid requests
+       connectionRetryTimeout: 90 * 1000,
+
+       // Default request retries count
+       connectionRetryCount: 3,
+
        // ==================
-       // Specify Test Files
+       // Test Files
        // ==================
-       // Define which test specs should run. The pattern is relative to the directory
-       // from which `wdio` was called. Notice that, if you are calling `wdio` from an
-       // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
-       // directory is where your package.json resides, so `wdio` will be called from there.
        specs: [
+               relPath( './tests/selenium/wdio-mediawiki/specs/*.js' ),
                relPath( './tests/selenium/specs/**/*.js' ),
                relPath( './extensions/*/tests/selenium/specs/**/*.js' ),
                relPath( './extensions/VisualEditor/modules/ve-mw/tests/selenium/specs/**/*.js' ),
                relPath( './skins/*/tests/selenium/specs/**/*.js' )
        ],
-       // Patterns to exclude.
+       // Patterns to exclude
        exclude: [
                relPath( './extensions/CirrusSearch/tests/selenium/specs/**/*.js' )
        ],
@@ -47,51 +56,37 @@ exports.config = {
        // ============
        // Capabilities
        // ============
-       // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
-       // time. Depending on the number of capabilities, WebdriverIO launches several test
-       // sessions. Within your capabilities you can overwrite the spec and exclude options in
-       // order to group specific specs to a specific capability.
 
-       // First, you can define how many instances should be started at the same time. Let's
-       // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
-       // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
-       // files and you set maxInstances to 10, all spec files will get tested at the same time
-       // and 30 processes will get spawned. The property handles how many capabilities
-       // from the same test should run tests.
+       // How many instances of the same capability (browser) may be started at the same time.
        maxInstances: 1,
 
-       // If you have trouble getting all important capabilities together, check out the
-       // Sauce Labs platform configurator - a great tool to configure your capabilities:
-       // https://docs.saucelabs.com/reference/platforms-configurator
-       //
-       // For Chrome/Chromium https://sites.google.com/a/chromium.org/chromedriver/capabilities
        capabilities: [ {
-               // maxInstances can get overwritten per capability. So if you have an in-house Selenium
-               // grid with only 5 firefox instances available you can make sure that not more than
-               // 5 instances get started at a time.
-               maxInstances: 1,
+               // For Chrome/Chromium https://sites.google.com/a/chromium.org/chromedriver/capabilities
                browserName: 'chrome',
+               maxInstances: 1,
                chromeOptions: {
-                       // If DISPLAY is set, assume running from developer machine and/or with Xvfb.
+                       // If DISPLAY is set, assume developer asked non-headless or CI with Xvfb.
                        // Otherwise, use --headless (added in Chrome 59)
                        // https://chromium.googlesource.com/chromium/src/+/59.0.3030.0/headless/README.md
-                       args: (
-                               process.env.DISPLAY ? [] : [ '--headless' ]
-                       ).concat(
+                       args: [
+                               ...( process.env.DISPLAY ? [] : [ '--headless' ] ),
                                // Chrome sandbox does not work in Docker
-                               fs.existsSync( '/.dockerenv' ) ? [ '--no-sandbox' ] : []
-                       )
+                               ...( fs.existsSync( '/.dockerenv' ) ? [ '--no-sandbox' ] : [] )
+                       ]
                }
        } ],
 
        // ===================
        // Test Configurations
        // ===================
-       // Define all options that are relevant for the WebdriverIO instance here
+
+       // Enabling synchronous mode (via the wdio-sync package), means specs don't have to
+       // use Promise#then() or await for browser commands, such as like `brower.element()`.
+       // Instead, it will automatically pause JavaScript execution until th command finishes.
        //
-       // By default WebdriverIO commands are executed in a synchronous way using
-       // the wdio-sync package. If you still want to run your tests in an async way
-       // e.g. using promises you can set the sync option to false.
+       // For non-browser commands (such as MWBot and other promises), this means you
+       // have to use `browser.call()` to make sure WDIO waits for it before the next
+       // browser command.
        sync: true,
 
        // Level of logging verbosity: silent | verbose | command | data | result | error
@@ -103,67 +98,23 @@ exports.config = {
        // Warns when a deprecated command is used
        deprecationWarnings: true,
 
-       // If you only want to run your tests until a specific amount of tests have failed use
-       // bail (default is 0 - don't bail, run all tests).
+       // Stop the tests once a certain number of failed tests have been recorded.
+       // Default is 0 - don't bail, run all tests.
        bail: 0,
 
-       // Saves a screenshot to a given path if a command fails.
+       // Setting this enables automatic screenshots for when a browser command fails
+       // It is also used by afterTest for capturig failed assertions.
        screenshotPath: logPath,
 
-       // Set a base URL in order to shorten url command calls. If your `url` parameter starts
-       // with `/`, the base url gets prepended, not including the path portion of your baseUrl.
-       // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
-       // gets prepended directly.
-       baseUrl: (
-               process.env.MW_SERVER || 'http://127.0.0.1:8080'
-       ) + (
-               process.env.MW_SCRIPT_PATH || '/w'
-       ),
+       // Default timeout for each waitFor* command.
+       waitforTimeout: 10 * 1000,
 
-       // Default timeout for all waitFor* commands.
-       waitforTimeout: 10000,
-
-       // Default timeout in milliseconds for request
-       // if Selenium Grid doesn't send response
-       connectionRetryTimeout: 90000,
-
-       // Default request retries count
-       connectionRetryCount: 3,
-
-       // Initialize the browser instance with a WebdriverIO plugin. The object should have the
-       // plugin name as key and the desired plugin options as properties. Make sure you have
-       // the plugin installed before running any tests. The following plugins are currently
-       // available:
-       // WebdriverCSS: https://github.com/webdriverio/webdrivercss
-       // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
-       // Browserevent: https://github.com/webdriverio/browserevent
-       // plugins: {
-       //      webdrivercss: {
-       //              screenshotRoot: 'my-shots',
-       //              failedComparisonsRoot: 'diffs',
-       //              misMatchTolerance: 0.05,
-       //              screenWidth: [320,480,640,1024]
-       //      },
-       //      webdriverrtc: {},
-       //      browserevent: {}
-       // },
-       //
-       // Test runner services
-       // Services take over a specific job you don't want to take care of. They enhance
-       // your test setup with almost no effort. Unlike plugins, they don't add new
-       // commands. Instead, they hook themselves up into the test process.
-       // services: [],//
        // Framework you want to run your specs with.
-       // The following are supported: Mocha, Jasmine, and Cucumber
-       // see also: http://webdriver.io/guide/testrunner/frameworks.html
-       //
-       // Make sure you have the wdio adapter package for the specific framework installed
-       // before running any tests.
+       // See also: http://webdriver.io/guide/testrunner/frameworks.html
        framework: 'mocha',
 
        // Test reporter for stdout.
-       // The only one supported by default is 'dot'
-       // see also: http://webdriver.io/guide/testrunner/reporters.html
+       // See also: http://webdriver.io/guide/testrunner/reporters.html
        reporters: [ 'spec', 'junit' ],
        reporterOptions: {
                junit: {
@@ -175,141 +126,24 @@ exports.config = {
        // See the full list at http://mochajs.org/
        mochaOpts: {
                ui: 'bdd',
-               timeout: 60000
+               timeout: 60 * 1000
        },
 
        // =====
        // Hooks
        // =====
-       // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
-       // it and to build services around it. You can either apply a single function or an array of
-       // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
-       // resolved to continue.
+       // See also: http://webdriver.io/guide/testrunner/configurationfile.html
 
        /**
-        * Gets executed once before all workers get launched.
-        * @param {Object} config wdio configuration object
-        * @param {Array.<Object>} capabilities list of capabilities details
+        * Save a screenshot when test fails.
+        *
+        * @param {Object} test Mocha Test object
         */
-       // onPrepare: function (config, capabilities) {
-       // },
-
-       /**
-        * Gets executed just before initialising the webdriver session and test framework. It allows you
-        * to manipulate configurations depending on the capability or spec.
-        * @param {Object} config wdio configuration object
-        * @param {Array.<Object>} capabilities list of capabilities details
-        * @param {Array.<String>} specs List of spec file paths that are to be run
-        */
-       // beforeSession: function (config, capabilities, specs) {
-       // },
-
-       /**
-        * Gets executed before test execution begins. At this point you can access to all global
-        * variables like `browser`. It is the perfect place to define custom commands.
-        * @param {Array.<Object>} capabilities list of capabilities details
-        * @param {Array.<String>} specs List of spec file paths that are to be run
-        */
-       // before: function (capabilities, specs) {
-       // },
-
-       /**
-        * Runs before a WebdriverIO command gets executed.
-        * @param {String} commandName hook command name
-        * @param {Array} args arguments that command would receive
-        */
-       // beforeCommand: function (commandName, args) {
-       // },
-
-       /**
-        * Hook that gets executed before the suite starts
-        * @param {Object} suite suite details
-        */
-       // beforeSuite: function (suite) {
-       // },
-
-       /**
-        * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
-        * @param {Object} test test details
-        */
-       // beforeTest: function (test) {
-       // },
-
-       /**
-        * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
-        * beforeEach in Mocha)
-        */
-       // beforeHook: function () {
-       // },
-
-       /**
-        * Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling
-        * afterEach in Mocha)
-        */
-       // afterHook: function () {
-       // },
-       /**
-        * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) ends.
-        * @param {Object} test test details
-        */
-       // from https://github.com/webdriverio/webdriverio/issues/269#issuecomment-306342170
        afterTest: function ( test ) {
-               var filename, filePath;
-               // if test passed, ignore, else take and save screenshot
-               if ( test.passed ) {
-                       return;
+               var filePath;
+               if ( !test.passed ) {
+                       filePath = saveScreenshot( test.title );
+                       console.log( '\n\tScreenshot: ' + filePath + '\n' );
                }
-               // get current test title and clean it, to use it as file name
-               filename = encodeURIComponent( test.title.replace( /\s+/g, '-' ) );
-               // build file path
-               filePath = this.screenshotPath + filename + '.png';
-               // save screenshot
-               browser.saveScreenshot( filePath );
-               console.log( '\n\tScreenshot location:', filePath, '\n' );
        }
-
-       /**
-        * Hook that gets executed after the suite has ended
-        * @param {Object} suite suite details
-        */
-       // afterSuite: function (suite) {
-       // },
-
-       /**
-        * Runs after a WebdriverIO command gets executed
-        * @param {String} commandName hook command name
-        * @param {Array} args arguments that command would receive
-        * @param {Number} result 0 - command success, 1 - command error
-        * @param {Object} error error object if any
-        */
-       // afterCommand: function (commandName, args, result, error) {
-       // },
-
-       /**
-        * Gets executed after all tests are done. You still have access to all global variables from
-        * the test.
-        * @param {Number} result 0 - test pass, 1 - test fail
-        * @param {Array.<Object>} capabilities list of capabilities details
-        * @param {Array.<String>} specs List of spec file paths that ran
-        */
-       // after: function (result, capabilities, specs) {
-       // },
-
-       /**
-        * Gets executed right after terminating the webdriver session.
-        * @param {Object} config wdio configuration object
-        * @param {Array.<Object>} capabilities list of capabilities details
-        * @param {Array.<String>} specs List of spec file paths that ran
-        */
-       // afterSession: function (config, capabilities, specs) {
-       // },
-
-       /**
-        * Gets executed after all workers got shut down and the process is about to exit.
-        * @param {Object} exitCode 0 - success, 1 - fail
-        * @param {Object} config wdio configuration object
-        * @param {Array.<Object>} capabilities list of capabilities details
-        */
-       // onComplete: function(exitCode, config, capabilities) {
-       // }
 };