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 cde24e2e62..37d113bba0 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 8f4aa3b521..0000000000 --- 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 eb09664a87..0000000000 --- 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 d16a4ff763..0000000000 --- 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 0ad7fe40c4..0000000000 --- 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 1a390cf1d6..0000000000 --- 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 95a00f5abf..a34634f251 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 2a167fe5ae..619cf389de 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 fdc988bd10..846deb95e1 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 0000000000..fa7edca386 --- /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 0000000000..ba8a233aab --- /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 65fe3d3e8f..7cda45fb28 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 19a1e89de6..f6bf7f1785 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 ad6a0d0ab1..d22c8d04cd 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' ); } ); -- 2.20.1