Jayprakash12345 has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/405057 )
Change subject: Convert Arrays to use extension registration ...................................................................... Convert Arrays to use extension registration Bug: T185253 Change-Id: Ieeb16525f7e45437c53239cbe6d813fd179ba6a5 --- M Arrays.php A ExtArrays.class.php A extension.json 3 files changed, 1,355 insertions(+), 1,339 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Arrays refs/changes/57/405057/2 diff --git a/Arrays.php b/Arrays.php index a04de8a..16dce8d 100644 --- a/Arrays.php +++ b/Arrays.php @@ -17,1342 +17,17 @@ * @author Jie Bao * @author Daniel Werner < danwe...@web.de > (since version 1.3) */ - -if ( ! defined( 'MEDIAWIKI' ) ) { die(); } - -$wgExtensionCredits['parserhook'][] = array( - 'path' => __FILE__, - 'name' => 'Arrays', - 'url' => 'https://www.mediawiki.org/wiki/Extension:Arrays', - 'author' => array( - 'Li Ding', - 'Jie Bao', - '[https://www.mediawiki.org/wiki/User:Danwe Daniel Werner]' - ), - 'descriptionmsg' => 'arrays-desc', - 'license-name' => 'MIT', - 'version' => ExtArrays::VERSION -); - -// Internationalization -$wgMessagesDirs['Arrays'] = __DIR__ . '/i18n'; -$wgExtensionMessagesFiles['ArraysMagic'] = ExtArrays::getDir() . '/Arrays.i18n.magic.php'; - -// hooks registration: -$wgHooks['ParserFirstCallInit'][] = 'ExtArrays::init'; -$wgHooks['ParserClearState' ][] = 'ExtArrays::onParserClearState'; - -// parser tests registration: -$wgParserTestFiles[] = ExtArrays::getDir() . '/arrayParserTests.txt'; -$wgParserTestFiles[] = ExtArrays::getDir() . '/arrayLoopsInteractionParserTests.txt'; - -// Include the settings file: -require_once ExtArrays::getDir() . '/Arrays.settings.php'; - - -/** - * Extension class with all the array functionality, also serves as store for arrays per - * Parser object and offers public accessors for interaction with the 'Arrays' extension. - * - * @since 2.0 ('ArrayExtension' before and one global instance, also non-static parser functions) - */ -class ExtArrays { - - /** - * Version of the 'Arrays' extension. - * - * @since 2.0 (before in 'Arrays' class since 1.3.2) - */ - const VERSION = '2.1.0'; - - /** - * Store for arrays. - * - * @var array - * @private - */ - public $mArrays = array(); - - /** - * Default separator for '#arrayprint'. Might be ', ' in compatibility-mode or - * by default since Arrays 2.0 the languages comma separator. - * - * @since 2.0 - * - * @var string - */ - static $mDefaultSep; - - /** - * Sets up parser functions - * - * @since 2.0 - */ - public static function init( Parser &$parser ) { - global $egArraysCompatibilityMode; - /* - * store for arrays per Parser object. This will solve several bugs related to - * 'ParserClearState' hook clearing all variables early in combination with certain - * other extensions. (since v2.0) - */ - $parser->mExtArrays = new self(); - - // initialize default separator for '#arrayprint' - if( $egArraysCompatibilityMode ) { - // COMPATIBILITY-MODE - self::$mDefaultSep = ', '; - } - else { - // since 2.0 the default separator for arrayprint is set to the languages default - self::$mDefaultSep = wfMessage( 'comma-separator' )->inContentLanguage()->text(); - } - - // Parser::SFH_OBJECT_ARGS available since MW 1.12 - self::initFunction( $parser, 'arraydefine' ); - self::initFunction( $parser, 'arrayprint', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arrayindex', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arraysize' ); - self::initFunction( $parser, 'arraysearch', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arraysearcharray', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arrayslice' ); - self::initFunction( $parser, 'arrayreset', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arrayunique' ); - self::initFunction( $parser, 'arraysort' ); - self::initFunction( $parser, 'arraymerge', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arrayunion', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arraydiff', Parser::SFH_OBJECT_ARGS ); - self::initFunction( $parser, 'arrayintersect', Parser::SFH_OBJECT_ARGS ); - - return true; - } - private static function initFunction( Parser &$parser, $name, $flags = 0 ) { - // all parser functions with prefix: - $prefix = ( $flags & Parser::SFH_OBJECT_ARGS ) ? 'pfObj_' : 'pf_'; - $functionCallback = array( __CLASS__, $prefix . $name ); - - $parser->setFunctionHook( $name, $functionCallback, $flags ); - } - - /** - * Returns the extensions base installation directory. - * - * @since 2.0 - * - * @return string - */ - public static function getDir() { - static $dir = null; - if( $dir === null ) { - $dir = dirname( __FILE__ ); - } - return $dir; - } - - - #################### - # Parser Functions # - #################### - - /////////////////////////////////////////////////////////// - // PART 1. Array Construction - /////////////////////////////////////////////////////////// - - /** - * Define an array by a list of 'values' deliminated by 'delimiter', - * the delimiter should be perl regular expression pattern - * usage: - * {{#arraydefine:arrayid|values|delimiter|options}} - * - * http://us2.php.net/manual/en/book.pcre.php - * see also: http://us2.php.net/manual/en/function.preg-split.php - */ - static function pf_arraydefine( - Parser &$parser, - $arrayId, - $value = null, - $delimiter = '/\s*,\s*/', - $options = '' - ) { - if ( !isset( $arrayId ) ) { - return ''; - } - - $out = ''; - $array = array(); - $trimDone = false; // whether or not we can be sure that all array elements are trimmed - - // normalize - $delimiter = trim( $delimiter ); - - if( $value === null ) { - // no element set, not even an empty one - $array = array(); - } - else { - // fill array with user input: - if( $delimiter === '' ) { - // whole input one element, also takes care of special case empty '' value and 'unique' option set - $array = array( $value ); - $trimDone = true; - } - else { - // if no regex delimiter given, build one: - if( ! self::isValidRegEx( $delimiter ) ) { - $delimiter = '/\s*' . preg_quote( $delimiter, '/' ) . '\s*/'; - $trimDone = true; // spaces are part of the delimiter now - } - $array = preg_split( $delimiter, $value ); - } - - // trim all values before unique if still necessary, otherwise unique might not work correctly - if( ! $trimDone ) { - $array = self::sanitizeArray( $array ); - } - - // now parse the options, and do posterior process on the created array - $arrayOptions = self::parse_options( $options ); - - // make it unique if option is set - if( array_key_exists( 'unique', $arrayOptions ) ) { - // unique like the parser function would do it - $array = self::array_unique( $array ); - } - - // if 'singleempty' is NOT set, {{#arraydefine:a|}} will be empty. - // by default this would give an empty array (due to historical as well as usability reasons) - if( ! array_key_exists( 'singleempty', $arrayOptions ) ) { - // there is no other uncomplicated way than this to define a single empty elemented array currently! - if( count( $array ) === 1 && $array[0] === '' ) { - $array = array(); - } - } - - /** - * @ToDo: - * The 'empty' option was introduced in r81676 but actually breaks old functionality since it will remove - * all empty elements by default. - * 'unique' already allows to remove all empty elements but it will also remove al duplicates, there should - * be a more intelligent alternative to 'unique' which allows both, to preserve or remove empty elements - * independent from removing duplicate values. - */ - /* - // remove all empty '' elements if option is NOT set - if( ! array_key_exists( 'empty', $arrayOptions ) ) { - $values = array(); // temp array so we won't have gaps (don't use unset!) - foreach ( $array as $key => $value ) { - if( $value !== '' ) { - $values[] = $elem; - } - } - $array = $values; - unset( $values ); - } - */ - - // sort array if the option is set - if( array_key_exists( 'sort', $arrayOptions ) ) { - $array = self::arraySort( $array, self::array_value( $arrayOptions, 'sort' ) ); - } - - // print the array upon request - switch( self::array_value( $arrayOptions, 'print' ) ) { - case 'list': - // simple list output - $out = implode( self::$mDefaultSep, $array ); - break; - case 'pretty': - global $wgLang; - $out = $wgLang->listToText( $array ); - break; - } - } - - self::get( $parser )->setArray( $arrayId, $array ); - - return $out; - } - - - /////////////////////////////////////////////////////////// - // PART 2. Extracting Information - /////////////////////////////////////////////////////////// - - - /** - * print an array. - * foreach element of the array, print 'subject' where all occurrences of 'search' is replaced with the element, - * and each element print-out is deliminated by 'delimiter' - * The subject can embed parser functions; wiki links; and templates. - * usage: - * {{#arrayprint:arrayid|delimiter|search|subject|options}} - * examples: - * {{#arrayprint:b}} -- simple - * {{#arrayprint:b|<br/>}} -- add change line - * {{#arrayprint:b|<br/>|@@@|[[@@@]]}} -- embed wiki links - * {{#arrayprint:b|<br/>|@@@|{{#set:prop=@@@}} }} -- embed parser function - * {{#arrayprint:b|<br/>|@@@|{{f.tag{{f.print.vbar}}prop{{f.print.vbar}}@@@}} }} -- embed template function - * {{#arrayprint:b|<br/>|@@@|[[name::@@@]]}} -- make SMW links - */ - static function pfObj_arrayprint( Parser &$parser, PPFrame $frame, $args ) { - global $egArraysCompatibilityMode, $egArraysExpansionEscapeTemplates; - - // Get Parameters - $arrayId = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; - $delimiter = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : self::$mDefaultSep; - /* - * PPFrame::NO_ARGS and PPFrame::NO_TEMPLATES for expansion make a lot of sense here since the patterns getting replaced - * in $subject before $subject is being parsed. So any template or argument influence in the patterns wouldn't make any - * sense in any sane scenario. - */ - $search = isset( $args[2] ) ? trim( $frame->expand( $args[2], PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES ) ) : null; - $subject = isset( $args[3] ) ? trim( $frame->expand( $args[3], PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES ) ) : null; - // options array: - $options = isset( $args[4] ) - ? self::parse_options( $frame->expand( $args[4] ) ) - : array(); - - - // get array, null if non-existant: - $array = self::get( $parser )->getArray( $arrayId ); - - if( $array === null ) { - // array we want to print doesn't exist! - if( ! $egArraysCompatibilityMode ) { - return ''; - } else { - // COMPATIBILITY-MODE - return "undefined array: $arrayId"; - } - } - - // if there is no subject, there is no point in expanding. Faster! - if( $subject === null ) { - if( ! $egArraysCompatibilityMode && $egArraysExpansionEscapeTemplates !== null ) { - // we can ignore options here, since if subject is null, options won't be set as well! - return trim( implode( $delimiter, $array ) ); - } else { - // COMPATIBILITY-MODE - // set search and subject so the old routine can be done - $search = $subject = '@@@@'; - } - } - - $rendered_values = array(); - - foreach( $array as $val ) { - - if( ! $egArraysCompatibilityMode ) { - // NO COMPATIBILITY-MODE - /** - * escape the array value so it won't destroy the users wiki markup expression. - */ - $val = self::escapeForExpansion( $val ); - } - // replace place holder with current value: - $rawResult = str_replace( $search, $val, $subject ); - /* - * $subjectd still is un-expanded (this allows to use some parser functions like - * {{FULLPAGENAME:@@@@}} directly without getting parsed before @@@@ is replaced. - * Expand it so we replace templates like {{!}} which we need for the final parse. - */ - $rawResult = $parser->preprocessToDom( $rawResult, $frame->isTemplate() ? Parser::PTD_FOR_INCLUSION : 0 ); - $rawResult = trim( $frame->expand( $rawResult ) ); - - $rendered_values[] = $rawResult; - } - - // follow special print options: - switch( self::array_value( $options, 'print' ) ) { - case 'pretty': - // pretty list print with ' and ' connecting the last two items - if( $delimiter === '' ) { - // '' as delimiter isn't pretty, so in this case we take the (languages) default - $output = self::arrayToText( $rendered_values ); - } else { - $output = self::arrayToText( $rendered_values, $delimiter ); - } - break; - - default: - // normal print with one delimiter, might be the languages default - $output = implode( $delimiter, $rendered_values ); - break; - } - - if( $egArraysCompatibilityMode || $egArraysExpansionEscapeTemplates === null ) { - // COMPATIBLITY-MODE: - /* - * don't leave the final parse to Parser::braceSubstitution() since there are some special cases where it - * would produce unexpected output (it uses a new child frame and ignores whether the frame is a template!) - */ - $output = $parser->preprocessToDom( $output, $frame->isTemplate() ? Parser::PTD_FOR_INCLUSION : 0 ); - $output = $frame->expand( $output ); - } - - return trim( $output ); - } - - /** - * print the value of an array (identified by arrayid) by the index, invalid index results in the default value being printed. note the index is 0-based. - * usage: - * {{#arrayindex:arrayid|index}} - */ - static function pfObj_arrayindex( Parser &$parser, PPFrame $frame, $args ) { - global $egArraysCompatibilityMode; - - // Get Parameters - $arrayId = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; - $rawOptions = isset( $args[2] ) ? $args[2] : ''; - - if( ! isset( $args[1] ) ) { - return ''; - } - $index = trim( $frame->expand( $args[1] ) ); - - // get value or null if it doesn't exist. Takes care of negative index as well - $val = self::get( $parser )->getArrayValue( $arrayId, $index ); - - if( $val === null || ( $val === '' && !$egArraysCompatibilityMode ) ) { - // index doesn't exist, return default (parameter 3)! - // without compatibility, also return default in case of empty string '' - - // only expand default when needed - $defaultOrOptions = trim( $frame->expand( $rawOptions ) ); - - if( $egArraysCompatibilityMode ) { - // COMPATIBILITY-MODE - // now parse the options, and do posterior process on the created array - $options = self::parse_options( $defaultOrOptions ); - $default = self::array_value( $options, 'default' ); - } else { - $default = $defaultOrOptions; - } - - return $default; - } - - return $val; - } - - /** - * returns the size of an array. - * Print the size (number of elements) in the specified array and '' if array doesn't exist - * usage: - * {{#arraysize:arrayid}} - * - * See: http://www.php.net/manual/en/function.count.php - */ - static function pf_arraysize( Parser &$parser, $arrayId ) { - $store = self::get( $parser ); - - if( ! $store->arrayExists( $arrayId ) ) { - return ''; - } - - return count( $store->getArray( $arrayId ) ); - } - - - /** - * locate the index of the first occurrence of an element starting from the 'index' - * - print "-1" (not found) or index (found) to show the index of the first occurrence of 'value' in the array identified by arrayid - * - if 'yes' and 'no' are set, print value of them when found or not-found - * - index is 0-based , it must be non-negative and less than lenth - * usage: - * {{#arraysearch:arrayid|value|index|yes|no}} - * - * See: http://www.php.net/manual/en/function.array-search.php - * note it is extended to support regular expression match and index - */ - static function pfObj_arraysearch( Parser &$parser, PPFrame $frame, $args ) { - // Get Parameters - $arrayId = trim( $frame->expand( $args[0] ) ); - $index = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : 0; - - $store = self::get( $parser ); - - if( $store->arrayExists( $arrayId ) - && $store->validate_array_index( $arrayId, $index, false ) - ) { - $array = $store->getArray( $arrayId ); - - // validate/build search regex: - if( isset( $args[1] ) ) { - - $needle = trim( $frame->expand( $args[1] ) ); - - if ( ! self::isValidRegEx( $needle ) ) { - $needle = '/^\s*' . preg_quote( trim( $needle ), '/' ) . '\s*$/'; - } - } - else { - $needle = '/^\s*$/'; - } - - // search for a match inside the array: - $total = count( $array ); - for ( $i = $index; $i < $total; $i++ ) { - $value = $array[ $i ]; - - if ( preg_match( $needle, $value ) ) { - // found! - if ( isset( $args[3] ) ) { - // Expand only when needed! - return trim( $frame->expand( $args[3] ) ); - } - else { - // return index of first found item - return $i; - } - } - } - } - - // no match! (Expand only when needed!) - if( isset( $args[4] ) ) { - $no = trim( $frame->expand( $args[4] ) ); - } else { - global $egArraysCompatibilityMode; - $no = $egArraysCompatibilityMode - ? '-1' // COMPATIBILITY-MODE - : ''; - } - return $no; - } - - /** - * search an array and create a new array with all the results. Transforming the new entries before storing them is possible too. - * usage: - * {{#arraysearcharray:arrayid_new|arrayid|needle|index|limit|transform}} - * - * "needle" can be a regular expression or a string search value. If "needle" is a regular expression, "transform" can contain - * "$n" where "n" stands for a number to access a variable from the regex result. - */ - static function pfObj_arraysearcharray( Parser &$parser, PPFrame $frame, $args ) { - $store = self::get( $parser ); - - // get first two parameters - $arrayId = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : null; - $arrayId_new = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; - - if( $arrayId === null ) { - global $egArraysCompatibilityMode; - if( ! $egArraysCompatibilityMode ) { // COMPATIBILITY-MODE - $store->setArray( $arrayId_new ); - } - return ''; - } - - // Get Parameters the other parameters - $needle = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '/^(\s*)$/'; - $index = isset( $args[3] ) ? trim( $frame->expand( $args[3] ) ) : 0; - $limit = isset( $args[4] ) ? trim( $frame->expand( $args[4] ) ) : ''; - $rawTransform = isset( $args[5] ) ? $args[5] : null; - - // also takes care of negative index by calculating start index: - $validIndex = $store->validate_array_index( $arrayId, $index, false ); - - // make sure at least empty array exists but don't overwrite data - // we still need in case new array ID same as target array ID - $array = $store->getArray( $arrayId ); - $store->setArray( $arrayId_new ); - - if( $array === null || !$validIndex ) { - return ''; - } - - // non-numeric limit will be set to 0, except limit was omitted ('') - $limit = $limit === '' ? -1 : (int)$limit; - if( $limit === 0 ) { - return ''; - } - - $newArr = array(); - - $regexFunSupport = self::hasRegexFunSupport(); - if( ! self::isValidRegEx( $needle, $regexFunSupport ) ) { - $needle = '/^\s*(' . preg_quote( $needle, '/' ) . ')\s*$/'; - } - - // search the array for all matches and put them in the new array - $total = count( $array ); - for( $i = $index; $i < $total; $i++ ) { - - $value = $array[ $i ]; - - if( preg_match( $needle, $value ) ) { - // Found something! - if( $rawTransform !== null ) { - // Transform the matching string. Can we use 'Regex Fun' with special 'e' flag support ? - if( $regexFunSupport ) { - // do the transformation with Regex Fun to support 'e' flag: - $transform = trim( $frame->expand( - $rawTransform, - PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES // leave expanding of templates to 'Regex Fun' - ) ); - $value = ExtRegexFun::doPregReplace( - $needle, - $rawTransform, - $value, - -1, - $parser, - $frame, - array( ExtRegexFun::FLAG_REPLACEMENT_PARSE ) - ); - } - else { - // regular preg_replace: - $transform = trim( $frame->expand( $rawTransform ) ); - $value = preg_replace( $needle, $transform, $value ); - } - } - $newArr[] = trim( $value ); - - // stop if limit is reached, limit -1 means no limit - if( --$limit === 0 ) { - break; - } - } - } - - // set new array: - $store->setArray( $arrayId_new, $newArr ); - return ''; - } - - /** - * extract a slice from an array - * usage: - * {{#arrayslice:arrayid_new|arrayid|offset|length}} - * - * extract a slice from an array - * see: http://www.php.net/manual/en/function.array-slice.php - */ - static function pf_arrayslice( Parser &$parser, $arrayId_new, $arrayId = null , $offset = 0, $length = null ) { - $store = self::get( $parser ); - if( $arrayId === null ) { - global $egArraysCompatibilityMode; - if( ! $egArraysCompatibilityMode ) { // COMPATIBILITY-MODE - $store->setArray( $arrayId_new ); - } - return ''; - } - // get target array before overwriting it in any way - $array = $store->getArray( $arrayId ); - - // make sure at least an empty array exists if we return early - $store->setArray( $arrayId_new ); - - if( $array === null - || ! is_numeric( $offset ) // don't ignore invalid offset - ) { - return ''; - } - - if( ! is_numeric( $length ) ) { - $length = null; // ignore invalid input, slice till end - } - - // array_slice will re-organize keys - $newArray = array_slice( $array, $offset, $length ); - $store->setArray( $arrayId_new, $newArray ); - - return ''; - } - - - /////////////////////////////////////////////////////////// - // PART 3. Array Alteration - /////////////////////////////////////////////////////////// - - /** - * reset some or all defined arrayes - * usage: - * {{#arrayreset:}} - * {{#arrayreset:arrayid1,arrayid2,...arrayidn}} - */ - static function pfObj_arrayreset( Parser &$parser, PPFrame $frame, $args) { - global $egArraysCompatibilityMode; - - if( $egArraysCompatibilityMode && count( $args ) == 1 ) { - /* - * COMPATIBILITY-MODE: before arrays were separated by ';' which is an bad idea since - * the ',' is an allowed character in array names! - */ - $args = preg_split( '/\s*,\s*/', trim( $frame->expand( $args[0] ) ) ); - } - - $store = self::get( $parser ); - - // reset all hash tables if no specific tables are given: - if( ! isset( $args[0] ) || ( $args[0] === '' && count( $args ) == 1 ) ) { - // reset ALL arrays! - $store->mArrays = array(); - } - else { - // reset specific hash tables: - foreach( $args as $arg ) { - $arrayId = trim( $frame->expand( $arg ) ); - $store->unsetArray( $arrayId ); - } - } - return ''; - } - - - /** - * convert an array to a set - * convert the array identified by arrayid into a set (all elements are unique) - * also removes empty '' elements from the array - * usage: - * {{#arrayunique:arrayid}} - * - * see: http://www.php.net/manual/en/function.array-unique.php - */ - static function pf_arrayunique( Parser &$parser, $arrayId ) { - $store = self::get( $parser ); - - if( $store->arrayExists( $arrayId ) ) { - $array = $store->getArray( $arrayId ); - $array = self::array_unique( $array ); - $store->setArray( $arrayId, $array ); - } - return ''; - } - - - /** - * sort specified array in the following order: - * - none: No sort (default) - * - desc: In descending order, large to small - * - asce: In ascending order, small to large - * - random: Shuffle the arrry in random order - * - reverse: Return an array with elements in reverse order - * usage: - * {{#arraysort:arrayid|order}} - * - * see: http://www.php.net/manual/en/function.sort.php - * http://www.php.net/manual/en/function.rsort.php - * http://www.php.net/manual/en/function.shuffle.php - * http://us3.php.net/manual/en/function.array-reverse.php - */ - static function pf_arraysort( Parser &$parser, $arrayId , $sort = 'none' ) { - $store = self::get( $parser ); - - $array = $store->getArray( $arrayId ); - - if( $array === null ) { - return ''; - } - - // sort array and store it - $array = self::arraySort( $array, $sort ); - $store->setArray( $arrayId, $array ); - return ''; - } - - - /////////////////////////////////////////////////////////// - // PART 4. Array Interaction - /////////////////////////////////////////////////////////// - - /** - * Merge values two arrayes identified by arrayid1 and arrayid2 into a new array identified by arrayid_new. - * This merge differs from array_merge of php because it merges values. - * - * Usage: - * {{#arraymerge:arrayid_new |array1 |array2 |... |array n}} - * See: http://www.php.net/manual/en/function.array-merge.php - */ - static function pfObj_arraymerge( &$parser, $frame, $args) { - self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); - return ''; - } - private function multi_arraymerge( $array1, $array2 ) { - // keys will not be re-organized - return array_merge( $array1, $array2 ); - } - - /** - * Usage: - * {{#arrayunion:arrayid_new|arrayid1|arrayid2}} - * - * Set operation, {red, white} = {red, white} union {red} - * Similar to arraymerge but with unique values. This union works on values. - */ - static function pfObj_arrayunion( &$parser, $frame, $args) { - self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); - return ''; - } - private function multi_arrayunion( $array1, $array2 ) { - // keys will not be re-organized - return array_unique( array_merge( $array1, $array2 ) ); - } - - /** - * Usage: - * {{#arrayintersect:arrayid_new |array1 |array2 |... |array n}} - * - * Set operation, {red} = {red, white} intersect {red,black} - * See: http://www.php.net/manual/en/function.array-intersect.php - */ - static function pfObj_arrayintersect( &$parser, $frame, $args) { - self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); - return ''; - } - private function multi_arrayintersect( $array1, $array2 ) { - // keys will be preserved! - return array_intersect( $array1, $array2 ); - } - - /** - * - * Usage: - * {{#arraydiff:arrayid_new |array1 |array2 |... |array n}} - * - * Set operation, {white} = {red, white} - {red} - * See: http://www.php.net/manual/en/function.array-diff.php - */ - static function pfObj_arraydiff( &$parser, $frame, $args) { - self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); - return ''; - } - private function multi_arraydiff( $array1, $array2 ) { - // keys will be preserved! - return array_diff( $array1, $array2 ); - } - - - ################## - # Private helper # - ################## - - /** - * Base function for operations with multiple arrays given thru n parameters - * $operationFunc expects a function name prefix (suffix 'multi_') with two parameters - * $array1 and $array2 which will perform an action between $array1 and $array2 which - * will result into a new $array1. There can be 1 to n $hash2 in the whole process. - * - * Note: This function is similar to that of Extension:HashTables. - * - * @since 2.0 - * - * @param $frame PPFrame - * @param $args array - * @param $operationFunc string name of the function calling this. There must be a counterpart - * function with prefix 'multi_' which should have two parameters. Both parameters - * will receive an array, the function must return the result array of the processing. - * @param $runFuncOnSingleArray boolean whether the $operationFunc function should be run in case - * only one array id is given. If not, the original array will end up in the new array. - */ - protected function multiArrayOperation( PPFrame $frame, array $args, $operationFunc, $runFuncOnSingleArray = true ) { - $lastArray = null; - $operationRan = false; - $finalArrayId = trim( $frame->expand( $args[0] ) ); - $operationFunc = 'multi_' . preg_replace( '/^pfObj_/', '', $operationFunc ); - - // For all arrays given in parameters 2 to n (ignore 1 because this is the name of the new array) - for( $i = 1; $i < count( $args ); $i++ ) { - // just make sure we don't fall into gaps of given arguments: - if( ! array_key_exists( $i, $args ) ) { - continue; - } - $argArrayId = trim( $frame->expand( $args[ $i ] ) ); - - // ignore all tables which do not exist - if( $this->arrayExists( $argArrayId ) ) { - $argArray = $this->getArray( $argArrayId ); - if( $lastArray === null ) { - // first valid array, process together with second... - $lastArray = $argArray; - } - else { - // second or later hash table, process with previous: - $lastArray = $this->{ $operationFunc }( $lastArray, $argArray ); // perform action between last and current array - $operationRan = true; - } - } - } - - // in case no array was given at all: - if( $lastArray === null ) { - $lastArray = array(); - } - - global $egArraysCompatibilityMode; - - if( ! $operationRan && $egArraysCompatibilityMode - && $operationFunc !== 'multi_arraymerge' // only exception was 'arraymerge' - ) { - /* - * COMPATIBILITY-MODE: - * Before version 2.0 we didn't create a new array in case only one array was given. - * The only exception was 'arraymerge' which did duplicate the array. - */ - return ''; - } - - // if the operation didn't run because there was only one or no array: - if( ! $operationRan && $runFuncOnSingleArray ) { - $lastArray = $this->{ $operationFunc }( $lastArray ); - } - - // re-organize all keys since some 'multi_' functions will preserve keys! - $lastArray = array_merge( $lastArray ); - - $this->setArray( $finalArrayId, $lastArray ); - } - - /** - * Validates an index for an array and returns true in case the index is a valid index within - * the array. This also changes the index value, which is given by reference, in case it is - * set to a negative value. In case $strictIndex is set to false, further transforming of - * $index might be done - in the same cases normally the function would return false. - * - * @param string $arrayId - * @param mixed &$index - * @param bool $strictIndex Whether non-numeric indexes and negative indexes which would - * end up out of range, below 0, should be set to 0 automatically. - * - * @return boolean - */ - protected function validate_array_index( $arrayId, &$index, $strictIndex = false ) { - if( ! is_numeric( $index ) ) { - if( $strictIndex ) { - return false; - } else { - $index = 0; - } - } - $index = (int)$index; - - if( ! array_key_exists( $arrayId, $this->mArrays ) ) { - return false; - } - - $array = $this->mArrays[ $arrayId ]; - - // calculate start index for negative start indexes: - if( $index < 0 ) { - $index = count( $array ) + $index; - if ( $index < 0 && !$strictIndex ) { - $index = 0; - } - } - - if( ! isset( $array ) ) { - return false; - } - if( ! array_key_exists( $index, $array ) ) { - return false; - } - return true; - } - - /** - * private function for validating array by name - * @ToDo: get rid of this! - * @deprecated - */ - protected function validate_array_by_arrayId( $arrayId ) { - if( ! isset( $arrayId ) ) { - return ''; - } - if( ! isset( $this->mArrays ) - || ! array_key_exists( $arrayId, $this->mArrays ) - || ! is_array( $this->mArrays[ $arrayId ] ) - ) { - global $egArraysCompatibilityMode; - if( $egArraysCompatibilityMode ) { - return "undefined array: $arrayId"; // COMPATIBILITY-MODE - } else { - return ''; - } - } - - return true; - } - - /** - * Convenience function to get a value from an array. Returns '' in case the - * value doesn't exist or no array was given - * - * @return string - */ - protected static function array_value( $array, $field ) { - if ( is_array( $array ) && array_key_exists( $field, $array ) ) { - return $array[ $field ]; - } - return ''; - } - - /** - * Parses a string of options separated by ','. Options can be just certain key-words or - * key-value pairs separated by '='. Options are case-insensitive and spacing between - * separators will be ignored. - */ - protected static function parse_options( $options ) { - if( ! isset( $options ) ) { - return array(); - } - - // now parse the options, and do posterior process on the created array - $options = preg_split( '/\s*,\s*/', strtolower( trim( $options ) ) ); - - $ret = array(); - foreach( $options as $option ) { - $optPair = preg_split( '/\s*\=\s*/', $option, 2 ); - if( sizeof( $optPair ) == 1 ) { - $ret[ $optPair[0] ] = true; - } else { - $ret[ $optPair[0] ] = $optPair[1]; - } - } - return $ret; - } - - /** - * same as self::arrayUnique() but without sanitazation, only for internal use. - */ - protected static function array_unique( array $array ) { - // delete duplicate values - $array = array_unique( $array ); - - $values = array(); - foreach( $array as $key => $val ) { - // don't put emty elements into the array - if( $val !== '' ) { - $values[] = $val; - } - } - - return $values; - } - - - ############## - # Used Hooks # - ############## - - static function onParserClearState( Parser &$parser ) { - // remove all arrays to avoid conflicts with job queue or Special:Import or SMW semantic updates - $parser->mExtArrays = new self(); - return true; - } - - - #################################### - # Public functions for interaction # - #################################### - # - # public non-parser functions, accessible for - # other extensions doing interactive stuff - # with the Array extension. - # - - /** - * Convenience function to return the 'Arrays' extensions array store connected - * to a certain Parser object. Each parser has its own store which will be reset after - * a parsing process [Parser::parse()] has finished. - * - * @since 2.0 - * - * @param Parser &$parser - * - * @return ExtArrays by reference so we still have the right object after 'ParserClearState' - */ - public static function &get( Parser &$parser ) { - return $parser->mExtArrays; - } - - /** - * Returns an array identified by $arrayId. If it doesn't exist, null will be returned. - * - * @since 2.0 - * - * @param string $arrayId - * - * @return array|null - */ - function getArray( $arrayId ) { - $arrayId = trim( $arrayId ); - if( $this->arrayExists( $arrayId ) ) { - return $this->mArrays[ $arrayId ]; - } - return null; - } - - /** - * This will add a new array or overwrite an existing one. Values should be delliverd as array - * values in form of a string. The array will be sanitized internally. - * - * @param string $arrayId - * @param array $array - */ - public function createArray( $arrayId, $array = array() ) { - $array = self::sanitizeArray( $array ); - $this->mArrays[ trim( $arrayId ) ] = $array; - } - - /** - * Same as the public function createArray() but without sanitizing the array automatically. - * This is save and faster for internal usage, just be sure your array doesn't have un-trimmed - * values or non-numeric or negative array keys and no gaps between keys. - * - * @param string $arrayId - * @param array $array - */ - protected function setArray( $arrayId, $array = array() ) { - $this->mArrays[ trim( $arrayId ) ] = $array; - } - - /** - * Returns whether a certain array is defined within the page scope. - * - * @param string $arrayId - * - * @return boolean - */ - function arrayExists( $arrayId ) { - return array_key_exists( trim( $arrayId ), $this->mArrays ); - } - - /** - * Returns a value within an array. If key or array do not exist, this will return null - * or another predefined default. $index can also be a negative value, in this case the - * value that far from the end of the array will be returned. - * - * @since 2.0 - * - * @param string $arrayId - * @param string $index - * @param mixed $default value to return in case the value doesn't exist. null by default. - * - * @return string|null - */ - function getArrayValue( $arrayId, $index, $default = null ) { - $arrayId = trim( $arrayId ); - if( $this->arrayExists( $arrayId ) - && $this->validate_array_index( $arrayId, $index, true ) - && array_key_exists( $index, $this->mArrays[ $arrayId ] ) - ) { - return $this->mArrays[ $arrayId ][ $index ]; - } - else { - return $default; - } - } - - - /** - * Removes an existing array. If array didn't exist this will return false, otherwise true. - * - * @since 2.0 - * - * @param string $arrayId - * - * @return boolean whether the array existed and has been removed - */ - public function unsetArray( $arrayId ) { - $arrayId = trim( $arrayId ); - if( $this->arrayExists( $arrayId ) ) { - unset( $this->mArrays[ $arrayId ] ); - return true; - } - return false; - } - - /** - * Rebuild the array and reorganize all keys, trim all values. - * All gaps between array items will be closed. - * - * @since 2.0 - * - * @param array $arr array to be reorganized - * @return array - */ - public static function sanitizeArray( $array ) { - $newArray = array(); - foreach( $array as $val ) { - $newArray[] = trim( $val ); - } - return $newArray; - } - - /** - * Removes duplicate values and all empty elements from an array just like the - * '#arrayunique' parser function would do it. The array will be sanitized internally. - * - * @since 2.0 - * - * @param array $array - * - * @return array - */ - public static function arrayUnique( array $array ) { - return self::array_unique( self::sanitizeArray( $array ) ); - } - - /** - * Sorts an array just like parser function '#arraysort' would do it and allows the - * same sort modes. - * - * @since 2.0 - * - * @param array $array - * @param string $sortMode one of the following sort modes: - * - random: random array order - * - reverse: last entry will be first, first the last. - * - asce: sort array in ascending order. - * - desc: sort array in descending order. - * - natural: sort with a 'natural order' algorithm. See PHPs natsort() function. - * - * In addition, this function allows to set several flags behind the sort mode. The must be - * separated by a space. The following keys are allowed: - * - nolocale: will prevent 'asce' and 'desc' mode to considering PHP-defined language rules. - * - * @return array - */ - public static function arraySort( array $array, $sortMode ) { - global $egArraysCompatibilityMode; - - $flags = preg_split( '/\s+/s', $sortMode ); - $sortMode = array_shift( $flags ); // first string is the actual sort mode - - $localeFlag = SORT_LOCALE_STRING; // sort strings accordingly to what was set via setlocale() - if( - in_array( 'nolocale', $flags ) - || $egArraysCompatibilityMode // COMPATIBILITY-MODE - ) { - // 'nolocale' will prevent from using this flag! - $localeFlag = null; - } - - // do the requested sorting of the given array: - switch( $sortMode ) { - case 'asc': - case 'asce': - case 'ascending': - sort( $array, $localeFlag ); - break; - - case 'desc': - case 'descending': - rsort( $array, $localeFlag ); - break; - - case 'nat': - case 'natural': - natsort( $array ); - break; - - case 'rand': - case 'random': - shuffle( $array ); - break; - - case 'reverse': - $array = array_reverse( $array ); - break; - }; - return $array; - } - - /** - * Pretty much the same as Language::listToText() but allows us to set a custom comma separator. - * - * @since 2.0 - * - * @param Array $array - * @param string $commaSep - * - * @return string - */ - public static function arrayToText( $array, $commaSep = null ) { - global $wgLang; - $commaSep = $commaSep === null ? self::$mDefaultSep : $commaSep; - $s = ''; - $m = count( $array ) - 1; - if ( $m == 1 ) { - return $array[0] . $wgLang->getMessageFromDB( 'and' ) . $wgLang->getMessageFromDB( 'word-separator' ) . $array[1]; - } else { - for ( $i = $m; $i >= 0; $i-- ) { - if ( $i == $m ) { - $s = $array[$i]; - } else if ( $i == $m - 1 ) { - $s = $array[$i] . $wgLang->getMessageFromDB( 'and' ) . $wgLang->getMessageFromDB( 'word-separator' ) . $s; - } else { - $s = $array[$i] . $commaSep . $s; - } - } - return $s; - } - } - - /** - * Escapes a string so it can be used within PPFrame::expand() expansion without actually being - * changed because of special characters. - * Respects the configuration variable '$egArraysEscapeTemplates'. - * - * This is a workaround for bug #32829 - * - * @since 2.0 - * - * @param string $string - * @return string - */ - public static function escapeForExpansion( $string ) { - global $egArraysExpansionEscapeTemplates; - - if( $egArraysExpansionEscapeTemplates === null ) { - return $string; - } - - $string = strtr( - $string, - $egArraysExpansionEscapeTemplates - ); - - return $string; - } - - /** - * Decides for the given $pattern whether its a valid regular expression acceptable for - * Arrays parser functions or not. - * - * @param string $pattern regular expression including delimiters and optional flags - * @param bool $forRegexFun whether the regular expression is inteded to be used with 'Regex Fun' - * if supported by the wikis infrastructure. In case 'Regex Fun' is not available, - * the default validation will be used. - * - * @return boolean - */ - static function isValidRegEx( $pattern, $forRegexFun = false ) { - if( $forRegexFun && self::hasRegexFunSupport() ) { - return ExtRegexFun::validateRegex( $pattern ); - } - - if( ! preg_match( '/^([\\/\\|%]).*\\1[imsSuUx]*$/', $pattern ) ) { - return false; - } - wfSuppressWarnings(); // instead of using the evil @ operator! - $isValid = false !== preg_match( $pattern, ' ' ); // preg_match returns false on error - wfRestoreWarnings(); - return $isValid; - } - - /** - * Whether 'Regex Fun' extension is available in this wiki to take over preg_replace handling - * for '#arraysearcharray' function. - */ - static function hasRegexFunSupport() { - static $support = null; - if( $support === null ) { - $support = ( - defined( 'ExtRegexFun::VERSION' ) - && version_compare( ExtRegexFun::VERSION, '1.1', '>=' ) - ); - } - return $support; - } -} + + if ( function_exists( 'wfLoadExtension' ) ) { + wfLoadExtension( 'Arrays' ); + // Keep i18n globals so mergeMessageFileList.php doesn't break + $wgMessagesDirs['Arrays'] = __DIR__ . '/i18n'; + wfWarn( + 'Deprecated PHP entry point used for the Arrays extension. ' . + 'Please use wfLoadExtension instead, ' . + 'see https://www.mediawiki.org/wiki/Extension_registration for more details.' + ); + return; +} else { + die( 'This version of the Arrays extension requires MediaWiki 1.29+' ); +} \ No newline at end of file diff --git a/ExtArrays.class.php b/ExtArrays.class.php new file mode 100644 index 0000000..11aaf68 --- /dev/null +++ b/ExtArrays.class.php @@ -0,0 +1,1309 @@ +<?php +/** + * Extension class with all the array functionality, also serves as store for arrays per + * Parser object and offers public accessors for interaction with the 'Arrays' extension. + * + * @since 2.0 ('ArrayExtension' before and one global instance, also non-static parser functions) + */ +class ExtArrays { + + /** + * Version of the 'Arrays' extension. + * + * @since 2.0 (before in 'Arrays' class since 1.3.2) + */ + const VERSION = '2.1.0'; + + /** + * Store for arrays. + * + * @var array + * @private + */ + public $mArrays = array(); + + /** + * Default separator for '#arrayprint'. Might be ', ' in compatibility-mode or + * by default since Arrays 2.0 the languages comma separator. + * + * @since 2.0 + * + * @var string + */ + static $mDefaultSep; + + /** + * Sets up parser functions + * + * @since 2.0 + */ + public static function init( Parser &$parser ) { + global $egArraysCompatibilityMode; + /* + * store for arrays per Parser object. This will solve several bugs related to + * 'ParserClearState' hook clearing all variables early in combination with certain + * other extensions. (since v2.0) + */ + $parser->mExtArrays = new self(); + + // initialize default separator for '#arrayprint' + if( $egArraysCompatibilityMode ) { + // COMPATIBILITY-MODE + self::$mDefaultSep = ', '; + } + else { + // since 2.0 the default separator for arrayprint is set to the languages default + global $wgLang; + $wgLang->getMessageFromDB( 'comma-separator' ); + self::$mDefaultSep = $wgLang->getMessageFromDB( 'comma-separator' ); + } + + // Parser::SFH_OBJECT_ARGS available since MW 1.12 + self::initFunction( $parser, 'arraydefine' ); + self::initFunction( $parser, 'arrayprint', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arrayindex', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arraysize' ); + self::initFunction( $parser, 'arraysearch', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arraysearcharray', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arrayslice' ); + self::initFunction( $parser, 'arrayreset', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arrayunique' ); + self::initFunction( $parser, 'arraysort' ); + self::initFunction( $parser, 'arraymerge', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arrayunion', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arraydiff', Parser::SFH_OBJECT_ARGS ); + self::initFunction( $parser, 'arrayintersect', Parser::SFH_OBJECT_ARGS ); + + return true; + } + private static function initFunction( Parser &$parser, $name, $flags = 0 ) { + // all parser functions with prefix: + $prefix = ( $flags & Parser::SFH_OBJECT_ARGS ) ? 'pfObj_' : 'pf_'; + $functionCallback = array( __CLASS__, $prefix . $name ); + + $parser->setFunctionHook( $name, $functionCallback, $flags ); + } + + /** + * Returns the extensions base installation directory. + * + * @since 2.0 + * + * @return string + */ + public static function getDir() { + static $dir = null; + if( $dir === null ) { + $dir = dirname( __FILE__ ); + } + return $dir; + } + + + #################### + # Parser Functions # + #################### + + /////////////////////////////////////////////////////////// + // PART 1. Array Construction + /////////////////////////////////////////////////////////// + + /** + * Define an array by a list of 'values' deliminated by 'delimiter', + * the delimiter should be perl regular expression pattern + * usage: + * {{#arraydefine:arrayid|values|delimiter|options}} + * + * http://us2.php.net/manual/en/book.pcre.php + * see also: http://us2.php.net/manual/en/function.preg-split.php + */ + static function pf_arraydefine( + Parser &$parser, + $arrayId, + $value = null, + $delimiter = '/\s*,\s*/', + $options = '' + ) { + if ( !isset( $arrayId ) ) { + return ''; + } + + $out = ''; + $array = array(); + $trimDone = false; // whether or not we can be sure that all array elements are trimmed + + // normalize + $delimiter = trim( $delimiter ); + + if( $value === null ) { + // no element set, not even an empty one + $array = array(); + } + else { + // fill array with user input: + if( $delimiter === '' ) { + // whole input one element, also takes care of special case empty '' value and 'unique' option set + $array = array( $value ); + $trimDone = true; + } + else { + // if no regex delimiter given, build one: + if( ! self::isValidRegEx( $delimiter ) ) { + $delimiter = '/\s*' . preg_quote( $delimiter, '/' ) . '\s*/'; + $trimDone = true; // spaces are part of the delimiter now + } + $array = preg_split( $delimiter, $value ); + } + + // trim all values before unique if still necessary, otherwise unique might not work correctly + if( ! $trimDone ) { + $array = self::sanitizeArray( $array ); + } + + // now parse the options, and do posterior process on the created array + $arrayOptions = self::parse_options( $options ); + + // make it unique if option is set + if( array_key_exists( 'unique', $arrayOptions ) ) { + // unique like the parser function would do it + $array = self::array_unique( $array ); + } + + // if 'singleempty' is NOT set, {{#arraydefine:a|}} will be empty. + // by default this would give an empty array (due to historical as well as usability reasons) + if( ! array_key_exists( 'singleempty', $arrayOptions ) ) { + // there is no other uncomplicated way than this to define a single empty elemented array currently! + if( count( $array ) === 1 && $array[0] === '' ) { + $array = array(); + } + } + + /** + * @ToDo: + * The 'empty' option was introduced in r81676 but actually breaks old functionality since it will remove + * all empty elements by default. + * 'unique' already allows to remove all empty elements but it will also remove al duplicates, there should + * be a more intelligent alternative to 'unique' which allows both, to preserve or remove empty elements + * independent from removing duplicate values. + */ + /* + // remove all empty '' elements if option is NOT set + if( ! array_key_exists( 'empty', $arrayOptions ) ) { + $values = array(); // temp array so we won't have gaps (don't use unset!) + foreach ( $array as $key => $value ) { + if( $value !== '' ) { + $values[] = $elem; + } + } + $array = $values; + unset( $values ); + } + */ + + // sort array if the option is set + if( array_key_exists( 'sort', $arrayOptions ) ) { + $array = self::arraySort( $array, self::array_value( $arrayOptions, 'sort' ) ); + } + + // print the array upon request + switch( self::array_value( $arrayOptions, 'print' ) ) { + case 'list': + // simple list output + $out = implode( self::$mDefaultSep, $array ); + break; + case 'pretty': + global $wgLang; + $out = $wgLang->listToText( $array ); + break; + } + } + + self::get( $parser )->setArray( $arrayId, $array ); + + return $out; + } + + + /////////////////////////////////////////////////////////// + // PART 2. Extracting Information + /////////////////////////////////////////////////////////// + + + /** + * print an array. + * foreach element of the array, print 'subject' where all occurrences of 'search' is replaced with the element, + * and each element print-out is deliminated by 'delimiter' + * The subject can embed parser functions; wiki links; and templates. + * usage: + * {{#arrayprint:arrayid|delimiter|search|subject|options}} + * examples: + * {{#arrayprint:b}} -- simple + * {{#arrayprint:b|<br/>}} -- add change line + * {{#arrayprint:b|<br/>|@@@|[[@@@]]}} -- embed wiki links + * {{#arrayprint:b|<br/>|@@@|{{#set:prop=@@@}} }} -- embed parser function + * {{#arrayprint:b|<br/>|@@@|{{f.tag{{f.print.vbar}}prop{{f.print.vbar}}@@@}} }} -- embed template function + * {{#arrayprint:b|<br/>|@@@|[[name::@@@]]}} -- make SMW links + */ + static function pfObj_arrayprint( Parser &$parser, PPFrame $frame, $args ) { + global $egArraysCompatibilityMode, $egArraysExpansionEscapeTemplates; + + // Get Parameters + $arrayId = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; + $delimiter = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : self::$mDefaultSep; + /* + * PPFrame::NO_ARGS and PPFrame::NO_TEMPLATES for expansion make a lot of sense here since the patterns getting replaced + * in $subject before $subject is being parsed. So any template or argument influence in the patterns wouldn't make any + * sense in any sane scenario. + */ + $search = isset( $args[2] ) ? trim( $frame->expand( $args[2], PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES ) ) : null; + $subject = isset( $args[3] ) ? trim( $frame->expand( $args[3], PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES ) ) : null; + // options array: + $options = isset( $args[4] ) + ? self::parse_options( $frame->expand( $args[4] ) ) + : array(); + + + // get array, null if non-existant: + $array = self::get( $parser )->getArray( $arrayId ); + + if( $array === null ) { + // array we want to print doesn't exist! + if( ! $egArraysCompatibilityMode ) { + return ''; + } else { + // COMPATIBILITY-MODE + return "undefined array: $arrayId"; + } + } + + // if there is no subject, there is no point in expanding. Faster! + if( $subject === null ) { + if( ! $egArraysCompatibilityMode && $egArraysExpansionEscapeTemplates !== null ) { + // we can ignore options here, since if subject is null, options won't be set as well! + return trim( implode( $delimiter, $array ) ); + } else { + // COMPATIBILITY-MODE + // set search and subject so the old routine can be done + $search = $subject = '@@@@'; + } + } + + $rendered_values = array(); + + foreach( $array as $val ) { + + if( ! $egArraysCompatibilityMode ) { + // NO COMPATIBILITY-MODE + /** + * escape the array value so it won't destroy the users wiki markup expression. + */ + $val = self::escapeForExpansion( $val ); + } + // replace place holder with current value: + $rawResult = str_replace( $search, $val, $subject ); + /* + * $subjectd still is un-expanded (this allows to use some parser functions like + * {{FULLPAGENAME:@@@@}} directly without getting parsed before @@@@ is replaced. + * Expand it so we replace templates like {{!}} which we need for the final parse. + */ + $rawResult = $parser->preprocessToDom( $rawResult, $frame->isTemplate() ? Parser::PTD_FOR_INCLUSION : 0 ); + $rawResult = trim( $frame->expand( $rawResult ) ); + + $rendered_values[] = $rawResult; + } + + // follow special print options: + switch( self::array_value( $options, 'print' ) ) { + case 'pretty': + // pretty list print with ' and ' connecting the last two items + if( $delimiter === '' ) { + // '' as delimiter isn't pretty, so in this case we take the (languages) default + $output = self::arrayToText( $rendered_values ); + } else { + $output = self::arrayToText( $rendered_values, $delimiter ); + } + break; + + default: + // normal print with one delimiter, might be the languages default + $output = implode( $delimiter, $rendered_values ); + break; + } + + if( $egArraysCompatibilityMode || $egArraysExpansionEscapeTemplates === null ) { + // COMPATIBLITY-MODE: + /* + * don't leave the final parse to Parser::braceSubstitution() since there are some special cases where it + * would produce unexpected output (it uses a new child frame and ignores whether the frame is a template!) + */ + $output = $parser->preprocessToDom( $output, $frame->isTemplate() ? Parser::PTD_FOR_INCLUSION : 0 ); + $output = $frame->expand( $output ); + } + + return trim( $output ); + } + + /** + * print the value of an array (identified by arrayid) by the index, invalid index results in the default value being printed. note the index is 0-based. + * usage: + * {{#arrayindex:arrayid|index}} + */ + static function pfObj_arrayindex( Parser &$parser, PPFrame $frame, $args ) { + global $egArraysCompatibilityMode; + + // Get Parameters + $arrayId = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; + $rawOptions = isset( $args[2] ) ? $args[2] : ''; + + if( ! isset( $args[1] ) ) { + return ''; + } + $index = trim( $frame->expand( $args[1] ) ); + + // get value or null if it doesn't exist. Takes care of negative index as well + $val = self::get( $parser )->getArrayValue( $arrayId, $index ); + + if( $val === null || ( $val === '' && !$egArraysCompatibilityMode ) ) { + // index doesn't exist, return default (parameter 3)! + // without compatibility, also return default in case of empty string '' + + // only expand default when needed + $defaultOrOptions = trim( $frame->expand( $rawOptions ) ); + + if( $egArraysCompatibilityMode ) { + // COMPATIBILITY-MODE + // now parse the options, and do posterior process on the created array + $options = self::parse_options( $defaultOrOptions ); + $default = self::array_value( $options, 'default' ); + } else { + $default = $defaultOrOptions; + } + + return $default; + } + + return $val; + } + + /** + * returns the size of an array. + * Print the size (number of elements) in the specified array and '' if array doesn't exist + * usage: + * {{#arraysize:arrayid}} + * + * See: http://www.php.net/manual/en/function.count.php + */ + static function pf_arraysize( Parser &$parser, $arrayId ) { + $store = self::get( $parser ); + + if( ! $store->arrayExists( $arrayId ) ) { + return ''; + } + + return count( $store->getArray( $arrayId ) ); + } + + + /** + * locate the index of the first occurrence of an element starting from the 'index' + * - print "-1" (not found) or index (found) to show the index of the first occurrence of 'value' in the array identified by arrayid + * - if 'yes' and 'no' are set, print value of them when found or not-found + * - index is 0-based , it must be non-negative and less than lenth + * usage: + * {{#arraysearch:arrayid|value|index|yes|no}} + * + * See: http://www.php.net/manual/en/function.array-search.php + * note it is extended to support regular expression match and index + */ + static function pfObj_arraysearch( Parser &$parser, PPFrame $frame, $args ) { + // Get Parameters + $arrayId = trim( $frame->expand( $args[0] ) ); + $index = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : 0; + + $store = self::get( $parser ); + + if( $store->arrayExists( $arrayId ) + && $store->validate_array_index( $arrayId, $index, false ) + ) { + $array = $store->getArray( $arrayId ); + + // validate/build search regex: + if( isset( $args[1] ) ) { + + $needle = trim( $frame->expand( $args[1] ) ); + + if ( ! self::isValidRegEx( $needle ) ) { + $needle = '/^\s*' . preg_quote( trim( $needle ), '/' ) . '\s*$/'; + } + } + else { + $needle = '/^\s*$/'; + } + + // search for a match inside the array: + $total = count( $array ); + for ( $i = $index; $i < $total; $i++ ) { + $value = $array[ $i ]; + + if ( preg_match( $needle, $value ) ) { + // found! + if ( isset( $args[3] ) ) { + // Expand only when needed! + return trim( $frame->expand( $args[3] ) ); + } + else { + // return index of first found item + return $i; + } + } + } + } + + // no match! (Expand only when needed!) + if( isset( $args[4] ) ) { + $no = trim( $frame->expand( $args[4] ) ); + } else { + global $egArraysCompatibilityMode; + $no = $egArraysCompatibilityMode + ? '-1' // COMPATIBILITY-MODE + : ''; + } + return $no; + } + + /** + * search an array and create a new array with all the results. Transforming the new entries before storing them is possible too. + * usage: + * {{#arraysearcharray:arrayid_new|arrayid|needle|index|limit|transform}} + * + * "needle" can be a regular expression or a string search value. If "needle" is a regular expression, "transform" can contain + * "$n" where "n" stands for a number to access a variable from the regex result. + */ + static function pfObj_arraysearcharray( Parser &$parser, PPFrame $frame, $args ) { + $store = self::get( $parser ); + + // get first two parameters + $arrayId = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : null; + $arrayId_new = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : ''; + + if( $arrayId === null ) { + global $egArraysCompatibilityMode; + if( ! $egArraysCompatibilityMode ) { // COMPATIBILITY-MODE + $store->setArray( $arrayId_new ); + } + return ''; + } + + // Get Parameters the other parameters + $needle = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '/^(\s*)$/'; + $index = isset( $args[3] ) ? trim( $frame->expand( $args[3] ) ) : 0; + $limit = isset( $args[4] ) ? trim( $frame->expand( $args[4] ) ) : ''; + $rawTransform = isset( $args[5] ) ? $args[5] : null; + + // also takes care of negative index by calculating start index: + $validIndex = $store->validate_array_index( $arrayId, $index, false ); + + // make sure at least empty array exists but don't overwrite data + // we still need in case new array ID same as target array ID + $array = $store->getArray( $arrayId ); + $store->setArray( $arrayId_new ); + + if( $array === null || !$validIndex ) { + return ''; + } + + // non-numeric limit will be set to 0, except limit was omitted ('') + $limit = $limit === '' ? -1 : (int)$limit; + if( $limit === 0 ) { + return ''; + } + + $newArr = array(); + + $regexFunSupport = self::hasRegexFunSupport(); + if( ! self::isValidRegEx( $needle, $regexFunSupport ) ) { + $needle = '/^\s*(' . preg_quote( $needle, '/' ) . ')\s*$/'; + } + + // search the array for all matches and put them in the new array + $total = count( $array ); + for( $i = $index; $i < $total; $i++ ) { + + $value = $array[ $i ]; + + if( preg_match( $needle, $value ) ) { + // Found something! + if( $rawTransform !== null ) { + // Transform the matching string. Can we use 'Regex Fun' with special 'e' flag support ? + if( $regexFunSupport ) { + // do the transformation with Regex Fun to support 'e' flag: + $transform = trim( $frame->expand( + $rawTransform, + PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES // leave expanding of templates to 'Regex Fun' + ) ); + $value = ExtRegexFun::doPregReplace( + $needle, + $rawTransform, + $value, + -1, + $parser, + $frame, + array( ExtRegexFun::FLAG_REPLACEMENT_PARSE ) + ); + } + else { + // regular preg_replace: + $transform = trim( $frame->expand( $rawTransform ) ); + $value = preg_replace( $needle, $transform, $value ); + } + } + $newArr[] = trim( $value ); + + // stop if limit is reached, limit -1 means no limit + if( --$limit === 0 ) { + break; + } + } + } + + // set new array: + $store->setArray( $arrayId_new, $newArr ); + return ''; + } + + /** + * extract a slice from an array + * usage: + * {{#arrayslice:arrayid_new|arrayid|offset|length}} + * + * extract a slice from an array + * see: http://www.php.net/manual/en/function.array-slice.php + */ + static function pf_arrayslice( Parser &$parser, $arrayId_new, $arrayId = null , $offset = 0, $length = null ) { + $store = self::get( $parser ); + if( $arrayId === null ) { + global $egArraysCompatibilityMode; + if( ! $egArraysCompatibilityMode ) { // COMPATIBILITY-MODE + $store->setArray( $arrayId_new ); + } + return ''; + } + // get target array before overwriting it in any way + $array = $store->getArray( $arrayId ); + + // make sure at least an empty array exists if we return early + $store->setArray( $arrayId_new ); + + if( $array === null + || ! is_numeric( $offset ) // don't ignore invalid offset + ) { + return ''; + } + + if( ! is_numeric( $length ) ) { + $length = null; // ignore invalid input, slice till end + } + + // array_slice will re-organize keys + $newArray = array_slice( $array, $offset, $length ); + $store->setArray( $arrayId_new, $newArray ); + + return ''; + } + + + /////////////////////////////////////////////////////////// + // PART 3. Array Alteration + /////////////////////////////////////////////////////////// + + /** + * reset some or all defined arrayes + * usage: + * {{#arrayreset:}} + * {{#arrayreset:arrayid1,arrayid2,...arrayidn}} + */ + static function pfObj_arrayreset( Parser &$parser, PPFrame $frame, $args) { + global $egArraysCompatibilityMode; + + if( $egArraysCompatibilityMode && count( $args ) == 1 ) { + /* + * COMPATIBILITY-MODE: before arrays were separated by ';' which is an bad idea since + * the ',' is an allowed character in array names! + */ + $args = preg_split( '/\s*,\s*/', trim( $frame->expand( $args[0] ) ) ); + } + + $store = self::get( $parser ); + + // reset all hash tables if no specific tables are given: + if( ! isset( $args[0] ) || ( $args[0] === '' && count( $args ) == 1 ) ) { + // reset ALL arrays! + $store->mArrays = array(); + } + else { + // reset specific hash tables: + foreach( $args as $arg ) { + $arrayId = trim( $frame->expand( $arg ) ); + $store->unsetArray( $arrayId ); + } + } + return ''; + } + + + /** + * convert an array to a set + * convert the array identified by arrayid into a set (all elements are unique) + * also removes empty '' elements from the array + * usage: + * {{#arrayunique:arrayid}} + * + * see: http://www.php.net/manual/en/function.array-unique.php + */ + static function pf_arrayunique( Parser &$parser, $arrayId ) { + $store = self::get( $parser ); + + if( $store->arrayExists( $arrayId ) ) { + $array = $store->getArray( $arrayId ); + $array = self::array_unique( $array ); + $store->setArray( $arrayId, $array ); + } + return ''; + } + + + /** + * sort specified array in the following order: + * - none: No sort (default) + * - desc: In descending order, large to small + * - asce: In ascending order, small to large + * - random: Shuffle the arrry in random order + * - reverse: Return an array with elements in reverse order + * usage: + * {{#arraysort:arrayid|order}} + * + * see: http://www.php.net/manual/en/function.sort.php + * http://www.php.net/manual/en/function.rsort.php + * http://www.php.net/manual/en/function.shuffle.php + * http://us3.php.net/manual/en/function.array-reverse.php + */ + static function pf_arraysort( Parser &$parser, $arrayId , $sort = 'none' ) { + $store = self::get( $parser ); + + $array = $store->getArray( $arrayId ); + + if( $array === null ) { + return ''; + } + + // sort array and store it + $array = self::arraySort( $array, $sort ); + $store->setArray( $arrayId, $array ); + return ''; + } + + + /////////////////////////////////////////////////////////// + // PART 4. Array Interaction + /////////////////////////////////////////////////////////// + + /** + * Merge values two arrayes identified by arrayid1 and arrayid2 into a new array identified by arrayid_new. + * This merge differs from array_merge of php because it merges values. + * + * Usage: + * {{#arraymerge:arrayid_new |array1 |array2 |... |array n}} + * See: http://www.php.net/manual/en/function.array-merge.php + */ + static function pfObj_arraymerge( &$parser, $frame, $args) { + self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); + return ''; + } + private function multi_arraymerge( $array1, $array2 ) { + // keys will not be re-organized + return array_merge( $array1, $array2 ); + } + + /** + * Usage: + * {{#arrayunion:arrayid_new|arrayid1|arrayid2}} + * + * Set operation, {red, white} = {red, white} union {red} + * Similar to arraymerge but with unique values. This union works on values. + */ + static function pfObj_arrayunion( &$parser, $frame, $args) { + self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); + return ''; + } + private function multi_arrayunion( $array1, $array2 ) { + // keys will not be re-organized + return array_unique( array_merge( $array1, $array2 ) ); + } + + /** + * Usage: + * {{#arrayintersect:arrayid_new |array1 |array2 |... |array n}} + * + * Set operation, {red} = {red, white} intersect {red,black} + * See: http://www.php.net/manual/en/function.array-intersect.php + */ + static function pfObj_arrayintersect( &$parser, $frame, $args) { + self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); + return ''; + } + private function multi_arrayintersect( $array1, $array2 ) { + // keys will be preserved! + return array_intersect( $array1, $array2 ); + } + + /** + * + * Usage: + * {{#arraydiff:arrayid_new |array1 |array2 |... |array n}} + * + * Set operation, {white} = {red, white} - {red} + * See: http://www.php.net/manual/en/function.array-diff.php + */ + static function pfObj_arraydiff( &$parser, $frame, $args) { + self::get( $parser )->multiArrayOperation( $frame, $args, __FUNCTION__, false ); + return ''; + } + private function multi_arraydiff( $array1, $array2 ) { + // keys will be preserved! + return array_diff( $array1, $array2 ); + } + + + ################## + # Private helper # + ################## + + /** + * Base function for operations with multiple arrays given thru n parameters + * $operationFunc expects a function name prefix (suffix 'multi_') with two parameters + * $array1 and $array2 which will perform an action between $array1 and $array2 which + * will result into a new $array1. There can be 1 to n $hash2 in the whole process. + * + * Note: This function is similar to that of Extension:HashTables. + * + * @since 2.0 + * + * @param $frame PPFrame + * @param $args array + * @param $operationFunc string name of the function calling this. There must be a counterpart + * function with prefix 'multi_' which should have two parameters. Both parameters + * will receive an array, the function must return the result array of the processing. + * @param $runFuncOnSingleArray boolean whether the $operationFunc function should be run in case + * only one array id is given. If not, the original array will end up in the new array. + */ + protected function multiArrayOperation( PPFrame $frame, array $args, $operationFunc, $runFuncOnSingleArray = true ) { + $lastArray = null; + $operationRan = false; + $finalArrayId = trim( $frame->expand( $args[0] ) ); + $operationFunc = 'multi_' . preg_replace( '/^pfObj_/', '', $operationFunc ); + + // For all arrays given in parameters 2 to n (ignore 1 because this is the name of the new array) + for( $i = 1; $i < count( $args ); $i++ ) { + // just make sure we don't fall into gaps of given arguments: + if( ! array_key_exists( $i, $args ) ) { + continue; + } + $argArrayId = trim( $frame->expand( $args[ $i ] ) ); + + // ignore all tables which do not exist + if( $this->arrayExists( $argArrayId ) ) { + $argArray = $this->getArray( $argArrayId ); + if( $lastArray === null ) { + // first valid array, process together with second... + $lastArray = $argArray; + } + else { + // second or later hash table, process with previous: + $lastArray = $this->{ $operationFunc }( $lastArray, $argArray ); // perform action between last and current array + $operationRan = true; + } + } + } + + // in case no array was given at all: + if( $lastArray === null ) { + $lastArray = array(); + } + + global $egArraysCompatibilityMode; + + if( ! $operationRan && $egArraysCompatibilityMode + && $operationFunc !== 'multi_arraymerge' // only exception was 'arraymerge' + ) { + /* + * COMPATIBILITY-MODE: + * Before version 2.0 we didn't create a new array in case only one array was given. + * The only exception was 'arraymerge' which did duplicate the array. + */ + return ''; + } + + // if the operation didn't run because there was only one or no array: + if( ! $operationRan && $runFuncOnSingleArray ) { + $lastArray = $this->{ $operationFunc }( $lastArray ); + } + + // re-organize all keys since some 'multi_' functions will preserve keys! + $lastArray = array_merge( $lastArray ); + + $this->setArray( $finalArrayId, $lastArray ); + } + + /** + * Validates an index for an array and returns true in case the index is a valid index within + * the array. This also changes the index value, which is given by reference, in case it is + * set to a negative value. In case $strictIndex is set to false, further transforming of + * $index might be done - in the same cases normally the function would return false. + * + * @param string $arrayId + * @param mixed &$index + * @param bool $strictIndex Whether non-numeric indexes and negative indexes which would + * end up out of range, below 0, should be set to 0 automatically. + * + * @return boolean + */ + protected function validate_array_index( $arrayId, &$index, $strictIndex = false ) { + if( ! is_numeric( $index ) ) { + if( $strictIndex ) { + return false; + } else { + $index = 0; + } + } + $index = (int)$index; + + if( ! array_key_exists( $arrayId, $this->mArrays ) ) { + return false; + } + + $array = $this->mArrays[ $arrayId ]; + + // calculate start index for negative start indexes: + if( $index < 0 ) { + $index = count( $array ) + $index; + if ( $index < 0 && !$strictIndex ) { + $index = 0; + } + } + + if( ! isset( $array ) ) { + return false; + } + if( ! array_key_exists( $index, $array ) ) { + return false; + } + return true; + } + + /** + * private function for validating array by name + * @ToDo: get rid of this! + * @deprecated + */ + protected function validate_array_by_arrayId( $arrayId ) { + if( ! isset( $arrayId ) ) { + return ''; + } + if( ! isset( $this->mArrays ) + || ! array_key_exists( $arrayId, $this->mArrays ) + || ! is_array( $this->mArrays[ $arrayId ] ) + ) { + global $egArraysCompatibilityMode; + if( $egArraysCompatibilityMode ) { + return "undefined array: $arrayId"; // COMPATIBILITY-MODE + } else { + return ''; + } + } + + return true; + } + + /** + * Convenience function to get a value from an array. Returns '' in case the + * value doesn't exist or no array was given + * + * @return string + */ + protected static function array_value( $array, $field ) { + if ( is_array( $array ) && array_key_exists( $field, $array ) ) { + return $array[ $field ]; + } + return ''; + } + + /** + * Parses a string of options separated by ','. Options can be just certain key-words or + * key-value pairs separated by '='. Options are case-insensitive and spacing between + * separators will be ignored. + */ + protected static function parse_options( $options ) { + if( ! isset( $options ) ) { + return array(); + } + + // now parse the options, and do posterior process on the created array + $options = preg_split( '/\s*,\s*/', strtolower( trim( $options ) ) ); + + $ret = array(); + foreach( $options as $option ) { + $optPair = preg_split( '/\s*\=\s*/', $option, 2 ); + if( sizeof( $optPair ) == 1 ) { + $ret[ $optPair[0] ] = true; + } else { + $ret[ $optPair[0] ] = $optPair[1]; + } + } + return $ret; + } + + /** + * same as self::arrayUnique() but without sanitazation, only for internal use. + */ + protected static function array_unique( array $array ) { + // delete duplicate values + $array = array_unique( $array ); + + $values = array(); + foreach( $array as $key => $val ) { + // don't put emty elements into the array + if( $val !== '' ) { + $values[] = $val; + } + } + + return $values; + } + + + ############## + # Used Hooks # + ############## + + static function onParserClearState( Parser &$parser ) { + // remove all arrays to avoid conflicts with job queue or Special:Import or SMW semantic updates + $parser->mExtArrays = new self(); + return true; + } + + + #################################### + # Public functions for interaction # + #################################### + # + # public non-parser functions, accessible for + # other extensions doing interactive stuff + # with the Array extension. + # + + /** + * Convenience function to return the 'Arrays' extensions array store connected + * to a certain Parser object. Each parser has its own store which will be reset after + * a parsing process [Parser::parse()] has finished. + * + * @since 2.0 + * + * @param Parser &$parser + * + * @return ExtArrays by reference so we still have the right object after 'ParserClearState' + */ + public static function &get( Parser &$parser ) { + return $parser->mExtArrays; + } + + /** + * Returns an array identified by $arrayId. If it doesn't exist, null will be returned. + * + * @since 2.0 + * + * @param string $arrayId + * + * @return array|null + */ + function getArray( $arrayId ) { + $arrayId = trim( $arrayId ); + if( $this->arrayExists( $arrayId ) ) { + return $this->mArrays[ $arrayId ]; + } + return null; + } + + /** + * This will add a new array or overwrite an existing one. Values should be delliverd as array + * values in form of a string. The array will be sanitized internally. + * + * @param string $arrayId + * @param array $array + */ + public function createArray( $arrayId, $array = array() ) { + $array = self::sanitizeArray( $array ); + $this->mArrays[ trim( $arrayId ) ] = $array; + } + + /** + * Same as the public function createArray() but without sanitizing the array automatically. + * This is save and faster for internal usage, just be sure your array doesn't have un-trimmed + * values or non-numeric or negative array keys and no gaps between keys. + * + * @param string $arrayId + * @param array $array + */ + protected function setArray( $arrayId, $array = array() ) { + $this->mArrays[ trim( $arrayId ) ] = $array; + } + + /** + * Returns whether a certain array is defined within the page scope. + * + * @param string $arrayId + * + * @return boolean + */ + function arrayExists( $arrayId ) { + return array_key_exists( trim( $arrayId ), $this->mArrays ); + } + + /** + * Returns a value within an array. If key or array do not exist, this will return null + * or another predefined default. $index can also be a negative value, in this case the + * value that far from the end of the array will be returned. + * + * @since 2.0 + * + * @param string $arrayId + * @param string $index + * @param mixed $default value to return in case the value doesn't exist. null by default. + * + * @return string|null + */ + function getArrayValue( $arrayId, $index, $default = null ) { + $arrayId = trim( $arrayId ); + if( $this->arrayExists( $arrayId ) + && $this->validate_array_index( $arrayId, $index, true ) + && array_key_exists( $index, $this->mArrays[ $arrayId ] ) + ) { + return $this->mArrays[ $arrayId ][ $index ]; + } + else { + return $default; + } + } + + + /** + * Removes an existing array. If array didn't exist this will return false, otherwise true. + * + * @since 2.0 + * + * @param string $arrayId + * + * @return boolean whether the array existed and has been removed + */ + public function unsetArray( $arrayId ) { + $arrayId = trim( $arrayId ); + if( $this->arrayExists( $arrayId ) ) { + unset( $this->mArrays[ $arrayId ] ); + return true; + } + return false; + } + + /** + * Rebuild the array and reorganize all keys, trim all values. + * All gaps between array items will be closed. + * + * @since 2.0 + * + * @param array $arr array to be reorganized + * @return array + */ + public static function sanitizeArray( $array ) { + $newArray = array(); + foreach( $array as $val ) { + $newArray[] = trim( $val ); + } + return $newArray; + } + + /** + * Removes duplicate values and all empty elements from an array just like the + * '#arrayunique' parser function would do it. The array will be sanitized internally. + * + * @since 2.0 + * + * @param array $array + * + * @return array + */ + public static function arrayUnique( array $array ) { + return self::array_unique( self::sanitizeArray( $array ) ); + } + + /** + * Sorts an array just like parser function '#arraysort' would do it and allows the + * same sort modes. + * + * @since 2.0 + * + * @param array $array + * @param string $sortMode one of the following sort modes: + * - random: random array order + * - reverse: last entry will be first, first the last. + * - asce: sort array in ascending order. + * - desc: sort array in descending order. + * - natural: sort with a 'natural order' algorithm. See PHPs natsort() function. + * + * In addition, this function allows to set several flags behind the sort mode. The must be + * separated by a space. The following keys are allowed: + * - nolocale: will prevent 'asce' and 'desc' mode to considering PHP-defined language rules. + * + * @return array + */ + public static function arraySort( array $array, $sortMode ) { + global $egArraysCompatibilityMode; + + $flags = preg_split( '/\s+/s', $sortMode ); + $sortMode = array_shift( $flags ); // first string is the actual sort mode + + $localeFlag = SORT_LOCALE_STRING; // sort strings accordingly to what was set via setlocale() + if( + in_array( 'nolocale', $flags ) + || $egArraysCompatibilityMode // COMPATIBILITY-MODE + ) { + // 'nolocale' will prevent from using this flag! + $localeFlag = null; + } + + // do the requested sorting of the given array: + switch( $sortMode ) { + case 'asc': + case 'asce': + case 'ascending': + sort( $array, $localeFlag ); + break; + + case 'desc': + case 'descending': + rsort( $array, $localeFlag ); + break; + + case 'nat': + case 'natural': + natsort( $array ); + break; + + case 'rand': + case 'random': + shuffle( $array ); + break; + + case 'reverse': + $array = array_reverse( $array ); + break; + }; + return $array; + } + + /** + * Pretty much the same as Language::listToText() but allows us to set a custom comma separator. + * + * @since 2.0 + * + * @param Array $array + * @param string $commaSep + * + * @return string + */ + public static function arrayToText( $array, $commaSep = null ) { + global $wgLang; + $commaSep = $commaSep === null ? self::$mDefaultSep : $commaSep; + $s = ''; + $m = count( $array ) - 1; + if ( $m == 1 ) { + return $array[0] . $wgLang->getMessageFromDB( 'and' ) . $wgLang->getMessageFromDB( 'word-separator' ) . $array[1]; + } else { + for ( $i = $m; $i >= 0; $i-- ) { + if ( $i == $m ) { + $s = $array[$i]; + } else if ( $i == $m - 1 ) { + $s = $array[$i] . $wgLang->getMessageFromDB( 'and' ) . $wgLang->getMessageFromDB( 'word-separator' ) . $s; + } else { + $s = $array[$i] . $commaSep . $s; + } + } + return $s; + } + } + + /** + * Escapes a string so it can be used within PPFrame::expand() expansion without actually being + * changed because of special characters. + * Respects the configuration variable '$egArraysEscapeTemplates'. + * + * This is a workaround for bug #32829 + * + * @since 2.0 + * + * @param string $string + * @return string + */ + public static function escapeForExpansion( $string ) { + global $egArraysExpansionEscapeTemplates; + + if( $egArraysExpansionEscapeTemplates === null ) { + return $string; + } + + $string = strtr( + $string, + $egArraysExpansionEscapeTemplates + ); + + return $string; + } + + /** + * Decides for the given $pattern whether its a valid regular expression acceptable for + * Arrays parser functions or not. + * + * @param string $pattern regular expression including delimiters and optional flags + * @param bool $forRegexFun whether the regular expression is inteded to be used with 'Regex Fun' + * if supported by the wikis infrastructure. In case 'Regex Fun' is not available, + * the default validation will be used. + * + * @return boolean + */ + static function isValidRegEx( $pattern, $forRegexFun = false ) { + if( $forRegexFun && self::hasRegexFunSupport() ) { + return ExtRegexFun::validateRegex( $pattern ); + } + + if( ! preg_match( '/^([\\/\\|%]).*\\1[imsSuUx]*$/', $pattern ) ) { + return false; + } + wfSuppressWarnings(); // instead of using the evil @ operator! + $isValid = false !== preg_match( $pattern, ' ' ); // preg_match returns false on error + wfRestoreWarnings(); + return $isValid; + } + + /** + * Whether 'Regex Fun' extension is available in this wiki to take over preg_replace handling + * for '#arraysearcharray' function. + */ + static function hasRegexFunSupport() { + static $support = null; + if( $support === null ) { + $support = ( + defined( 'ExtRegexFun::VERSION' ) + && version_compare( ExtRegexFun::VERSION, '1.1', '>=' ) + ); + } + return $support; + } +} diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..c82ab21 --- /dev/null +++ b/extension.json @@ -0,0 +1,32 @@ +{ + "name": "Arrays", + "version": "2.1.0", + "author": [ + "Li Ding", + "Jie Bao" + ], + "url": "https://www.mediawiki.org/wiki/Extension:Arrays", + "descriptionmsg": "arrays-desc", + "license-name": "MIT", + "type": "parserhook", + "MessagesDirs": { + "Arrays": [ + "i18n" + ] + }, + "ExtensionMessagesFiles": { + "ArraysMagic": "Arrays.i18n.magic.php" + }, + "Hooks": { + "ParserFirstCallInit": "ExtArrays::init", + "ParserClearState": "ExtArrays::onParserClearState" + }, + "ParserTestFiles": [ + "arrayParserTests.txt", + "arrayLoopsInteractionParserTests.txt" + ], + "AutoloadClasses": { + "ExtArrays": "ExtArrays.class.php" + }, + "manifest_version": 2 +} -- To view, visit https://gerrit.wikimedia.org/r/405057 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ieeb16525f7e45437c53239cbe6d813fd179ba6a5 Gerrit-PatchSet: 2 Gerrit-Project: mediawiki/extensions/Arrays Gerrit-Branch: master Gerrit-Owner: Jayprakash12345 <0freerunn...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits