From fc00763f0dc3ef1dbe1021e5e8759991d72c9e5e Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Sun, 3 Mar 2013 22:35:05 -0500 Subject: [PATCH] Add parser method to call parser functions There is currently no straightforward way for anything to call a parser function and get the result. This abstracts out that portion of braceSubstitution() to allow this. The immediate motivation for this patch is to close bug 41769 against Scribunto, see I0138836654b0e34c5c23daaedcdf5d4f9d1c7ab2. Bug: 41769 Change-Id: I339b882010dedd714e7965e25ad650ed8b8cd48f --- includes/parser/Parser.php | 190 ++++++++++++------ .../includes/parser/ParserMethodsTest.php | 17 ++ 2 files changed, 145 insertions(+), 62 deletions(-) diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 99042ae784..58f0a7a9fe 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -3237,70 +3237,22 @@ class Parser { $colonPos = strpos( $part1, ':' ); if ( $colonPos !== false ) { - # Case sensitive functions - $function = substr( $part1, 0, $colonPos ); - if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { - $function = $this->mFunctionSynonyms[1][$function]; - } else { - # Case insensitive functions - $function = $wgContLang->lc( $function ); - if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { - $function = $this->mFunctionSynonyms[0][$function]; - } else { - $function = false; - } + $func = substr( $part1, 0, $colonPos ); + $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) ); + for ( $i = 0; $i < $args->getLength(); $i++ ) { + $funcArgs[] = $args->item( $i ); } - if ( $function ) { - wfProfileIn( __METHOD__ . '-pfunc-' . $function ); - list( $callback, $flags ) = $this->mFunctionHooks[$function]; - $initialArgs = array( &$this ); - $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) ); - if ( $flags & SFH_OBJECT_ARGS ) { - # Add a frame parameter, and pass the arguments as an array - $allArgs = $initialArgs; - $allArgs[] = $frame; - for ( $i = 0; $i < $args->getLength(); $i++ ) { - $funcArgs[] = $args->item( $i ); - } - $allArgs[] = $funcArgs; - } else { - # Convert arguments to plain text - for ( $i = 0; $i < $args->getLength(); $i++ ) { - $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) ); - } - $allArgs = array_merge( $initialArgs, $funcArgs ); - } - - # Workaround for PHP bug 35229 and similar - if ( !is_callable( $callback ) ) { - wfProfileOut( __METHOD__ . '-pfunc-' . $function ); - wfProfileOut( __METHOD__ . '-pfunc' ); - wfProfileOut( __METHOD__ ); - throw new MWException( "Tag hook for $function is not callable\n" ); - } - $result = call_user_func_array( $callback, $allArgs ); - $found = true; - $noparse = true; - $preprocessFlags = 0; - - if ( is_array( $result ) ) { - if ( isset( $result[0] ) ) { - $text = $result[0]; - unset( $result[0] ); - } - - # Extract flags into the local scope - # This allows callers to set flags such as nowiki, found, etc. - extract( $result ); - } else { - $text = $result; - } - if ( !$noparse ) { - $text = $this->preprocessToDom( $text, $preprocessFlags ); - $isChildObj = true; - } - wfProfileOut( __METHOD__ . '-pfunc-' . $function ); + try { + $result = $this->callParserFunction( $frame, $func, $funcArgs ); + } catch ( Exception $ex ) { + wfProfileOut( __METHOD__ . '-pfunc' ); + throw $ex; } + + # The interface for parser functions allows for extracting + # flags into the local scope. Extract any forwarded flags + # here. + extract( $result ); } wfProfileOut( __METHOD__ . '-pfunc' ); } @@ -3497,6 +3449,120 @@ class Parser { return $ret; } + /** + * Call a parser function and return an array with text and flags. + * + * The returned array will always contain a boolean 'found', indicating + * whether the parser function was found or not. It may also contain the + * following: + * text: string|object, resulting wikitext or PP DOM object + * isHTML: bool, $text is HTML, armour it against wikitext transformation + * isChildObj: bool, $text is a DOM node needing expansion in a child frame + * isLocalObj: bool, $text is a DOM node needing expansion in the current frame + * nowiki: bool, wiki markup in $text should be escaped + * + * @since 1.21 + * @param $frame PPFrame The current frame, contains template arguments + * @param $function string Function name + * @param $args array Arguments to the function + * @return array + */ + public function callParserFunction( $frame, $function, array $args = array() ) { + global $wgContLang; + + wfProfileIn( __METHOD__ ); + + # Case sensitive functions + if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { + $function = $this->mFunctionSynonyms[1][$function]; + } else { + # Case insensitive functions + $function = $wgContLang->lc( $function ); + if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { + $function = $this->mFunctionSynonyms[0][$function]; + } else { + wfProfileOut( __METHOD__ ); + return array( 'found' => false ); + } + } + + wfProfileIn( __METHOD__ . '-pfunc-' . $function ); + list( $callback, $flags ) = $this->mFunctionHooks[$function]; + + # Workaround for PHP bug 35229 and similar + if ( !is_callable( $callback ) ) { + wfProfileOut( __METHOD__ . '-pfunc-' . $function ); + wfProfileOut( __METHOD__ ); + throw new MWException( "Tag hook for $function is not callable\n" ); + } + + $allArgs = array( &$this ); + if ( $flags & SFH_OBJECT_ARGS ) { + # Convert arguments to PPNodes and collect for appending to $allArgs + $funcArgs = array(); + foreach ( $args as $k => $v ) { + if ( $v instanceof PPNode || $k === 0 ) { + $funcArgs[] = $v; + } else { + $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 ); + } + } + + # Add a frame parameter, and pass the arguments as an array + $allArgs[] = $frame; + $allArgs[] = $funcArgs; + } else { + # Convert arguments to plain text and append to $allArgs + foreach ( $args as $k => $v ) { + if ( $v instanceof PPNode ) { + $allArgs[] = trim( $frame->expand( $v ) ); + } elseif ( is_int( $k ) && $k >= 0 ) { + $allArgs[] = trim( $v ); + } else { + $allArgs[] = trim( "$k=$v" ); + } + } + } + + $result = call_user_func_array( $callback, $allArgs ); + + # The interface for function hooks allows them to return a wikitext + # string or an array containing the string and any flags. This mungs + # things around to match what this method should return. + if ( !is_array( $result ) ) { + $result = array( + 'found' => true, + 'text' => $result, + ); + } else { + if ( isset( $result[0] ) && !isset( $result['text'] ) ) { + $result['text'] = $result[0]; + } + unset( $result[0] ); + $result += array( + 'found' => true, + ); + } + + $noparse = true; + $preprocessFlags = 0; + if ( isset( $result['noparse'] ) ) { + $noparse = $result['noparse']; + } + if ( isset( $result['preprocessFlags'] ) ) { + $preprocessFlags = $result['preprocessFlags']; + } + + if ( !$noparse ) { + $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags ); + $result['isChildObj'] = true; + } + wfProfileOut( __METHOD__ . '-pfunc-' . $function ); + wfProfileOut( __METHOD__ ); + + return $result; + } + /** * Get the semi-parsed DOM representation of a template with a given title, * and its redirect destination title. Cached. diff --git a/tests/phpunit/includes/parser/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php index 5c1a26866f..50fe0e4d9a 100644 --- a/tests/phpunit/includes/parser/ParserMethodsTest.php +++ b/tests/phpunit/includes/parser/ParserMethodsTest.php @@ -28,5 +28,22 @@ class ParserMethodsTest extends MediaWikiLangTestCase { $this->assertEquals( $expected, $text ); } + public function testCallParserFunction() { + global $wgParser; + + // Normal parses test passing PPNodes. Test passing an array. + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML ); + $frame = $wgParser->getPreprocessor()->newFrame(); + $ret = $wgParser->callParserFunction( $frame, '#tag', + array( 'pre', 'foo', 'style' => 'margin-left: 1.6em' ) + ); + $ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] ); + $this->assertSame( array( + 'found' => true, + 'text' => '
foo
', + ), $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' ); + } + // TODO: Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText() } -- 2.20.1