diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..a20e8b0 --- /dev/null +++ b/.gitreview @@ -0,0 +1,6 @@ +[gerrit] +host=gerrit.wikimedia.org +port=29418 +project=mediawiki/extensions/Cognate.git +defaultbranch=master +defaultrebase=0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e542f9 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# InterwikiSorting extension +EDIT \ No newline at end of file diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..2b97fd8 --- /dev/null +++ b/extension.json @@ -0,0 +1,33 @@ +{ + "name": "InterwikiSorting", + "version": "0.0.1", + "author": [ + "WMDE" + ], + "url": "https://www.mediawiki.org/wiki/Extension:InterwikiSorting", + "descriptionmsg": "interwikisorting-desc", + "license-name": "GPL-2.0+", + "type": "other", + "config": { + "InterwikiSortingNamespaces": [], + "InterwikiSortingAlwaysSort": false, + "InterwikiSortingSort": "code", + "InterwikiSortingSortPrepend": [], + "InterwikiSortingInterwikiSortOrders": [] + }, + "AutoloadClasses": { + "InterwikiSorting\\InterwikiSorter": "src/InterwikiSorter.php", + "InterwikiSorting\\InterwikiSortingHooks": "src/InterwikiSortingHooks.php", + "InterwikiSorting\\InterwikiSortingHookHandlers": "src/InterwikiSortingHookHandlers.php" + }, + "Hooks": { + "BeforeInitialize": [ "InterwikiSorting\\InterwikiSortingHooks::onBeforeInitialize" ], + "UnitTestsList": [ "InterwikiSorting\\InterwikiSortingHooks::onUnitTestsList" ] + }, + "MessagesDirs": { + "InterwikiSorting": [ + "i18n" + ] + }, + "manifest_version": 1 +} \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..1264bf0 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Addshore" + ] + }, + "interwikisorting-desc": "Sort interwiki links." +} \ No newline at end of file diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100644 index 0000000..158978c --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Addshore" + ] + }, + "interwikisorting-desc": "{{desc|name=Cognate|url=https://www.mediawiki.org/wiki/Extension:InterwikiSorting}}" +} diff --git a/src/InterwikiSorter.php b/src/InterwikiSorter.php new file mode 100644 index 0000000..02ced82 --- /dev/null +++ b/src/InterwikiSorter.php @@ -0,0 +1,142 @@ + + * @author Katie Filbert < aude.wiki@gmail.com > + * @author Thiemo Mättig + */ +class InterwikiSorter { + + /** + * @see Documentation of "sort" and "interwikiSortOrders" options in docs/options.wiki. + */ + const SORT_CODE = 'code'; + + /** + * @var array[] + */ + private $sortOrders; + + /** + * @var string + */ + private $sort; + + /** + * @var string[] + */ + private $sortPrepend; + + /** + * @var int[]|null + */ + private $sortOrder = null; + + /** + * @param string $sort + * @param array[] $sortOrders + * @param string[] $sortPrepend + */ + public function __construct( $sort, array $sortOrders = array(), array $sortPrepend = array() ) { + $this->sort = $sort; + $this->sortOrders = $sortOrders; + $this->sortPrepend = $sortPrepend; + } + + /** + * Sort an array of links in-place + * @version Copied from InterlanguageExtension rev 114818 + * + * @param string[] $links + * + * @return string[] + */ + public function sortLinks( array $links ) { + if ( $this->sortOrder === null ) { + $this->sortOrder = $this->buildSortOrder( $this->sort, $this->sortOrders ); + } + + // Prepare the array for sorting. + foreach ( $links as $k => $langLink ) { + $links[$k] = explode( ':', $langLink, 2 ); + } + + usort( $links, array( $this, 'compareLinks' ) ); + + // Restore the sorted array. + foreach ( $links as $k => $langLink ) { + $links[$k] = implode( ':', $langLink ); + } + + return $links; + } + + /** + * usort() callback function, compares the links on the basis of $sortOrder + * + * @param string[] $a + * @param string[] $b + * + * @return int + */ + private function compareLinks( array $a, array $b ) { + $a = $a[0]; + $b = $b[0]; + + if ( $a === $b ) { + return 0; + } + + $aIndex = array_key_exists( $a, $this->sortOrder ) ? $this->sortOrder[$a] : null; + $bIndex = array_key_exists( $b, $this->sortOrder ) ? $this->sortOrder[$b] : null; + + if ( $aIndex === $bIndex ) { + // If we encounter multiple unknown languages, which may happen if the sort table is not + // updated, we list them alphabetically. + return strcmp( $a, $b ); + } elseif ( $aIndex === null ) { + // Unknown languages must go under the known languages. + return 1; + } elseif ( $bIndex === null ) { + return -1; + } else { + return $aIndex - $bIndex; + } + } + + /** + * Build sort order to be used by compareLinks(). + * + * @param string $sort + * @param array[] $sortOrders + * + * @return int[] + */ + private function buildSortOrder( $sort, array $sortOrders ) { + if ( $sort === self::SORT_CODE ) { + // The concept of known/unknown languages is irrelevant in strict code order. + $sortOrder = array(); + } elseif ( !array_key_exists( $sort, $sortOrders ) ) { + // Something went wrong, but we can use default "code" order. + wfDebugLog( + __CLASS__, + __FUNCTION__ . ': Invalid or unknown sort order specified for interwiki links.' + ); + $sortOrder = array(); + } else { + $sortOrder = $sortOrders[$sort]; + } + + if ( $this->sortPrepend !== array() ) { + $sortOrder = array_unique( array_merge( $this->sortPrepend, $sortOrder ) ); + } + + return array_flip( $sortOrder ); + } + +} diff --git a/src/InterwikiSortingHookHandlers.php b/src/InterwikiSortingHookHandlers.php new file mode 100644 index 0000000..069518f --- /dev/null +++ b/src/InterwikiSortingHookHandlers.php @@ -0,0 +1,79 @@ +getMainConfig(); + + return new ParserOutputUpdateHookHandlers( + new InterwikiSorter( + $config->get( 'InterwikiSortingSort' ), + $config->get( 'InterwikiSortOrders' ), + $config->get( 'InterwikiSortingSortPrepend' ) + ), + $config->get( 'InterwikiSortingNamespaces' ), + $config->get( 'InterwikiSortingAlwaysSort' ) + ); + } + + /** + * @param InterwikiSorter $sorter + * @param int[] $namespaces + * @param boolean $alwaysSort + */ + public function __construct( + InterwikiSorter $sorter, + $namespaces, + $alwaysSort + ) { + $this->interwikiSorter = $sorter; + $this->namespaces = $namespaces; + $this->alwaysSort = $alwaysSort; + } + + /** + * Hook runs after internal parsing + * @see https://www.mediawiki.org/wiki/Manual:Hooks/ContentAlterParserOutput + * + * @param Title $title + * @param ParserOutput $parserOutput + * + * @return bool + */ + public function doContentAlterParserOutput( Title $title, ParserOutput $parserOutput ) { + if ( !$title->inNamespaces( $this->namespaces ) ) { + // shorten out + return true; + } + + if ( $this->alwaysSort ) { + $interwikiLinks = $parserOutput->getLanguageLinks(); + $sortedLinks = $this->interwikiSorter->sortLinks( $interwikiLinks ); + $parserOutput->setLanguageLinks( $sortedLinks ); + } + + return true; + } + +} diff --git a/src/InterwikiSortingHooks.php b/src/InterwikiSortingHooks.php new file mode 100644 index 0000000..07ac69f --- /dev/null +++ b/src/InterwikiSortingHooks.php @@ -0,0 +1,52 @@ +doContentAlterParserOutput( $title, $parserOutput ); + } + + public static function onBeforeInitialize( + /* Deliberately ignore all params ( We dont need them ) */ + ) { + global $wgHooks; + /** + * The ContentAlterParserOutput hook is registered in the BeforeInitialize so that + * the sorting of interwiki links is always done after anything else might change the + * ParserOutput. + * Hooks::register() can not be used due to the array_merge in Hooks::getHandlers() + * which will return hooks in $wgHooks last. + */ + $wgHooks['ContentAlterParserOutput'][] = + array( InterwikiSortingHooks::class, 'onContentAlterParserOutput' ); + } + + public static function onUnitTestsList( &$files ) { + $files = array_merge( $files, __DIR__ . '/tests/phpunit' ); + return true; + } + +} diff --git a/tests/phpunit/InterwikiSorterTest.php b/tests/phpunit/InterwikiSorterTest.php new file mode 100644 index 0000000..6a1e630 --- /dev/null +++ b/tests/phpunit/InterwikiSorterTest.php @@ -0,0 +1,134 @@ + + */ +class InterwikiSorterTest extends \PHPUnit_Framework_TestCase { + + public function sortOrdersProvider() { + return array( + 'alphabetic' => array( 'ar', 'de', 'en', 'fr', 'ks', 'rn', 'ky', 'hu', 'ja', 'pt' ), + 'alphabetic_revised' => array( 'ar', 'de', 'en', 'fr', 'ks', 'ky', 'rn', 'hu', 'ja', 'pt' ), + 'alphabetic_sr' => array( 'ar', 'de', 'en', 'fr', 'ky', 'rn', 'ks', 'ja', 'hu', 'pt' ), + 'mycustomorder' => array( 'de', 'ja', 'pt', 'hu', 'en' ), + ); + } + + public function constructorProvider() { + $sortOrders = $this->sortOrdersProvider(); + return array( + array( 'code', $sortOrders, array() ), + array( 'code', $sortOrders, array( 'en' ) ) + ); + } + + /** + * @dataProvider constructorProvider + */ + public function testConstructor( $sort, $sortOrders, $sortPrepend ) { + $interwikiSorter = new InterwikiSorter( $sort, $sortOrders, $sortPrepend ); + $this->assertInstanceOf( InterwikiSorter::class, $interwikiSorter ); + } + + public function sortLinksProvider() { + $sortOrders = $this->sortOrdersProvider(); + $links = array( 'fr', 'ky', 'hu', 'ar', 'ks', 'ja', 'de', 'en', 'pt', 'rn' ); + + return array( + array( + $links, 'code', $sortOrders, array(), + array( 'ar', 'de', 'en', 'fr', 'hu', 'ja', 'ks', 'ky', 'pt', 'rn' ) + ), + array( + $links, 'code', $sortOrders, array( 'en' ), + array( 'en', 'ar', 'de', 'fr', 'hu', 'ja', 'ks', 'ky', 'pt', 'rn' ) + ), + array( + $links, 'alphabetic', $sortOrders, array(), + $sortOrders['alphabetic'] + ), + array( + $links, 'alphabetic', $sortOrders, array( 'en', 'ja' ), + array( 'en', 'ja', 'ar', 'de','fr', 'ks', 'rn', 'ky', 'hu', 'pt' ) + ), + array( + $links, 'alphabetic_revised', $sortOrders, array(), + $sortOrders['alphabetic_revised'] + ), + array( + $links, 'alphabetic_revised', $sortOrders, array( 'hu' ), + array( 'hu', 'ar', 'de', 'en', 'fr', 'ks', 'ky', 'rn', 'ja', 'pt' ) + ), + array( + array( 'ja', 'de', 'pt', 'en', 'hu' ), 'mycustomorder', $sortOrders, array(), + $sortOrders['mycustomorder'] + ), + array( + array( 'x2', 'x1', 'x3' ), + 'alphabetic', + array( 'alphabetic' => array() ), + array(), + array( 'x1', 'x2', 'x3' ) + ), + array( + array( 'x2', 'x1', 'en', 'de', 'a2', 'a1' ), + 'alphabetic', + $sortOrders, + array(), + array( 'de', 'en', 'a1', 'a2', 'x1', 'x2' ) + ), + array( + array( 'f', 'd', 'b', 'a', 'c', 'e' ), + 'alphabetic', + array( 'alphabetic' => array( 'c', 'a' ) ), + array( 'e' ), + array( 'e', 'c', 'a', 'b', 'd', 'f' ) + ), + 'Strict code order' => array( + array( 'f', 'd', 'b', 'a', 'c', 'e' ), + 'code', + array( 'alphabetic' => array( 'c', 'a' ) ), // this should be ignored + array( 'e' ), // prepend + array( 'e', 'a', 'b', 'c', 'd', 'f' ) + ), + 'Code w/o alphabetic' => array( + array( 'c', 'b', 'a' ), + 'code', + array(), + array(), + array( 'a', 'b', 'c' ) + ), + array( + array( 'a', 'b', 'k', 'x' ), + 'alphabetic', + array( 'alphabetic' => array( 'x', 'k', 'a' ) ), + array(), + array( 'x', 'k', 'a', 'b' ) + ), + 'Fall back to code order' => array( + array( 'b', 'a' ), + 'invalid', + array(), + array(), + array( 'a', 'b' ) + ) + ); + } + + /** + * @dataProvider sortLinksProvider + */ + public function testSortLinks( array $links, $sort, array $sortOrders, $sortPrepend, $expected ) { + $interwikiSorter = new InterwikiSorter( $sort, $sortOrders, $sortPrepend ); + $sortedLinks = $interwikiSorter->sortLinks( $links ); + $this->assertEquals( $expected, $sortedLinks ); + } + +} diff --git a/tests/phpunit/InterwikiSortingHooksTest.php b/tests/phpunit/InterwikiSortingHooksTest.php new file mode 100644 index 0000000..6bc79e1 --- /dev/null +++ b/tests/phpunit/InterwikiSortingHooksTest.php @@ -0,0 +1,39 @@ +assertContains( $initHook, Hooks::getHandlers( 'BeforeInitialize' ) ); + + // Fire the init hook which should register the second hook. + $initHook(); + + // Make sure that the hook has been registered and is at the end of the list. + $onContentAlterParserOutputHooks = Hooks::getHandlers( 'ContentAlterParserOutput' ); + $this->assertContains( $finalHook, $onContentAlterParserOutputHooks ); + $this->assertEquals( + $finalHook, + end( $onContentAlterParserOutputHooks ), + 'Hook should be the last to be fired' + ); + + } + +} \ No newline at end of file