* @author Daniel Werner */ abstract class ParserHook { const TYPE_TAG = 0; const TYPE_FUNCTION = 1; /** * @since 0.4.3 * * @var array */ protected static $registeredHooks = array(); /** * Returns an array of registered parser hooks (keys) and their handling * ParserHook deriving class names (values). * * @since 0.4.3 * * @return array */ public static function getRegisteredParserHooks() { return self::$registeredHooks; } /** * Returns the name of the ParserHook deriving class that defines a certain parser hook, * or false if there is none. * * @since 0.4.3 * * @param string $hookName * * @return mixed string or false */ public static function getHookClassName( $hookName ) { return array_key_exists( $hookName, self::$registeredHooks ) ? self::$registeredHooks[$hookName] : false; } /** * @since 0.4 * * @var Processor */ protected $validator; /** * @since 0.4 * * @var Parser */ protected $parser; /** * @since 0.4.4 * * @var PPFrame */ protected $frame; /** * @since 0.4.4 * * @var ParserHook::TYPE_ enum item */ protected $currentType; /** * @since 0.4 * * @var boolean */ public $forTagExtensions; /** * @since 0.4 * * @var boolean */ public $forParserFunctions; /** * Bitfifeld of Options influencing the characteristics of the registered * tag/parser function. * * @since 0.4.13 * * @var int */ protected $parserHookOptions; /** * Gets the name of the parser hook. * * @since 0.4 * * @return string or array of string */ protected abstract function getName(); /** * Renders and returns the output. * * @since 0.4 * * @param array $parameters * * @return string */ protected abstract function render( array $parameters ); /** * Flag for constructor, whether the function hook should be one callable without * leading hash, i.e. {{plural:...}} instead of {{#if:...}} * * @since 0.4.13 */ const FH_NO_HASH = 1; /* * * @ToDo: implementation of this functionality * * Flag for constructor, whether the tag hook should be handled as function tag hook * and not as a normal tag hook. See Parser::setFunctionTagHook() for details. */ #const TH_AS_FUNCTION_TAG = 2; /** * Constructor. * * @since 0.4 * * @param boolean $forTagExtensions * @param boolean $forParserFunctions * @param integer $flag combination of option flags to manipulare the parser hooks * characteristics. The following are available: * - ParserHook::FH_NO_HASH makes the function callable without leading hash. */ public function __construct( $forTagExtensions = true, $forParserFunctions = true, $flags = 0 ) { $this->forTagExtensions = $forTagExtensions; $this->forParserFunctions = $forParserFunctions; // store flags: $this->parserHookOptions = $flags; } /** * Function to hook up the coordinate rendering functions to the parser. * * @since 0.4 * * @param Parser $parser * * @return true */ public function init( Parser &$parser ) { $className = get_class( $this ); $first = true; foreach ( $this->getNames() as $name ) { if ( $first ) { self::$registeredHooks[$name] = $className; $first = false; } // Parser Tag hooking: if ( $this->forTagExtensions ) { $parser->setHook( $name, array( new ParserHookCaller( $className, 'renderTag' ), 'runTagHook' ) ); } // Parser Function hooking: if ( $this->forParserFunctions ) { $flags = 0; $function = 'renderFunction'; $callerFunction = 'runFunctionHook'; // use object arguments if available: if ( defined( 'SFH_OBJECT_ARGS' ) ) { $flags = $flags | SFH_OBJECT_ARGS; $function .= 'Obj'; $callerFunction .= 'Obj'; } // no leading Hash required? if ( $this->parserHookOptions & self::FH_NO_HASH ) { $flags = $flags | SFH_NO_HASH; } $parser->setFunctionHook( $name, array( new ParserHookCaller( $className, $function ), $callerFunction ), $flags ); } } return true; } /** * Returns an array with the names for the parser hook. * * @since 0.4 * * @return array */ protected function getNames() { $names = $this->getName(); if ( !is_array( $names ) ) { $names = array( $names ); } return $names; } /** * Function to add the magic word in pre MW 1.16. * * @since 0.4 * * @param array $magicWords * @param string $langCode * * @return boolean */ public function magic( array &$magicWords, $langCode ) { foreach ( $this->getNames() as $name ) { $magicWords[$name] = array( 0, $name ); } return true; } /** * Handler for rendering the tag hook registered by Parser::setHook() * * @since 0.4 * * @param mixed $input string or null * @param array $args * @param Parser $parser * @param PPFrame $frame Available from 1.16 * * @return string */ public function renderTag( $input, array $args, Parser $parser, PPFrame $frame = null ) { $this->parser = $parser; $this->frame = $frame; $defaultParameters = $this->getDefaultParameters( self::TYPE_TAG ); $defaultParam = array_shift( $defaultParameters ); // If there is a first default parameter, set the tag contents as its value. if ( !is_null( $defaultParam ) && !is_null( $input ) ) { $args[$defaultParam] = $input; } return $this->validateAndRender( $args, self::TYPE_TAG ); } /** * Handler for rendering the function hook registered by Parser::setFunctionHook() * * @since 0.4 * * @param Parser &$parser * ... further arguments ... * * @return array */ public function renderFunction( Parser &$parser /*, n args */ ) { $args = func_get_args(); $this->parser = array_shift( $args ); $output = $this->validateAndRender( $args, self::TYPE_FUNCTION ); $options = $this->getFunctionOptions(); if ( array_key_exists( 'isHTML', $options ) && $options['isHTML'] ) { /** @ToDo: FIXME: Is this really necessary? The same thing is propably going to * happen in Parser::braceSubstitution() if 'isHTML' is set! * @ToDo: other options besides 'isHTML' like 'noparse' are ignored here! */ return $this->parser->insertStripItem( $output, $this->parser->mStripState ); } return array_merge( array( $output ), $options ); } /** * Handler for rendering the function hook registered by Parser::setFunctionHook() together * with object style arguments (SFH_OBJECT_ARGS flag). * * @since 0.4.13 * * @param Parser &$parser * @param PPFrame $frame * @param type $args * @return array */ public function renderFunctionObj( Parser &$parser, PPFrame $frame, $args ) { $this->frame = $frame; // create non-object args for old style 'renderFunction()' $oldStyleArgs = array( &$parser ); foreach( $args as $arg ) { $oldStyleArgs[] = trim( $frame->expand( $arg ) ); } /* * since we can't validate un-expandet arguments, we just go on with old-style function * handling from here. Only advantage is that we have $this->frame set properly. */ return call_user_func_array( array( $this, 'renderFunction' ), $oldStyleArgs ); } /** * Returns the parser function otpions. * * @since 0.4 * * @return array */ protected function getFunctionOptions() { return array(); } /** * Takes care of validation and rendering, and returns the output. * * @since 0.4 * * @param array $arguments * @param integer $type Item of the ParserHook::TYPE_ enum * * @return string */ public function validateAndRender( array $arguments, $type ) { $names = $this->getNames(); $this->validator = Processor::newDefault(); $this->validator->getOptions()->setName( $names[0] ); if ( $type === self::TYPE_FUNCTION ) { $this->validator->setFunctionParams( $arguments, $this->getParameterInfo( $type ), $this->getDefaultParameters( $type ) ); } else { $this->validator->setParameters( $arguments, $this->getParameterInfo( $type ) ); } $this->validator->validateParameters(); $fatalError = $this->validator->hasFatalError(); if ( $fatalError === false ) { $output = $this->render( $this->validator->getParameterValues() ); } else { $output = $this->renderFatalError( $fatalError ); } return $output; } /** * Returns the ProcessingError objects for the errors and warnings that should be displayed. * * @since 0.4 * * @return array of array of ProcessingError */ protected function getErrorsToDisplay() { $errors = array(); $warnings = array(); foreach ( $this->validator->getErrors() as $error ) { // Check if the severity of the error is high enough to display it. if ( $error->shouldShow() ) { $errors[] = $error; } elseif ( $error->shouldWarn() ) { $warnings[] = $error; } } return array( 'errors' => $errors, 'warnings' => $warnings ); } /** * Creates and returns the output when a fatal error prevent regular rendering. * * @since 0.4 * * @param ProcessingError $error * * @return string */ protected function renderFatalError( ProcessingError $error ) { return '
' . wfMessage( 'validator-fatal-error', $error->getMessage() )->parse() . '


'; } // TODO: replace render errors functionality /** * Returns an array containing the parameter info. * Override in deriving classes to add parameter info. * * @since 0.4 * * @param integer $type Item of the ParserHook::TYPE_ enum * * @return array */ protected function getParameterInfo( $type ) { return array(); } public function getParamDefinitions( $type = self::TYPE_FUNCTION ) { return $this->getParameterInfo( $type ); } /** * Returns the list of default parameters. These parameters can be used as * unnamed parameters where it is not necessary to use the name and the '=' as * long as there is no '=' within the value. * It is possible to define that a parameter should not have a named fallback. * Therefore the information has to be returnd as sub-array with the parameter * name as first and Validator::PARAM_UNNAMED as second value. Parameter using * this option must be set first, before any unnamed parameter in the same order * as set here. All parameters defined before the last parameter making use of * Validator::PARAM_UNNAMED will automatically be populated with this option. * * Override in deriving classes to add default parameters. * * @since 0.4 * * @param integer $type Item of the ParserHook::TYPE_ enum * * @return array */ protected function getDefaultParameters( $type ) { return array(); } /** * Returns the data needed to describe the parser hook. * This is mainly needed because some of the individual get methods * that return the needed data are protected, and cannot be made * public without breaking b/c in a rather bad way. * * @since 0.4.3 * * @param integer $type Item of the ParserHook::TYPE_ enum * * @return array */ public function getDescriptionData( $type ) { return array( 'names' => $this->getNames(), 'description' => $this->getDescription(), 'message' => $this->getMessage(), 'parameters' => ParamDefinition::getCleanDefinitions( $this->getParameterInfo( $type ) ), 'defaults' => $this->getDefaultParameters( $type ), ); } /** * Returns a description for the parser hook, or false when there is none. * Override in deriving classes to add a message. * * @since 0.4.3 * @deprecated since 1.0 * * @return mixed string or false */ public function getDescription() { $msg = $this->getMessage(); return $msg === false ? false : wfMessage( $msg )->plain(); } /** * Returns a description message for the parser hook, or false when there is none. * Override in deriving classes to add a message. * * @since 0.4.10 * * @return mixed string or false */ public function getMessage() { return false; } /** * Returns if the current render request is coming from a tag extension. * * @since 0.4.4 * * @return boolean */ protected function isTag() { return $this->currentType == self::TYPE_TAG; } /** * Returns if the current render request is coming from a parser function. * * @since 0.4.4 * * @return boolean */ protected function isFunction() { return $this->currentType == self::TYPE_FUNCTION; } /** * Utility function to parse wikitext without having to care * about handling a tag extension or parser function. * * @since 0.4.4 * * @param string $text The wikitext to be parsed * * @return string the parsed output */ protected function parseWikitext( $text ) { // Parse the wikitext to HTML. if ( $this->isFunction() ) { return $this->parser->parse( $text, $this->parser->mTitle, $this->parser->mOptions, true, false )->getText(); } else { return $this->parser->recursiveTagParse( $text, $this->frame ); } } } /** * Completely evil class to create a new instance of the handling ParserHook when the actual hook gets called. * * @evillness >9000 - to be replaced when a better solution (LSB?) is possible. * * @since 0.4 * * @author Jeroen De Dauw * @author Daniel Werner */ class ParserHookCaller { protected $class; protected $method; function __construct( $class, $method ) { $this->class = $class; $this->method = $method; } /* * See Parser::braceSubstitution() and Parser::extensionSubstitution() for details about * how the Parser object and other parameters are being passed. Therefore for function * hooks &$parser fullfills the same purpos as $parser for the tag hook. * functionTagHook (!) calls (if implemented at a later time) are more like function hooks, * meaning, they would require &$parser as well. */ public function runTagHook( $input, array $args, Parser $parser, PPFrame $frame = null ) { $obj = new $this->class(); return $obj->{$this->method}( $input, $args, $parser, $frame ); } public function runFunctionHook( Parser &$parser /*, n args */ ) { $args = func_get_args(); $args[0] = &$parser; // with '&' becaus call_user_func_array is being used return call_user_func_array( array( new $this->class(), $this->method ), $args ); } public function runFunctionHookObj( Parser &$parser, PPFrame $frame, array $args ) { $obj = new $this->class(); return $obj->{$this->method}( $parser, $frame, $args ); } }