From 1c7c9bdf1fc5d57434aadeffff749807f042df24 Mon Sep 17 00:00:00 2001 From: Roan Kattouw Date: Mon, 5 Nov 2018 14:20:05 -0800 Subject: [PATCH] Use packageFiles feature to replace special-purpose RL modules For config vars, add a virtual package file called 'config.js', and use require( './config.js' ) in the module. For most data modules, add a virtual package file called 'data.js', use require( './data.js' ) in the module. Where needed, add wrapper files that put the data in the relevant global object and in module.exports. LanguageDataModule is the only special-purpose module not being removed in this commit, because it uses languageScripts, and those are not compatible with packageFiles (yet). Also merge mediawiki.ForeignStructuredUpload.config into mediawiki.ForeignStructuredUpload, since that was the only thing that used it. Change-Id: I203d4e3ecdeeeb16729eba2dcf40d11a41d2e582 --- autoload.php | 5 - .../ResourceLoaderJqueryMsgModule.php | 79 -------------- .../ResourceLoaderLanguageNamesModule.php | 77 ------------- .../ResourceLoaderMediaWikiUtilModule.php | 53 --------- ...sourceLoaderSpecialCharacterDataModule.php | 102 ------------------ .../ResourceLoaderUploadDialogModule.php | 49 --------- resources/Resources.php | 97 ++++++++++++++--- .../src/mediawiki.ForeignStructuredUpload.js | 2 +- .../mediawiki.jqueryMsg.js | 6 +- .../mediawiki.language.names.js | 4 + .../mediawiki.language.specialCharacters.js | 5 + resources/src/mediawiki.util.js | 22 +++- .../resourceloader/ResourceLoaderTest.php | 4 - .../mediawiki/mediawiki.util.test.js | 13 +-- 14 files changed, 126 insertions(+), 392 deletions(-) delete mode 100644 includes/resourceloader/ResourceLoaderJqueryMsgModule.php delete mode 100644 includes/resourceloader/ResourceLoaderLanguageNamesModule.php delete mode 100644 includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php delete mode 100644 includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php delete mode 100644 includes/resourceloader/ResourceLoaderUploadDialogModule.php create mode 100644 resources/src/mediawiki.language/mediawiki.language.names.js create mode 100644 resources/src/mediawiki.language/mediawiki.language.specialCharacters.js diff --git a/autoload.php b/autoload.php index cde24e2e62f..37d113bba01 100644 --- a/autoload.php +++ b/autoload.php @@ -1239,11 +1239,8 @@ $wgAutoloadLocalClasses = [ 'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php', 'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php', 'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php', - 'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php', 'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php', - 'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php', 'ResourceLoaderLessVarFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLessVarFileModule.php', - 'ResourceLoaderMediaWikiUtilModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php', 'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php', 'ResourceLoaderOOUIFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIFileModule.php', 'ResourceLoaderOOUIImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIImageModule.php', @@ -1251,9 +1248,7 @@ $wgAutoloadLocalClasses = [ 'ResourceLoaderSiteModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSiteModule.php', 'ResourceLoaderSiteStylesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSiteStylesModule.php', 'ResourceLoaderSkinModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSkinModule.php', - 'ResourceLoaderSpecialCharacterDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php', 'ResourceLoaderStartUpModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderStartUpModule.php', - 'ResourceLoaderUploadDialogModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUploadDialogModule.php', 'ResourceLoaderUserDefaultsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserDefaultsModule.php', 'ResourceLoaderUserModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserModule.php', 'ResourceLoaderUserOptionsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserOptionsModule.php', diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php deleted file mode 100644 index 8f4aa3b5210..00000000000 --- a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php +++ /dev/null @@ -1,79 +0,0 @@ - $this->getConfig()->get( 'Sitename' ), - ]; - Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] ); - - $parserDefaults = [ - 'allowedHtmlElements' => $allowedHtmlElements, - 'magic' => $magicWords, - ]; - - $setDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [ - $parserDefaults, - // Pass deep=true because mediawiki.jqueryMsg.js contains - // page-specific magic words that must not be overwritten. - true, - ] ); - - return $fileScript . $setDataScript; - } - - /** - * @param ResourceLoaderContext $context - * @return array - */ - public function getScriptURLsForDebug( ResourceLoaderContext $context ) { - // Bypass file module urls - return ResourceLoaderModule::getScriptURLsForDebug( $context ); - } - - /** - * @return bool - */ - public function enableModuleContentVersion() { - return true; - } -} diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php deleted file mode 100644 index eb09664a87c..00000000000 --- a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php +++ /dev/null @@ -1,77 +0,0 @@ -getLanguage(), - 'all' - ); - } - - /** - * @param ResourceLoaderContext $context - * @return string JavaScript code - */ - public function getScript( ResourceLoaderContext $context ) { - return Xml::encodeJsCall( - 'mw.language.setData', - [ - $context->getLanguage(), - 'languageNames', - $this->getData( $context ) - ], - ResourceLoader::inDebugMode() - ); - } - - /** - * @param ResourceLoaderContext|null $context - * @return array - */ - public function getDependencies( ResourceLoaderContext $context = null ) { - return [ 'mediawiki.language' ]; - } - - /** - * @return bool - */ - public function enableModuleContentVersion() { - return true; - } - -} diff --git a/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php b/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php deleted file mode 100644 index d16a4ff763a..00000000000 --- a/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php +++ /dev/null @@ -1,53 +0,0 @@ - $this->getConfig()->get( 'FragmentMode' ) ] - ) - . "\n" - . parent::getScript( $context ); - } - - /** - * @inheritDoc - */ - public function supportsURLLoading() { - return false; - } - - /** - * @inheritDoc - */ - public function enableModuleContentVersion() { - return true; - } -} diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php deleted file mode 100644 index 0ad7fe40c4a..00000000000 --- a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php +++ /dev/null @@ -1,102 +0,0 @@ -path}" ) ); - } - - /** - * @param ResourceLoaderContext $context - * @return string JavaScript code - */ - public function getScript( ResourceLoaderContext $context ) { - return Xml::encodeJsCall( - 'mw.language.setSpecialCharacters', - [ - $this->getData() - ], - ResourceLoader::inDebugMode() - ); - } - - /** - * @return bool - */ - public function enableModuleContentVersion() { - return true; - } - - /** - * @param ResourceLoaderContext|null $context - * @return array - */ - public function getDependencies( ResourceLoaderContext $context = null ) { - return [ 'mediawiki.language' ]; - } - - /** - * @return array - */ - public function getMessages() { - return [ - 'special-characters-group-latin', - 'special-characters-group-latinextended', - 'special-characters-group-ipa', - 'special-characters-group-symbols', - 'special-characters-group-greek', - 'special-characters-group-greekextended', - 'special-characters-group-cyrillic', - 'special-characters-group-arabic', - 'special-characters-group-arabicextended', - 'special-characters-group-persian', - 'special-characters-group-hebrew', - 'special-characters-group-bangla', - 'special-characters-group-tamil', - 'special-characters-group-telugu', - 'special-characters-group-sinhala', - 'special-characters-group-devanagari', - 'special-characters-group-gujarati', - 'special-characters-group-thai', - 'special-characters-group-lao', - 'special-characters-group-khmer', - 'special-characters-group-canadianaboriginal', - 'special-characters-title-endash', - 'special-characters-title-emdash', - 'special-characters-title-minus' - ]; - } -} diff --git a/includes/resourceloader/ResourceLoaderUploadDialogModule.php b/includes/resourceloader/ResourceLoaderUploadDialogModule.php deleted file mode 100644 index 1a390cf1d64..00000000000 --- a/includes/resourceloader/ResourceLoaderUploadDialogModule.php +++ /dev/null @@ -1,49 +0,0 @@ -getResourceLoader()->getConfig(); - return ResourceLoader::makeConfigSetScript( [ - 'wgUploadDialog' => $config->get( 'UploadDialog' ), - ] ); - } - - /** - * @return bool - */ - public function enableModuleContentVersion() { - return true; - } -} diff --git a/resources/Resources.php b/resources/Resources.php index 95a00f5abf7..a34634f251f 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -24,6 +24,8 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( 'Not an entry point.' ); } +global $wgResourceBasePath; + return [ /** @@ -1177,14 +1179,15 @@ return [ 'upload-foreign-cant-upload', ] ], - 'mediawiki.ForeignStructuredUpload.config' => [ - 'class' => ResourceLoaderUploadDialogModule::class, - ], 'mediawiki.ForeignStructuredUpload' => [ - 'scripts' => 'resources/src/mediawiki.ForeignStructuredUpload.js', + 'localBasePath' => "$IP/resources/src", + 'remoteBasePath' => "$wgResourceBasePath/resources/src", + 'packageFiles' => [ + 'mediawiki.ForeignStructuredUpload.js', + 'config.json' => [ 'config' => [ 'UploadDialog' ] ], + ], 'dependencies' => [ 'mediawiki.ForeignUpload', - 'mediawiki.ForeignStructuredUpload.config', ], 'messages' => [ 'upload-foreign-cant-load-config', @@ -1327,8 +1330,12 @@ return [ ] ], 'mediawiki.util' => [ - 'class' => ResourceLoaderMediaWikiUtilModule::class, - 'scripts' => 'resources/src/mediawiki.util.js', + 'localBasePath' => "$IP/resources/src", + 'remoteBasePath' => "$wgResourceBasePath/resources/src", + 'packageFiles' => [ + 'mediawiki.util.js', + 'config.json' => [ 'config' => [ 'FragmentMode' ] ], + ], 'dependencies' => [ 'jquery.accessKeyLabel', 'mediawiki.RegExp', @@ -1570,9 +1577,31 @@ return [ ], 'mediawiki.jqueryMsg' => [ - // Add data for mediawiki.jqueryMsg, such as allowed tags - 'class' => ResourceLoaderJqueryMsgModule::class, - 'scripts' => 'resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js', + 'localBasePath' => "$IP/resources/src/mediawiki.jqueryMsg", + 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.jqueryMsg", + 'packageFiles' => [ + 'mediawiki.jqueryMsg.js', + 'parserDefaults.json' => [ 'callback' => function ( ResourceLoaderContext $context ) { + $tagData = Sanitizer::getRecognizedTagData(); + $allowedHtmlElements = array_merge( + array_keys( $tagData['htmlpairs'] ), + array_diff( + array_keys( $tagData['htmlsingle'] ), + array_keys( $tagData['htmlsingleonly'] ) + ) + ); + + $magicWords = [ + 'SITENAME' => $context->getConfig()->get( 'Sitename' ), + ]; + Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] ); + + return [ + 'allowedHtmlElements' => $allowedHtmlElements, + 'magic' => $magicWords, + ]; + } ], + ], 'dependencies' => [ 'mediawiki.util', 'mediawiki.language', @@ -1592,10 +1621,54 @@ return [ ) ], - 'mediawiki.language.names' => [ 'class' => ResourceLoaderLanguageNamesModule::class ], + 'mediawiki.language.names' => [ + 'localBasePath' => "$IP/resources/src/mediawiki.language", + 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.language", + 'packageFiles' => [ + 'mediawiki.language.names.js', + 'names.json' => [ 'callback' => function ( ResourceLoaderContext $context ) { + return Language::fetchLanguageNames( $context->getLanguage(), 'all' ); + } ], + ], + 'dependencies' => 'mediawiki.language', + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.language.specialCharacters' => [ - 'class' => ResourceLoaderSpecialCharacterDataModule::class + 'localBasePath' => "$IP/resources/src/mediawiki.language", + 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.language", + 'packageFiles' => [ + 'mediawiki.language.specialCharacters.js', + 'specialcharacters.json' + ], + 'dependencies' => 'mediawiki.language', + 'targets' => [ 'desktop', 'mobile' ], + 'messages' => [ + 'special-characters-group-latin', + 'special-characters-group-latinextended', + 'special-characters-group-ipa', + 'special-characters-group-symbols', + 'special-characters-group-greek', + 'special-characters-group-greekextended', + 'special-characters-group-cyrillic', + 'special-characters-group-arabic', + 'special-characters-group-arabicextended', + 'special-characters-group-persian', + 'special-characters-group-hebrew', + 'special-characters-group-bangla', + 'special-characters-group-tamil', + 'special-characters-group-telugu', + 'special-characters-group-sinhala', + 'special-characters-group-devanagari', + 'special-characters-group-gujarati', + 'special-characters-group-thai', + 'special-characters-group-lao', + 'special-characters-group-khmer', + 'special-characters-group-canadianaboriginal', + 'special-characters-title-endash', + 'special-characters-title-emdash', + 'special-characters-title-minus' + ] ], /* MediaWiki Libs */ diff --git a/resources/src/mediawiki.ForeignStructuredUpload.js b/resources/src/mediawiki.ForeignStructuredUpload.js index 2a167fe5aec..619cf389dee 100644 --- a/resources/src/mediawiki.ForeignStructuredUpload.js +++ b/resources/src/mediawiki.ForeignStructuredUpload.js @@ -25,7 +25,7 @@ // Config for uploads to local wiki. // Can be overridden with foreign wiki config when #loadConfig is called. - this.config = mw.config.get( 'wgUploadDialog' ); + this.config = require( './config.json' ).UploadDialog; mw.ForeignUpload.call( this, target, apiconfig ); } diff --git a/resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js b/resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js index fdc988bd10e..846deb95e1d 100644 --- a/resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js +++ b/resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js @@ -14,13 +14,14 @@ var oldParser, slice = Array.prototype.slice, parserDefaults = { + // Magic words and their expansions. Server-side data is added to this below. magic: { PAGENAME: mw.config.get( 'wgPageName' ), PAGENAMEE: mw.util.wikiUrlencode( mw.config.get( 'wgPageName' ) ) }, // Whitelist for allowed HTML elements in wikitext. // Self-closing tags are not currently supported. - // Can be populated via setParserDefaults(). + // Filled in with server-side data below allowedHtmlElements: [], // Key tag name, value allowed attributes for that tag. // See Sanitizer::setupAttributeWhitelist @@ -56,6 +57,9 @@ format: 'parse' }; + // Add in server-side data (allowedHtmlElements and magic words) + $.extend( true, parserDefaults, require( './parserDefaults.json' ) ); + /** * Wrapper around jQuery append that converts all non-objects to TextNode so append will not * convert what it detects as an htmlString to an element. diff --git a/resources/src/mediawiki.language/mediawiki.language.names.js b/resources/src/mediawiki.language/mediawiki.language.names.js new file mode 100644 index 00000000000..fa7edca3863 --- /dev/null +++ b/resources/src/mediawiki.language/mediawiki.language.names.js @@ -0,0 +1,4 @@ +( function () { + var names = require( './names.json' ); + mw.language.setData( mw.config.get( 'wgUserLanguage' ), 'languageData', names ); +}() ); diff --git a/resources/src/mediawiki.language/mediawiki.language.specialCharacters.js b/resources/src/mediawiki.language/mediawiki.language.specialCharacters.js new file mode 100644 index 00000000000..ba8a233aab7 --- /dev/null +++ b/resources/src/mediawiki.language/mediawiki.language.specialCharacters.js @@ -0,0 +1,5 @@ +( function () { + var specialCharacters = require( './specialcharacters.json' ); + mw.language.setSpecialCharacters( specialCharacters ); + module.exports = specialCharacters; +}() ); diff --git a/resources/src/mediawiki.util.js b/resources/src/mediawiki.util.js index 65fe3d3e8fa..7cda45fb288 100644 --- a/resources/src/mediawiki.util.js +++ b/resources/src/mediawiki.util.js @@ -1,7 +1,9 @@ ( function () { 'use strict'; - var util; + var util, + config = require( './config.json' ), + origConfig = config; /** * Encode the string like PHP's rawurlencode @@ -50,6 +52,20 @@ /* Main body */ + setOptionsForTest: function ( opts ) { + if ( !window.QUnit ) { + throw new Error( 'Modifying options not allowed outside unit tests' ); + } + config = $.extend( {}, config, opts ); + }, + + resetOptionsForTest: function () { + if ( !window.QUnit ) { + throw new Error( 'Resetting options not allowed outside unit tests' ); + } + config = origConfig; + }, + /** * Encode the string like PHP's rawurlencode * @@ -68,7 +84,7 @@ * @return {string} Encoded string */ escapeIdForAttribute: function ( str ) { - var mode = mw.config.get( 'wgFragmentMode' )[ 0 ]; + var mode = config.FragmentMode[ 0 ]; return escapeIdInternal( str, mode ); }, @@ -83,7 +99,7 @@ * @return {string} Encoded string */ escapeIdForLink: function ( str ) { - var mode = mw.config.get( 'wgFragmentMode' )[ 0 ]; + var mode = config.FragmentMode[ 0 ]; return escapeIdInternal( str, mode ); }, diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php index 19a1e89de68..f6bf7f17856 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php @@ -148,10 +148,6 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { 'SkinModule (FileModule subclass)' => [ true, [ 'class' => ResourceLoaderSkinModule::class, 'scripts' => 'example.js' ] ], - 'JqueryMsgModule (FileModule subclass)' => [ true, [ - 'class' => ResourceLoaderJqueryMsgModule::class, - 'scripts' => 'example.js', - ] ], 'WikiModule' => [ false, [ 'class' => ResourceLoaderWikiModule::class, 'scripts' => [ 'MediaWiki:Example.js' ], diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js index ad6a0d0ab1c..d22c8d04cd8 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js @@ -80,6 +80,7 @@ }, teardown: function () { $.fn.updateTooltipAccessKeys.setTestMode( false ); + mw.util.resetOptionsForTest(); }, messages: { // Used by accessKeyLabel in test for addPortletLink @@ -114,7 +115,7 @@ // Distant future: no legacy fallbacks [ allNew, text, html5Encoded ] ].forEach( function ( testCase ) { - mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); + mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } ); assert.strictEqual( util.escapeIdForAttribute( testCase[ 1 ] ), testCase[ 2 ] ); } ); @@ -141,7 +142,7 @@ // Distant future: no legacy fallbacks [ allNew, text, html5Encoded ] ].forEach( function ( testCase ) { - mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); + mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } ); assert.strictEqual( util.escapeIdForLink( testCase[ 1 ] ), testCase[ 2 ] ); } ); @@ -210,22 +211,22 @@ href = util.getUrl( '#Fragment', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#Fragment', 'empty title with query string and fragment' ); - mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } ); href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_.C3.84', 'title with query string, fragment, and special characters' ); - mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } ); href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_Ä', 'title with query string, fragment, and special characters' ); href = util.getUrl( 'Foo:%23#Fragment', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?title=Foo:%2523&action=edit#Fragment', 'title containing %23 (#), fragment, and a query string' ); - mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } ); href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#.2B.26.3D:.3B.40.24-_..21.2A.2F.5B.5D.3C.3E.27.C2.A7', 'fragment with various characters' ); - mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } ); href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); assert.strictEqual( href, '/w/index.php?action=edit#+&=:;@$-_.!*/[]<>\'§', 'fragment with various characters' ); } );