"mw.Title",
"mw.Uri",
"mw.RegExp",
+ "mw.String",
"mw.messagePoster.*",
"mw.notification",
"mw.Notification_",
],
'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' => [
'mediawiki.inspect' => [
'scripts' => 'resources/src/mediawiki/mediawiki.inspect.js',
'dependencies' => [
- 'jquery.byteLength',
+ 'mediawiki.String',
'mediawiki.RegExp',
],
'targets' => [ 'desktop', 'mobile' ],
'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',
],
'resources/src/mediawiki/mediawiki.Title.phpCharToUpper.js',
],
'dependencies' => [
- 'jquery.byteLength',
- 'jquery.byteLimit',
+ 'mediawiki.String',
'mediawiki.util',
],
'targets' => [ 'desktop', 'mobile' ],
'resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js',
],
'dependencies' => [
- 'jquery.byteLength',
+ 'mediawiki.String',
'oojs',
'mediawiki.api',
'mediawiki.api.options',
// TitleInputWidget
'mediawiki.Title',
'mediawiki.api',
- 'jquery.byteLimit',
+ 'mediawiki.String',
],
'messages' => [
// NamespaceInputWidget
],
'dependencies' => [
'oojs-ui-core',
- 'jquery.byteLimit'
+ 'jquery.byteLimit',
+ 'mediawiki.String',
],
'targets' => [ 'desktop', 'mobile' ]
],
/**
* @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
/**
* @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
* "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.
* @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,
// 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,
* @class jQuery
* @mixins jQuery.plugin.byteLimit
*/
-}( jQuery ) );
+}( jQuery, mediaWiki ) );
( function ( mw, $ ) {
+
+ var byteLength = require( 'mediawiki.String' ).byteLength;
+
/* eslint no-underscore-dangle: "off" */
/**
* Controller for the filters in Recent Changes
// Stringify state
stringified = JSON.stringify( state );
- if ( $.byteLength( stringified ) > 65535 ) {
+ if ( byteLength( stringified ) > 65535 ) {
// Sanity check, since the preference can only hold that.
return;
}
-/**
- * @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 ) );
*/
( function ( $, mw ) {
+ var trimByteLength = require( 'mediawiki.String' ).trimByteLength;
+
/**
* Creates an mw.widgets.TitleInputWidget object.
*
// 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;
--- /dev/null
+( 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
+ };
+
+}() );
/* Private members */
var
+ mwString = require( 'mediawiki.String' ),
+
namespaceIds = mw.config.get( 'wgNamespaceIds' ),
/**
// 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;
}
* @return {string}
*/
trimToByteLength = function ( s, length ) {
- return $.trimByteLength( '', s, length ).newVal;
+ return mwString.trimByteLength( '', s, length ).newVal;
},
/**
( function ( mw, $ ) {
var inspect,
+ byteLength = require( 'mediawiki.String' ).byteLength,
hasOwn = Object.prototype.hasOwnProperty;
function sortByProperty( array, prop, descending ) {
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 ] ) );
}
}
$.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 ];
'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',
'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',
],
'dependencies' => [
'jquery.accessKeyLabel',
- 'jquery.byteLength',
'jquery.byteLimit',
'jquery.color',
'jquery.colorUtil',
'mediawiki.jqueryMsg',
'mediawiki.messagePoster',
'mediawiki.RegExp',
+ 'mediawiki.String',
'mediawiki.storage',
'mediawiki.Title',
'mediawiki.toc',
+++ /dev/null
-( 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 ) );
--- /dev/null
+( 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)' );
+ } );
+}() );
--- /dev/null
+( 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 ) );