Extension:Control Structure Functions/ControlStructureFunctions.php

From MediaWiki.org

Jump to: navigation, search

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 '';
    }
}