From f19c9021a466f414065db6f74257efc702325110 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Mon, 19 Feb 2018 21:23:36 +0100 Subject: [PATCH] Move $.byteLength and $.trimByteLength to new module 'mediawiki.String' These methods do not belong on the jQuery object. And to resolve T185948, we need to also add codePointLength and trimCodePointLength, and this new module seems like a good place to put them. There is no `mw.String` global, this module has to be used via `require()`. Deprecations: * Function `$.byteLength` (from module 'jquery.byteLength') is deprecated, use `require( 'mediawiki.String' ).byteLength` instead. * Function `$.trimByteLength` (from module 'jquery.byteLimit') is deprecated, use `require( 'mediawiki.String' ).trimByteLength` instead. * Module 'jquery.byteLength' is deprecated, use 'mediawiki.String' instead. Note that `$.fn.byteLimit` and the 'jquery.byteLimit' module are not deprecated. Change-Id: I2501a79efee644e5f4a9f5c977fe49c8c05c6eb3 --- maintenance/jsduck/categories.json | 1 + resources/Resources.php | 20 ++- resources/src/jquery/jquery.byteLength.js | 27 +-- resources/src/jquery/jquery.byteLimit.js | 129 +++------------ .../mw.rcfilters.Controller.js | 5 +- .../mediawiki.widgets.visibleByteLimit.js | 50 +++--- .../mw.widgets.TitleInputWidget.js | 4 +- resources/src/mediawiki/mediawiki.String.js | 156 ++++++++++++++++++ resources/src/mediawiki/mediawiki.Title.js | 6 +- resources/src/mediawiki/mediawiki.inspect.js | 9 +- tests/qunit/QUnitTestResources.php | 5 +- .../jquery/jquery.byteLength.test.js | 37 ----- .../mediawiki.String.byteLength.test.js | 39 +++++ .../mediawiki.String.trimByteLength.test.js | 150 +++++++++++++++++ 14 files changed, 430 insertions(+), 208 deletions(-) create mode 100644 resources/src/mediawiki/mediawiki.String.js delete mode 100644 tests/qunit/suites/resources/jquery/jquery.byteLength.test.js create mode 100644 tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js create mode 100644 tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index 66e8d01fcb..bebee85f3f 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -23,6 +23,7 @@ "mw.Title", "mw.Uri", "mw.RegExp", + "mw.String", "mw.messagePoster.*", "mw.notification", "mw.Notification_", diff --git a/resources/Resources.php b/resources/Resources.php index 4d89f87199..24654419b0 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -156,11 +156,13 @@ return [ ], 'jquery.byteLength' => [ 'scripts' => 'resources/src/jquery/jquery.byteLength.js', + 'deprecated' => 'Use "mediawiki.String" instead.', + 'dependencies' => 'mediawiki.String', 'targets' => [ 'desktop', 'mobile' ], ], 'jquery.byteLimit' => [ 'scripts' => 'resources/src/jquery/jquery.byteLimit.js', - 'dependencies' => 'jquery.byteLength', + 'dependencies' => 'mediawiki.String', 'targets' => [ 'desktop', 'mobile' ], ], 'jquery.checkboxShiftClick' => [ @@ -1105,7 +1107,7 @@ return [ 'mediawiki.inspect' => [ 'scripts' => 'resources/src/mediawiki/mediawiki.inspect.js', 'dependencies' => [ - 'jquery.byteLength', + 'mediawiki.String', 'mediawiki.RegExp', ], 'targets' => [ 'desktop', 'mobile' ], @@ -1169,6 +1171,10 @@ return [ 'scripts' => 'resources/src/mediawiki/mediawiki.RegExp.js', 'targets' => [ 'desktop', 'mobile' ], ], + 'mediawiki.String' => [ + 'scripts' => 'resources/src/mediawiki/mediawiki.String.js', + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.pager.tablePager' => [ 'styles' => 'resources/src/mediawiki/mediawiki.pager.tablePager.less', ], @@ -1201,8 +1207,7 @@ return [ 'resources/src/mediawiki/mediawiki.Title.phpCharToUpper.js', ], 'dependencies' => [ - 'jquery.byteLength', - 'jquery.byteLimit', + 'mediawiki.String', 'mediawiki.util', ], 'targets' => [ 'desktop', 'mobile' ], @@ -1762,7 +1767,7 @@ return [ 'resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js', ], 'dependencies' => [ - 'jquery.byteLength', + 'mediawiki.String', 'oojs', 'mediawiki.api', 'mediawiki.api.options', @@ -2421,7 +2426,7 @@ return [ // TitleInputWidget 'mediawiki.Title', 'mediawiki.api', - 'jquery.byteLimit', + 'mediawiki.String', ], 'messages' => [ // NamespaceInputWidget @@ -2480,7 +2485,8 @@ return [ ], 'dependencies' => [ 'oojs-ui-core', - 'jquery.byteLimit' + 'jquery.byteLimit', + 'mediawiki.String', ], 'targets' => [ 'desktop', 'mobile' ] ], diff --git a/resources/src/jquery/jquery.byteLength.js b/resources/src/jquery/jquery.byteLength.js index 222f14afd8..5764ae90ad 100644 --- a/resources/src/jquery/jquery.byteLength.js +++ b/resources/src/jquery/jquery.byteLength.js @@ -1,38 +1,19 @@ /** * @class jQuery.plugin.byteLength - * @author Jan Paul Posma, 2011 - * @author Timo Tijhof, 2012 - * @author David Chan, 2013 */ /** * Calculate the byte length of a string (accounting for UTF-8). * + * @method byteLength + * @deprecated Use `require( 'mediawiki.String' ).byteLength` instead. * @static * @inheritable * @param {string} str * @return {number} */ -jQuery.byteLength = function ( str ) { - // This basically figures out how many bytes a UTF-16 string (which is what js sees) - // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that. - // Note, surrogate (\uD800-\uDFFF) characters are counted as 2 bytes, since there's two of them - // and the actual character takes 4 bytes in UTF-8 (2*2=4). Might not work perfectly in - // edge cases such as illegal sequences, but that should never happen. - - // https://en.wikipedia.org/wiki/UTF-8#Description - // The mapping from UTF-16 code units to UTF-8 bytes is as follows: - // > Range 0000-007F: codepoints that become 1 byte of UTF-8 - // > Range 0080-07FF: codepoints that become 2 bytes of UTF-8 - // > Range 0800-D7FF: codepoints that become 3 bytes of UTF-8 - // > Range D800-DFFF: Surrogates (each pair becomes 4 bytes of UTF-8) - // > Range E000-FFFF: codepoints that become 3 bytes of UTF-8 (continued) - - return str - .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' ) - .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' ) - .length; -}; +mediaWiki.log.deprecate( jQuery, 'byteLength', require( 'mediawiki.String' ).byteLength, + 'Use require( \'mediawiki.String\' ).byteLength instead.', '$.byteLength' ); /** * @class jQuery diff --git a/resources/src/jquery/jquery.byteLimit.js b/resources/src/jquery/jquery.byteLimit.js index 3ce6e7fca5..eb21846935 100644 --- a/resources/src/jquery/jquery.byteLimit.js +++ b/resources/src/jquery/jquery.byteLimit.js @@ -1,32 +1,20 @@ /** * @class jQuery.plugin.byteLimit */ -( function ( $ ) { - - var eventKeys = [ - 'keyup.byteLimit', - 'keydown.byteLimit', - 'change.byteLimit', - 'mouseup.byteLimit', - 'cut.byteLimit', - 'paste.byteLimit', - 'focus.byteLimit', - 'blur.byteLimit' - ].join( ' ' ); - - // Like String#charAt, but return the pair of UTF-16 surrogates for characters outside of BMP. - function codePointAt( string, offset, backwards ) { - // We don't need to check for offsets at the beginning or end of string, - // String#slice will simply return a shorter (or empty) substring. - var maybePair = backwards ? - string.slice( offset - 1, offset + 1 ) : - string.slice( offset, offset + 2 ); - if ( /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( maybePair ) ) { - return maybePair; - } else { - return string.charAt( offset ); - } - } +( function ( $, mw ) { + + var + eventKeys = [ + 'keyup.byteLimit', + 'keydown.byteLimit', + 'change.byteLimit', + 'mouseup.byteLimit', + 'cut.byteLimit', + 'paste.byteLimit', + 'focus.byteLimit', + 'blur.byteLimit' + ].join( ' ' ), + trimByteLength = require( 'mediawiki.String' ).trimByteLength; /** * Utility function to trim down a string, based on byteLimit @@ -35,6 +23,8 @@ * "fobo", not "foba". Basically emulating the native maxlength by * reconstructing where the insertion occurred. * + * @method trimByteLength + * @deprecated Use `require( 'mediawiki.String' ).trimByteLength` instead. * @static * @param {string} safeVal Known value that was previously returned by this * function, if none, pass empty string. @@ -45,87 +35,8 @@ * @return {string} return.newVal * @return {boolean} return.trimmed */ - $.trimByteLength = function ( safeVal, newVal, byteLimit, fn ) { - var startMatches, endMatches, matchesLen, inpParts, chopOff, oldChar, newChar, - oldVal = safeVal; - - // Run the hook if one was provided, but only on the length - // assessment. The value itself is not to be affected by the hook. - if ( $.byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) { - // Limit was not reached, just remember the new value - // and let the user continue. - return { - newVal: newVal, - trimmed: false - }; - } - - // Current input is longer than the active limit. - // Figure out what was added and limit the addition. - startMatches = 0; - endMatches = 0; - - // It is important that we keep the search within the range of - // the shortest string's length. - // Imagine a user adds text that matches the end of the old value - // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without - // limiting both searches to the shortest length, endMatches would - // also be 3. - matchesLen = Math.min( newVal.length, oldVal.length ); - - // Count same characters from the left, first. - // (if "foo" -> "foofoo", assume addition was at the end). - while ( startMatches < matchesLen ) { - oldChar = codePointAt( oldVal, startMatches, false ); - newChar = codePointAt( newVal, startMatches, false ); - if ( oldChar !== newChar ) { - break; - } - startMatches += oldChar.length; - } - - while ( endMatches < ( matchesLen - startMatches ) ) { - oldChar = codePointAt( oldVal, oldVal.length - 1 - endMatches, true ); - newChar = codePointAt( newVal, newVal.length - 1 - endMatches, true ); - if ( oldChar !== newChar ) { - break; - } - endMatches += oldChar.length; - } - - inpParts = [ - // Same start - newVal.slice( 0, startMatches ), - // Inserted content - newVal.slice( startMatches, newVal.length - endMatches ), - // Same end - newVal.slice( newVal.length - endMatches ) - ]; - - // Chop off characters from the end of the "inserted content" string - // until the limit is statisfied. - if ( fn ) { - // stop, when there is nothing to slice - T43450 - while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[ 1 ].length > 0 ) { - // Do not chop off halves of surrogate pairs - chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1; - inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff ); - } - } else { - while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) { - // Do not chop off halves of surrogate pairs - chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1; - inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff ); - } - } - - return { - newVal: inpParts.join( '' ), - // For pathological fn() that always returns a value longer than the limit, we might have - // ended up not trimming - check for this case to avoid infinite loops - trimmed: newVal !== inpParts.join( '' ) - }; - }; + mw.log.deprecate( $, 'trimByteLength', trimByteLength, + 'Use require( \'mediawiki.String\' ).trimByteLength instead.', '$.trimByteLength' ); /** * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well, @@ -228,7 +139,7 @@ // See https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for // the order and characteristics of the key events. $el.on( eventKeys, function () { - var res = $.trimByteLength( + var res = trimByteLength( prevSafeVal, this.value, elLimit, @@ -257,4 +168,4 @@ * @class jQuery * @mixins jQuery.plugin.byteLimit */ -}( jQuery ) ); +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 4b78175064..dcce92d6bd 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -1,4 +1,7 @@ ( function ( mw, $ ) { + + var byteLength = require( 'mediawiki.String' ).byteLength; + /* eslint no-underscore-dangle: "off" */ /** * Controller for the filters in Recent Changes @@ -804,7 +807,7 @@ // Stringify state stringified = JSON.stringify( state ); - if ( $.byteLength( stringified ) > 65535 ) { + if ( byteLength( stringified ) > 65535 ) { // Sanity check, since the preference can only hold that. return; } diff --git a/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js b/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js index a810c9834b..03ffca7b95 100644 --- a/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js +++ b/resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js @@ -1,25 +1,31 @@ -/** - * @class mw.widgets - */ +( function ( mw ) { -/** - * Add a visible byte limit label to a TextInputWidget. - * - * Uses jQuery#byteLimit to enforce the limit. - * - * @param {OO.ui.TextInputWidget} textInputWidget Text input widget - * @param {number} [limit] Byte limit, defaults to $input's maxlength - */ -mediaWiki.widgets.visibleByteLimit = function ( textInputWidget, limit ) { - limit = limit || +textInputWidget.$input.attr( 'maxlength' ); + var byteLength = require( 'mediawiki.String' ).byteLength; - function updateCount() { - textInputWidget.setLabel( ( limit - $.byteLength( textInputWidget.getValue() ) ).toString() ); - } - textInputWidget.on( 'change', updateCount ); - // Initialise value - updateCount(); + /** + * @class mw.widgets + */ - // Actually enforce limit - textInputWidget.$input.byteLimit( limit ); -}; + /** + * Add a visible byte limit label to a TextInputWidget. + * + * Uses jQuery#byteLimit to enforce the limit. + * + * @param {OO.ui.TextInputWidget} textInputWidget Text input widget + * @param {number} [limit] Byte limit, defaults to $input's maxlength + */ + mw.widgets.visibleByteLimit = function ( textInputWidget, limit ) { + limit = limit || +textInputWidget.$input.attr( 'maxlength' ); + + function updateCount() { + textInputWidget.setLabel( ( limit - byteLength( textInputWidget.getValue() ) ).toString() ); + } + textInputWidget.on( 'change', updateCount ); + // Initialise value + updateCount(); + + // Actually enforce limit + textInputWidget.$input.byteLimit( limit ); + }; + +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js index 98d07f3bb7..4b1109b491 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js @@ -6,6 +6,8 @@ */ ( function ( $, mw ) { + var trimByteLength = require( 'mediawiki.String' ).trimByteLength; + /** * Creates an mw.widgets.TitleInputWidget object. * @@ -130,7 +132,7 @@ // Parent method value = mw.widgets.TitleInputWidget.parent.prototype.cleanUpValue.call( this, value ); - return $.trimByteLength( this.value, value, this.maxLength, function ( value ) { + return trimByteLength( this.value, value, this.maxLength, function ( value ) { var title = widget.getMWTitle( value ); return title ? title.getMain() : value; } ).newVal; diff --git a/resources/src/mediawiki/mediawiki.String.js b/resources/src/mediawiki/mediawiki.String.js new file mode 100644 index 0000000000..5e11680543 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.String.js @@ -0,0 +1,156 @@ +( function () { + + /** + * @class mw.String + * @singleton + */ + + /** + * Calculate the byte length of a string (accounting for UTF-8). + * + * @author Jan Paul Posma, 2011 + * @author Timo Tijhof, 2012 + * @author David Chan, 2013 + * + * @param {string} str + * @return {number} + */ + function byteLength( str ) { + // This basically figures out how many bytes a UTF-16 string (which is what js sees) + // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that. + // Note, surrogate (\uD800-\uDFFF) characters are counted as 2 bytes, since there's two of them + // and the actual character takes 4 bytes in UTF-8 (2*2=4). Might not work perfectly in + // edge cases such as illegal sequences, but that should never happen. + + // https://en.wikipedia.org/wiki/UTF-8#Description + // The mapping from UTF-16 code units to UTF-8 bytes is as follows: + // > Range 0000-007F: codepoints that become 1 byte of UTF-8 + // > Range 0080-07FF: codepoints that become 2 bytes of UTF-8 + // > Range 0800-D7FF: codepoints that become 3 bytes of UTF-8 + // > Range D800-DFFF: Surrogates (each pair becomes 4 bytes of UTF-8) + // > Range E000-FFFF: codepoints that become 3 bytes of UTF-8 (continued) + + return str + .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' ) + .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' ) + .length; + } + + // Like String#charAt, but return the pair of UTF-16 surrogates for characters outside of BMP. + function codePointAt( string, offset, backwards ) { + // We don't need to check for offsets at the beginning or end of string, + // String#slice will simply return a shorter (or empty) substring. + var maybePair = backwards ? + string.slice( offset - 1, offset + 1 ) : + string.slice( offset, offset + 2 ); + if ( /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( maybePair ) ) { + return maybePair; + } else { + return string.charAt( offset ); + } + } + + /** + * Utility function to trim down a string, based on byteLimit + * and given a safe start position. It supports insertion anywhere + * in the string, so "foo" to "fobaro" if limit is 4 will result in + * "fobo", not "foba". Basically emulating the native maxlength by + * reconstructing where the insertion occurred. + * + * @param {string} safeVal Known value that was previously returned by this + * function, if none, pass empty string. + * @param {string} newVal New value that may have to be trimmed down. + * @param {number} byteLimit Number of bytes the value may be in size. + * @param {Function} [fn] Function to call on the string before assessing the length. + * @return {Object} + * @return {string} return.newVal + * @return {boolean} return.trimmed + */ + function trimByteLength( safeVal, newVal, byteLimit, fn ) { + var startMatches, endMatches, matchesLen, inpParts, chopOff, oldChar, newChar, + oldVal = safeVal; + + // Run the hook if one was provided, but only on the length + // assessment. The value itself is not to be affected by the hook. + if ( byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) { + // Limit was not reached, just remember the new value + // and let the user continue. + return { + newVal: newVal, + trimmed: false + }; + } + + // Current input is longer than the active limit. + // Figure out what was added and limit the addition. + startMatches = 0; + endMatches = 0; + + // It is important that we keep the search within the range of + // the shortest string's length. + // Imagine a user adds text that matches the end of the old value + // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without + // limiting both searches to the shortest length, endMatches would + // also be 3. + matchesLen = Math.min( newVal.length, oldVal.length ); + + // Count same characters from the left, first. + // (if "foo" -> "foofoo", assume addition was at the end). + while ( startMatches < matchesLen ) { + oldChar = codePointAt( oldVal, startMatches, false ); + newChar = codePointAt( newVal, startMatches, false ); + if ( oldChar !== newChar ) { + break; + } + startMatches += oldChar.length; + } + + while ( endMatches < ( matchesLen - startMatches ) ) { + oldChar = codePointAt( oldVal, oldVal.length - 1 - endMatches, true ); + newChar = codePointAt( newVal, newVal.length - 1 - endMatches, true ); + if ( oldChar !== newChar ) { + break; + } + endMatches += oldChar.length; + } + + inpParts = [ + // Same start + newVal.slice( 0, startMatches ), + // Inserted content + newVal.slice( startMatches, newVal.length - endMatches ), + // Same end + newVal.slice( newVal.length - endMatches ) + ]; + + // Chop off characters from the end of the "inserted content" string + // until the limit is statisfied. + if ( fn ) { + // stop, when there is nothing to slice - T43450 + while ( byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[ 1 ].length > 0 ) { + // Do not chop off halves of surrogate pairs + chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1; + inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff ); + } + } else { + while ( byteLength( inpParts.join( '' ) ) > byteLimit ) { + // Do not chop off halves of surrogate pairs + chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1; + inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff ); + } + } + + return { + newVal: inpParts.join( '' ), + // For pathological fn() that always returns a value longer than the limit, we might have + // ended up not trimming - check for this case to avoid infinite loops + trimmed: newVal !== inpParts.join( '' ) + }; + } + + module.exports = { + byteLength: byteLength, + trimByteLength: trimByteLength + }; + +}() ); diff --git a/resources/src/mediawiki/mediawiki.Title.js b/resources/src/mediawiki/mediawiki.Title.js index 6a4ebb1f14..2b76187359 100644 --- a/resources/src/mediawiki/mediawiki.Title.js +++ b/resources/src/mediawiki/mediawiki.Title.js @@ -32,6 +32,8 @@ /* Private members */ var + mwString = require( 'mediawiki.String' ), + namespaceIds = mw.config.get( 'wgNamespaceIds' ), /** @@ -320,7 +322,7 @@ // Except for special pages, e.g. [[Special:Block/Long name]] // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should // be less than 512 bytes. - if ( namespace !== NS_SPECIAL && $.byteLength( title ) > TITLE_MAX_BYTES ) { + if ( namespace !== NS_SPECIAL && mwString.byteLength( title ) > TITLE_MAX_BYTES ) { return false; } @@ -407,7 +409,7 @@ * @return {string} */ trimToByteLength = function ( s, length ) { - return $.trimByteLength( '', s, length ).newVal; + return mwString.trimByteLength( '', s, length ).newVal; }, /** diff --git a/resources/src/mediawiki/mediawiki.inspect.js b/resources/src/mediawiki/mediawiki.inspect.js index f91ffbb41b..6478fd96d3 100644 --- a/resources/src/mediawiki/mediawiki.inspect.js +++ b/resources/src/mediawiki/mediawiki.inspect.js @@ -10,6 +10,7 @@ ( function ( mw, $ ) { var inspect, + byteLength = require( 'mediawiki.String' ).byteLength, hasOwn = Object.prototype.hasOwnProperty; function sortByProperty( array, prop, descending ) { @@ -117,9 +118,9 @@ size = 0; for ( i = 0; i < args.length; i++ ) { if ( typeof args[ i ] === 'function' ) { - size += $.byteLength( getFunctionBody( args[ i ] ) ); + size += byteLength( getFunctionBody( args[ i ] ) ); } else { - size += $.byteLength( JSON.stringify( args[ i ] ) ); + size += byteLength( JSON.stringify( args[ i ] ) ); } } @@ -285,8 +286,8 @@ $.extend( stats, mw.loader.store.stats ); try { raw = localStorage.getItem( mw.loader.store.getStoreKey() ); - stats.totalSizeInBytes = $.byteLength( raw ); - stats.totalSize = humanSize( $.byteLength( raw ) ); + stats.totalSizeInBytes = byteLength( raw ); + stats.totalSize = humanSize( byteLength( raw ) ); } catch ( e ) {} } return [ stats ]; diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 8390ab3c58..3372bf01fc 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -45,7 +45,6 @@ return [ 'scripts' => [ 'tests/qunit/suites/resources/startup.test.js', 'tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js', - 'tests/qunit/suites/resources/jquery/jquery.byteLength.test.js', 'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.color.test.js', 'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js', @@ -65,6 +64,8 @@ return [ 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.mustache.test.js', @@ -102,7 +103,6 @@ return [ ], 'dependencies' => [ 'jquery.accessKeyLabel', - 'jquery.byteLength', 'jquery.byteLimit', 'jquery.color', 'jquery.colorUtil', @@ -125,6 +125,7 @@ return [ 'mediawiki.jqueryMsg', 'mediawiki.messagePoster', 'mediawiki.RegExp', + 'mediawiki.String', 'mediawiki.storage', 'mediawiki.Title', 'mediawiki.toc', diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js deleted file mode 100644 index 558e64161d..0000000000 --- a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js +++ /dev/null @@ -1,37 +0,0 @@ -( function ( $ ) { - QUnit.module( 'jquery.byteLength', QUnit.newMwEnvironment() ); - - QUnit.test( 'Simple text', function ( assert ) { - var azLc = 'abcdefghijklmnopqrstuvwxyz', - azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - num = '0123456789', - x = '*', - space = ' '; - - assert.equal( $.byteLength( azLc ), 26, 'Lowercase a-z' ); - assert.equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' ); - assert.equal( $.byteLength( num ), 10, 'Numbers 0-9' ); - assert.equal( $.byteLength( x ), 1, 'An asterisk' ); - assert.equal( $.byteLength( space ), 3, '3 spaces' ); - - } ); - - QUnit.test( 'Special text', function ( assert ) { - // https://en.wikipedia.org/wiki/UTF-8 - var u0024 = '$', - // Cent symbol - u00A2 = '\u00A2', - // Euro symbol - u20AC = '\u20AC', - // Character \U00024B62 (Han script) can't be represented in javascript as a single - // code point, instead it is composed as a surrogate pair of two separate code units. - // http://codepoints.net/U+24B62 - // http://www.fileformat.info/info/unicode/char/24B62/index.htm - u024B62 = '\uD852\uDF62'; - - assert.strictEqual( $.byteLength( u0024 ), 1, 'U+0024' ); - assert.strictEqual( $.byteLength( u00A2 ), 2, 'U+00A2' ); - assert.strictEqual( $.byteLength( u20AC ), 3, 'U+20AC' ); - assert.strictEqual( $.byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' ); - } ); -}( jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js new file mode 100644 index 0000000000..ae3ebbf742 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js @@ -0,0 +1,39 @@ +( function () { + var byteLength = require( 'mediawiki.String' ).byteLength; + + QUnit.module( 'mediawiki.String.byteLength', QUnit.newMwEnvironment() ); + + QUnit.test( 'Simple text', function ( assert ) { + var azLc = 'abcdefghijklmnopqrstuvwxyz', + azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + num = '0123456789', + x = '*', + space = ' '; + + assert.equal( byteLength( azLc ), 26, 'Lowercase a-z' ); + assert.equal( byteLength( azUc ), 26, 'Uppercase A-Z' ); + assert.equal( byteLength( num ), 10, 'Numbers 0-9' ); + assert.equal( byteLength( x ), 1, 'An asterisk' ); + assert.equal( byteLength( space ), 3, '3 spaces' ); + + } ); + + QUnit.test( 'Special text', function ( assert ) { + // https://en.wikipedia.org/wiki/UTF-8 + var u0024 = '$', + // Cent symbol + u00A2 = '\u00A2', + // Euro symbol + u20AC = '\u20AC', + // Character \U00024B62 (Han script) can't be represented in javascript as a single + // code point, instead it is composed as a surrogate pair of two separate code units. + // http://codepoints.net/U+24B62 + // http://www.fileformat.info/info/unicode/char/24B62/index.htm + u024B62 = '\uD852\uDF62'; + + assert.strictEqual( byteLength( u0024 ), 1, 'U+0024' ); + assert.strictEqual( byteLength( u00A2 ), 2, 'U+00A2' ); + assert.strictEqual( byteLength( u20AC ), 3, 'U+20AC' ); + assert.strictEqual( byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' ); + } ); +}() ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js new file mode 100644 index 0000000000..e2eea94e84 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js @@ -0,0 +1,150 @@ +( function ( $, mw ) { + var simpleSample, U_20AC, poop, mbSample, + trimByteLength = require( 'mediawiki.String' ).trimByteLength; + + QUnit.module( 'mediawiki.String.trimByteLength', QUnit.newMwEnvironment() ); + + // Simple sample (20 chars, 20 bytes) + simpleSample = '12345678901234567890'; + + // 3 bytes (euro-symbol) + U_20AC = '\u20AC'; + + // Outside of the BMP (pile of poo emoji) + poop = '\uD83D\uDCA9'; // "💩" + + // Multi-byte sample (22 chars, 26 bytes) + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; + + /** + * Test factory for mw.String#trimByteLength + * + * @param {Object} options + * @param {string} options.description Test name + * @param {string} options.sample Sequence of characters to trim + * @param {string} [options.initial] Previous value of the sequence of characters, if any + * @param {Number} options.limit Length to trim to + * @param {Function} [options.fn] Filter function + * @param {string} options.expected Expected final value + */ + function byteLimitTest( options ) { + var opt = $.extend( { + description: '', + sample: '', + initial: '', + limit: 0, + fn: function ( a ) { return a; }, + expected: '' + }, options ); + + QUnit.test( opt.description, function ( assert ) { + var res = trimByteLength( opt.initial, opt.sample, opt.limit, opt.fn ); + + assert.equal( + res.newVal, + opt.expected, + 'New value matches the expected string' + ); + } ); + } + + byteLimitTest( { + description: 'Limit using the maxlength attribute', + limit: 10, + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte)', + limit: 14, + sample: mbSample, + expected: '1234567890' + U_20AC + '1' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte, outside BMP)', + limit: 3, + sample: poop, + expected: '' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte) overlapping a byte', + limit: 12, + sample: mbSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + limit: 6, + fn: function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + }, + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + limit: 6, + fn: function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + }, + sample: 'User:Example', + // The callback alters the value to be used to calculeate + // the length. The altered value is "Exampl" which has + // a length of 6, the "e" would exceed the limit. + expected: 'User:Exampl' + } ); + + byteLimitTest( { + description: 'Input filter that increases the length', + limit: 10, + fn: function ( text ) { + return 'prefix' + text; + }, + sample: simpleSample, + // Prefix adds 6 characters, limit is reached after 4 + expected: '1234' + } ); + + byteLimitTest( { + description: 'Trim from insertion when limit exceeded', + limit: 3, + initial: 'abc', + sample: 'zabc', + // Trim from the insertion point (at 0), not the end + expected: 'abc' + } ); + + byteLimitTest( { + description: 'Trim from insertion when limit exceeded', + limit: 3, + initial: 'abc', + sample: 'azbc', + // Trim from the insertion point (at 1), not the end + expected: 'abc' + } ); + + byteLimitTest( { + description: 'Do not cut up false matching substrings in emoji insertions', + limit: 12, + initial: '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" + sample: '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" + expected: '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9' // "💩💹💩" + } ); + + byteLimitTest( { + description: 'Unpaired surrogates do not crash', + limit: 4, + sample: '\uD800\uD800\uDFFF', + expected: '\uD800' + } ); + +}( jQuery, mediaWiki ) ); -- 2.20.1