Extension:Control Structure Functions/ControlStructureFunctions.php
From MediaWiki.org
ControlStructureFunctions.php:
<?php /** * Functions for control structures (i.e. if, and switch statments, and loops. * These functions support nested magic words, parser function calls and wiki * tags by way of character escapes so that their parsed/resolved/executed only * when they need to be and not prematurely. * * @package MediaWiki * @subpackage Extensions * * @link http://www.mediawiki.org/wiki/Extension:Control_Structure_Functions * Documentation * * @author David M. Sledge * @copyright Copyright © 2007 David M. Sledge * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 * or later * @version 0.9.0 * initial creation * @version 0.9.1 * removed unstrip code since the CharacterEscape API does it * automatically * @version 0.9.2 * restructured while and dowhile functions for performance * loops are now limited by the public static member $mMaxLoops * added "ifexist" to the allowed if* comparisons to the while and * dowhile functions * @version 0.9.3 * added error handling for ifexpr in the loops * changed credits so it doesn't break Special:Version page * made wfCtrlStructFuncLanguageGetMagic into the static class method * ExtCtrlStructFunc::languageGetMagic */ if ( !defined( 'MEDIAWIKI' ) ) { die( 'This file is a MediaWiki extension, it is not a valid entry point' ); } define('CTRL_STRUCT_FUNC_VERSION', '0.9.3'); // current version $wgExtensionFunctions[] = array( 'ExtCtrlStructFunc', 'setup' ); $wgExtensionCredits['parserhook'][] = array( 'name' => 'Control Structure Functions', 'version' => CTRL_STRUCT_FUNC_VERSION, 'description' => 'Functions for control structures (i.e. if, and switch ' . 'statements, and loops)', 'url' => 'http://www.mediawiki.org/wiki/Extension:Control_Structure_Functions', 'author' => 'David M. Sledge' ); $wgHooks['LanguageGetMagic'][] = 'ExtCtrlStructFunc::languageGetMagic'; class ExtCtrlStructFunc { public static $mMaxLoops = -1; // maximum number of loops allowed // (-1 = no limit) private static $mLoopCount = 0; // number of loops performed private static $mExprParser; private static $parserFunctions = array( 'if' => 'ifHook', 'ifeq' => 'ifeq', 'ifexpr' => 'ifexpr', 'switch' => 'switchHook', 'ifexist' => 'ifexist', 'dowhile' => 'dowhile', 'while' => 'whileHook', ); public static function setup() { global $wgParser, $wgMessageCache; foreach( self::$parserFunctions as $hook => $function ) $wgParser->setFunctionHook( $hook, array( __CLASS__, $function ) ); require_once( dirname( __FILE__ ) . '/ControlStructureFunctions.i18n.php' ); foreach( CtrlStructFunc_i18n::getMessages() as $lang => $messages ) $wgMessageCache->addMessages( $messages, $lang ); } public static function languageGetMagic( &$magicWords, $langCode ) { require_once( dirname( __FILE__ ) . '/ControlStructureFunctions.i18n.php' ); foreach( CtrlStructFunc_i18n::magicWords( $langCode ) as $word => $trans ) $magicWords[$word] = $trans; return true; } private static function &getExprParser() { if ( !isset( self::$mExprParser ) ) { if ( !class_exists( 'ExprParser' ) ) { require_once( dirname( __FILE__ ) . '/../LOParserFunctions/Expr.php' ); ExprParser::addMessages(); } self::$mExprParser = new ExprParser; } return self::$mExprParser; } private static function getIfFuncName( $ifLocal ) { global $wgContLang; $funcName = null; // get the magic words for this language code $words = CtrlStructFunc_i18n::magicWords( $wgContLang->getCode() ); // we're only interested synonyms for the words // 'if', 'ifeq', 'ifexpr', and 'ifexist' $words = array( 'if' => $words['if'], 'ifeq' => $words['ifeq'], 'ifexpr' => $words['ifexpr'], 'ifexist' => $words['ifexist'], ); foreach ( $words as $word => $synons ) { // whether or not the synonym is case-sensitive $case = array_shift( $synons ); // add the shortcuts '', 'eq', 'expr', and // 'exist' to the list of synonyms $synons[] = substr( $word, 2 ); foreach ( $synons as $syn ) { if ( $case ? !strcmp( $ifLocal, $syn ) : !strcasecmp( $ifLocal, $syn ) ) { $funcName = self::$parserFunctions[$word]; break; } } if ( $funcName !== null ) break; } return $funcName; } /** * The condition(s) that determines when the loops are performed is * evaluated as if the appropriate #if*: parser function had been called * (see Usage below). #if:, #ifeq:, #ifexpr: are the parser functions that * are supported. * * Usage: * * {{ * #while: < "if" | "" > * | <condition string> * | <block statement> * }} * * {{ * #while: < "ifeq" | "eq" > * | <comparison string 1> * | <comparison string 2> * | <block statement> * }} * * {{ * #while: < "ifexpr" | "expr" > * | <boolean expression> * | <block statement> * }} * * Templates (but not template parameters), magic words, tags, and other * parser functions must be escaped (see below) unless it is desired that * they be evaluated before the loop begins. Wiki tables must always be * escaped in order to be rendered. * * Character Escapes: * \l is translated to < * \g is translated to > * \o is translated to {{ * \c is translated to }} * \p is translated to | * * Example: * The wiki code below is the equivalent of following php code: * * for ( var $i = 0; $i < 5; $i++ ) * echo '\n' . $i . '\n'; * * {{ * #vardefine: i | 0 * }}{{ * #while: expr | \o #var: i \c < 5 * |\o #var: i \c * * \o * #vardefine: i * \p \o #expr: \o #var: i \c + 1 \c * \c * }} * * Escapes can be automated using the the <esc> tag: * * {{ * #vardefine: i | 0 * }}{{ * #while: expr | <esc>{{ #var: i }} < 5</esc> * | <esc>{{ #var: i }} * * {{ * #vardefine: i * | {{ #expr: {{ #var: i }} + 1 }} * }}</esc> * }} * */ public static function whileHook( &$parser, $ifFuncHook = '', $test = '' ) { $ifFuncName = self::getIfFuncName( $ifFuncHook ); $test = CharacterEscapes::charUnesc( $test, array(), $parser ); $args = array_slice( func_get_args(), 3 ); if ( $ifFuncName === null ) return wfMsgForContent( 'ctrl_struct_func_unsupp_if' ); // ifeq has two test conditions if ( $ifFuncName == 'ifeq' ) $test2 = isset( $args[0] ) ? CharacterEscapes::charUnesc( array_shift( $args ), array(), $parser ) : ''; $statement = isset( $args[0] ) ? CharacterEscapes::charUnesc( $args[0], array(), $parser ) : ''; $ifArgs = array( &$parser, $parser->replaceVariables( $test, end( $parser->mArgStack ) ) ); // ifeq has two test conditions if ( $ifFuncName == 'ifeq' ) $ifArgs[] = $parser->replaceVariables( $test2, end( $parser->mArgStack ) ); $ifArgs[] = '1'; $output = ''; while ( ( $ifReturn = call_user_func_array( array( __CLASS__, $ifFuncName), $ifArgs ) ) === '1' ) { if ( self::$mMaxLoops >= 0 && ++self::$mLoopCount > self::$mMaxLoops ) return wfMsgForContent( 'ctrl_struct_func_loop_max' ); $output .= $parser->replaceVariables( $statement, end( $parser->mArgStack ) ); $ifArgs = array( &$parser, $parser->replaceVariables( $test, end( $parser->mArgStack ) ) ); // ifeq has two test conditions if ( $ifFuncName == 'ifeq' ) $ifArgs[] = $parser->replaceVariables( $test2, end( $parser->mArgStack ) ); $ifArgs[] = '1'; } //return '<pre><nowiki>'. $output . '</nowiki></pre>'; return $ifReturn == '' ? $output : $ifReturn; } /** * dowhile works exactly like while except that * it's guaranteed to run at least once. */ public static function dowhile( &$parser, $ifFuncHook = '', $test = '' ) { $ifFuncName = self::getIfFuncName( $ifFuncHook ); $test = CharacterEscapes::charUnesc( $test, array(), $parser ); $args = array_slice( func_get_args(), 3 ); if ( $ifFuncName === null ) return wfMsgForContent( 'ctrl_struct_func_unsupp_if' ); // ifeq has two test conditions if ( $ifFuncName == 'ifeq' ) $test2 = isset( $args[0] ) ? CharacterEscapes::charUnesc( array_shift( $args ), array(), $parser ) : ''; $statement = isset( $args[0] ) ? CharacterEscapes::charUnesc( $args[0], array(), $parser ) : ''; $output = ''; do { if ( self::$mMaxLoops >= 0 && ++self::$mLoopCount > self::$mMaxLoops ) return wfMsgForContent( 'ctrl_struct_func_loop_max' ); $output .= $parser->replaceVariables( $statement, end( $parser->mArgStack ) ); $ifArgs = array( &$parser, $parser->replaceVariables( $test, end( $parser->mArgStack ) ) ); // ifeq has two test conditions if ( $ifFuncName == 'ifeq' ) $ifArgs[] = $parser->replaceVariables( $test2, end( $parser->mArgStack ) ); $ifArgs[] = '1'; } while ( ( $ifReturn = call_user_func_array( array( __CLASS__, $ifFuncName), $ifArgs ) ) === '1' ); //return '<pre><nowiki>'. $output . '</nowiki></pre>'; return $ifReturn == '' ? $output : $ifReturn; } /** * This function returns the else text only if the expression text * evaluates to zero. * * Usage: * * {{ * #ifexpr: <expression> * | <then text> * | <else text> * }} * * Templates (but not template parameters), magic words, tags, and other * parser functions must be escaped (see below) unless it is desired (or * when it doesn't matter) that they be expanded before the condition is * evaluated. Wiki tables must always be escaped in order to be rendered. * * Character Escapes: * \l is translated to < * \g is translated to > * \o is translated to {{ * \c is translated to }} * \p is translated to | * * Example: * The wiki code below is the equivalent of following php code: * * $i = 0; * * echo $i; * * if ( $i < 5 ) * $i += 1; * else * $i -= 1; * * echo $i; * * {{ * #vardefine: i | 0 * }}{{ * #var: i * }}{{ * #ifexpr: {{ #var: i }} < 5 * | \o * #vardefine: i * \p \o #expr: \o #var: i \c + 1 \c * \c * | \o * #vardefine: i * \p \o #expr: \o #var: i \c - 1 \c * \c * }} * * {{ * #var: i * }} * * Escapes can be automated using the the <esc> tag: * * {{ * #vardefine: i | 0 * }}{{ * #var: i * }}{{ * #ifexpr: {{ #var: i }} < 5 * | <esc>{{ * #vardefine: i * | {{ #expr: {{ #var: i }} + 1 }} * }}</esc> * | <esc>{{ * #vardefine: i * | {{ #expr: {{ #var: i }} - 1 }} * }}</esc> * }} * * {{ * #var: i * }} * */ public static function ifexpr( &$parser, $expr = '', $then = '', $else = '' ) { try { $output = self::getExprParser()->doExpression( $expr ) ? $then : $else; require_once( dirname( __FILE__ ) . '/../CharacterEscapes/CharacterEscapes.php' ); $output = CharacterEscapes::charUnesc( $output, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } catch ( ExprError $e ) { return $e->getMessage(); } } /** * This function works like the #ifexpr: function except it returns the * else text only if the condition text evaluates to an empty string or * a string containing only white-space. */ public static function ifHook( &$parser, $test = '', $then = '', $else = '' ) { $output = $test !== '' ? $then : $else; require_once( dirname( __FILE__ ) . '/../CharacterEscapes/CharacterEscapes.php' ); $output = CharacterEscapes::charUnesc( $output, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } /** * This function returns the then text only if both comparison texts are * equal. If both texts can be interpreted as numbers, the comparison is * numerical * * Usage: * * {{ * #ifeq: <comparison text 1> * | <comparison text 2> * | <then text> * | <else text> * }} * * Templates (but not template parameters), magic words, tags, and other * parser functions must be escaped (see below) unless it is desired (or * when it doesn't matter) that they be expanded before the condition is * evaluated. Wiki tables must always be escaped in order to be rendered. * * Character Escapes: * \l is translated to < * \g is translated to > * \o is translated to {{ * \c is translated to }} * \p is translated to | * * Example: * The wiki code below is the equivalent of following php code: * * $i = 0; * * echo $i; * * if ( $i == 5 ) * $i += 1; * else * $i -= 1; * * echo $i; * * {{ * #vardefine: i | 0 * }}{{ * #var: i * }}{{ * #ifeq: {{ #var: i }} * | 5 * | \o * #vardefine: i * \p \o #expr: \o #var: i \c + 1 \c * \c * | \o * #vardefine: i * \p \o #expr: \o #var: i \c - 1 \c * \c * }} * * {{ * #var: i * }} * * Escapes can be automated using the the <esc> tag: * * {{ * #vardefine: i | 0 * }}{{ * #var: i * }}{{ * #ifeq: {{ #var: i }} * | 5 * | <esc>{{ * #vardefine: i * | {{ #expr: {{ #var: i }} + 1 }} * }}</esc> * | <esc>{{ * #vardefine: i * | {{ #expr: {{ #var: i }} - 1 }} * }}</esc> * }} * * {{ * #var: i * }} * */ public static function ifeq( &$parser, $left = '', $right = '', $then = '', $else = '' ) { $output = $left == $right ? $then : $else; require_once( dirname( __FILE__ ) . '/../CharacterEscapes/CharacterEscapes.php' ); $output = CharacterEscapes::charUnesc( $output, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } /** * This function works like the #ifexpr: function except it returns the * else text only if the condition text is not the name of an existing * wiki page. */ public static function ifexist( &$parser, $title = '', $then = '', $else = '' ) { $title = Title::newFromText( $title ); require_once( dirname( __FILE__ ) . '/../CharacterEscapes/CharacterEscapes.php' ); if ( $title ) { $id = $title->getArticleID(); $parser->mOutput->addLink( $title, $id ); if ( $id ) { $output = CharacterEscapes::charUnesc( $then, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } } $output = CharacterEscapes::charUnesc( $else, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } /** * This function returns the then text only if both comparison texts are * equal. If both texts can be interpreted as numbers, the comparison is * numerical * * Usage: * * {{ * #switch: <text> * | <case 1> [ = <text 1> ] * | <case 2> [ = <text 2> ] * | <case 3> [ = <text 3> ] * | ... * | <case N> [ = <text N> ] * | < default case | "#default = " default text > * }} * * Templates (but not template parameters), magic words, tags, and other * parser functions must be escaped (see below) unless it is desired (or * when it doesn't matter) that they be expanded before the condition is * evaluated. Wiki tables must always be escaped in order to be rendered. * * Character Escapes: * \l is translated to < * \g is translated to > * \o is translated to {{ * \c is translated to }} * \p is translated to | * * Example: * */ public static function switchHook( &$parser /*,...*/ ) { $args = func_get_args(); array_shift( $args ); $value = array_shift( $args ); $found = false; $parts = null; $default = null; require_once( dirname( __FILE__ ) . '/../CharacterEscapes/CharacterEscapes.php' ); foreach( $args as $arg ) { $parts = array_map( 'trim', explode( '=', $arg, 2 ) ); if ( count( $parts ) == 2 ) { if ( $found || $parts[0] == $value ) { $output = CharacterEscapes::charUnesc( $parts[1], array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } else { $mwDefault =& MagicWord::get( 'default' ); if ( $mwDefault->matchStartAndRemove( $parts[0] ) ) $default = $parts[1]; # else wrong case, continue } } # Multiple input, single output # If the value matches, set a flag and continue elseif ( $parts[0] == $value ) $found = true; } # Default case # Check if the last item had no = sign, thus specifying the default case if ( count( $parts ) == 1) return $parts[0]; if ( !is_null( $default ) ) { $output = CharacterEscapes::charUnesc( $default, array(), $parser ); $output = $parser->replaceVariables( $output, end( $parser->mArgStack ) ); return $output; } return ''; } }

