4 * PHP version of the callback template processor
5 * This is currently used as a test rig and is likely to be used for
6 * compatibility purposes later, where the C++ extension is not available.
9 define( 'CBT_WHITE', " \t\r\n" );
10 define( 'CBT_BRACE', '{}' );
11 define( 'CBT_DELIM', CBT_WHITE
. CBT_BRACE
);
12 define( 'CBT_DEBUG', 0 );
14 $GLOBALS['cbtExecutingGenerated'] = 0;
17 * Attempting to be a MediaWiki-independent module
19 if ( !function_exists( 'wfProfileIn' ) ) {
20 function wfProfileIn() {}
22 if ( !function_exists( 'wfProfileOut' ) ) {
23 function wfProfileOut() {}
27 * Escape text for inclusion in template
29 function cbt_escape( $text ) {
30 return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
36 function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
37 global $cbtExecutingGenerated;
38 if ( $cbtExecutingGenerated ) {
41 return new CBTValue( $text, $deps, $isTemplate );
46 * A dependency-tracking value class
47 * Callback functions should return one of these, unless they have
48 * no dependencies in which case they can return a string.
51 var $mText, $mDeps, $mIsTemplate;
55 * @param $text String: , default ''.
56 * @param $deps Array: what this value depends on
57 * @param $isTemplate Bool: whether the result needs compilation/execution, default 'false'.
59 function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
61 if ( !is_array( $deps ) ) {
62 $this->mDeps
= array( $deps ) ;
66 $this->mIsTemplate
= $isTemplate;
69 /** Concatenate two values, merging their dependencies */
70 function cat( $val ) {
71 if ( is_object( $val ) ) {
72 $this->addDeps( $val );
73 $this->mText
.= $val->mText
;
79 /** Add the dependencies of another value to this one */
80 function addDeps( $values ) {
81 if ( !is_array( $values ) ) {
82 $this->mDeps
= array_merge( $this->mDeps
, $values->mDeps
);
84 foreach ( $values as $val ) {
85 if ( !is_object( $val ) ) {
86 var_dump( debug_backtrace() );
89 $this->mDeps
= array_merge( $this->mDeps
, $val->mDeps
);
94 /** Remove a list of dependencies */
95 function removeDeps( $deps ) {
96 $this->mDeps
= array_diff( $this->mDeps
, $deps );
99 function setText( $text ) {
100 $this->mText
= $text;
111 /** If the value is a template, execute it */
112 function execute( &$processor ) {
113 if ( $this->mIsTemplate
) {
114 $myProcessor = new CBTProcessor( $this->mText
, $processor->mFunctionObj
, $processor->mIgnorableDeps
);
115 $myProcessor->mCompiling
= $processor->mCompiling
;
116 $val = $myProcessor->doText( 0, strlen( $this->mText
) );
117 if ( $myProcessor->getLastError() ) {
118 $processor->error( $myProcessor->getLastError() );
121 $this->mText
= $val->mText
;
122 $this->addDeps( $val );
124 if ( !$processor->mCompiling
) {
125 $this->mIsTemplate
= false;
130 /** If the value is plain text, escape it for inclusion in a template */
131 function templateEscape() {
132 if ( !$this->mIsTemplate
) {
133 $this->mText
= cbt_escape( $this->mText
);
137 /** Return true if the value has no dependencies */
138 function isStatic() {
139 return count( $this->mDeps
) == 0;
144 * Template processor, for compilation and execution
147 var $mText, # The text being processed
148 $mFunctionObj, # The object containing callback functions
149 $mCompiling = false, # True if compiling to a template, false if executing to text
150 $mIgnorableDeps = array(), # Dependency names which should be treated as static
151 $mFunctionCache = array(), # A cache of function results keyed by argument hash
152 $mLastError = false, # Last error message or false for no error
153 $mErrorPos = 0, # Last error position
155 /** Built-in functions */
160 'lbrace' => 'bi_lbrace',
162 'rbrace' => 'bi_rbrace',
163 'escape' => 'bi_escape',
168 * Create a template processor for a given text, callback object and static dependency list
170 function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
171 $this->mText
= $text;
172 $this->mFunctionObj
= $functionObj;
173 $this->mIgnorableDeps
= $ignorableDeps;
177 * Execute the template.
178 * If $compile is true, produces an optimised template where functions with static
179 * dependencies have been replaced by their return values.
181 function execute( $compile = false ) {
182 $fname = 'CBTProcessor::execute';
183 wfProfileIn( $fname );
184 $this->mCompiling
= $compile;
185 $this->mLastError
= false;
186 $val = $this->doText( 0, strlen( $this->mText
) );
187 $text = $val->getText();
188 if ( $this->mLastError
!== false ) {
189 $pos = $this->mErrorPos
;
191 // Find the line number at which the error occurred
197 $startLine = $endLine +
1;
199 $endLine = strpos( $this->mText
, "\n", $startLine );
201 } while ( $endLine !== false && $endLine < $pos );
203 $text = "Template error at line $line: $this->mLastError\n<pre>\n";
205 $context = rtrim( str_replace( "\t", " ", substr( $this->mText
, $startLine, $endLine - $startLine ) ) );
206 $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
208 wfProfileOut( $fname );
212 /** Shortcut for execute(true) */
214 $fname = 'CBTProcessor::compile';
215 wfProfileIn( $fname );
216 $s = $this->execute( true );
217 wfProfileOut( $fname );
221 /** Shortcut for doOpenText( $start, $end, false */
222 function doText( $start, $end ) {
223 return $this->doOpenText( $start, $end, false );
227 * Escape text for a template if we are producing a template. Do nothing
228 * if we are producing plain text.
230 function templateEscape( $text ) {
231 if ( $this->mCompiling
) {
232 return cbt_escape( $text );
239 * Recursive workhorse for text mode.
241 * Processes text mode starting from offset $p, until either $end is
242 * reached or a closing brace is found. If $needClosing is false, a
243 * closing brace will flag an error, if $needClosing is true, the lack
244 * of a closing brace will flag an error.
246 * The parameter $p is advanced to the position after the closing brace,
247 * or after the end. A CBTValue is returned.
251 function doOpenText( &$p, $end, $needClosing = true ) {
252 $fname = 'CBTProcessor::doOpenText';
253 wfProfileIn( $fname );
256 $ret = new CBTValue( '', array(), $this->mCompiling
);
258 $foundClosing = false;
259 while ( $p < $end ) {
260 $matchLength = strcspn( $in, CBT_BRACE
, $p, $end - $p );
261 $pToken = $p +
$matchLength;
263 if ( $pToken >= $end ) {
264 // No more braces, output remainder
265 $ret->cat( substr( $in, $p ) );
270 // Output the text before the brace
271 $ret->cat( substr( $in, $p, $matchLength ) );
273 // Advance the pointer
276 // Check for closing brace
277 if ( $in[$pToken] == '}' ) {
278 $foundClosing = true;
282 // Handle the "{fn}" special case
283 if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
284 wfProfileOut( $fname );
285 $val = $this->doOpenFunction( $p, $end );
286 wfProfileIn( $fname );
287 if ( $p < $end && $in[$p] == '"' ) {
288 $val->setText( htmlspecialchars( $val->getText() ) );
292 // Process the function mode component
293 wfProfileOut( $fname );
294 $ret->cat( $this->doOpenFunction( $p, $end ) );
295 wfProfileIn( $fname );
298 if ( $foundClosing && !$needClosing ) {
299 $this->error( 'Errant closing brace', $p );
300 } elseif ( !$foundClosing && $needClosing ) {
301 $this->error( 'Unclosed text section', $start );
303 wfProfileOut( $fname );
308 * Recursive workhorse for function mode.
310 * Processes function mode starting from offset $p, until either $end is
311 * reached or a closing brace is found. If $needClosing is false, a
312 * closing brace will flag an error, if $needClosing is true, the lack
313 * of a closing brace will flag an error.
315 * The parameter $p is advanced to the position after the closing brace,
316 * or after the end. A CBTValue is returned.
320 function doOpenFunction( &$p, $end, $needClosing = true ) {
324 $unexecutedTokens = array();
326 $foundClosing = false;
327 while ( $p < $end ) {
329 if ( $char == '{' ) {
330 // Switch to text mode
333 $token = $this->doOpenText( $p, $end );
335 $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
336 } elseif ( $char == '}' ) {
339 $foundClosing = true;
341 } elseif ( false !== strpos( CBT_WHITE
, $char ) ) {
343 // Consume the rest of the whitespace
344 $p +
= strspn( $in, CBT_WHITE
, $p, $end - $p );
346 // Token, find the end of it
347 $tokenLength = strcspn( $in, CBT_DELIM
, $p, $end - $p );
348 $token = new CBTValue( substr( $in, $p, $tokenLength ) );
349 // Execute the token as a function if it's not the function name
350 if ( count( $tokens ) ) {
351 $tokens[] = $this->doFunction( array( $token ), $p );
355 $unexecutedTokens[] = $token->getText();
360 if ( !$foundClosing && $needClosing ) {
361 $this->error( 'Unclosed function', $start );
365 $val = $this->doFunction( $tokens, $start );
366 if ( $this->mCompiling
&& !$val->isStatic() ) {
369 foreach( $tokens as $i => $token ) {
375 if ( $token->isStatic() ) {
377 $compiled .= '{' . $token->getText() . '}';
379 $compiled .= $token->getText();
382 $compiled .= $unexecutedTokens[$i];
386 // The dynamic parts of the string are still represented as functions, and
387 // function invocations have no dependencies. Thus the compiled result has
389 $val = new CBTValue( "{{$compiled}}", array(), true );
395 * Execute a function, caching and returning the result value.
396 * $tokens is an array of CBTValue objects. $tokens[0] is the function
397 * name, the others are arguments. $p is the string position, and is used
398 * for error messages only.
400 function doFunction( $tokens, $p ) {
401 if ( count( $tokens ) == 0 ) {
404 $fname = 'CBTProcessor::doFunction';
405 wfProfileIn( $fname );
409 // All functions implicitly depend on their arguments, and the function name
410 // While this is not strictly necessary for all functions, it's true almost
411 // all the time and so convenient to do automatically.
412 $ret->addDeps( $tokens );
414 $this->mCurrentPos
= $p;
415 $func = array_shift( $tokens );
416 $func = $func->getText();
418 // Extract the text component from all the tokens
419 // And convert any templates to plain text
421 foreach ( $tokens as $token ) {
422 $token->execute( $this );
423 $textArgs[] = $token->getText();
426 // Try the local cache
427 $cacheKey = $func . "\n" . implode( "\n", $textArgs );
428 if ( isset( $this->mFunctionCache
[$cacheKey] ) ) {
429 $val = $this->mFunctionCache
[$cacheKey];
430 } elseif ( isset( $this->mBuiltins
[$func] ) ) {
431 $func = $this->mBuiltins
[$func];
432 $val = call_user_func_array( array( &$this, $func ), $tokens );
433 $this->mFunctionCache
[$cacheKey] = $val;
434 } elseif ( method_exists( $this->mFunctionObj
, $func ) ) {
435 $profName = get_class( $this->mFunctionObj
) . '::' . $func;
436 wfProfileIn( "$fname-callback" );
437 wfProfileIn( $profName );
438 $val = call_user_func_array( array( &$this->mFunctionObj
, $func ), $textArgs );
439 wfProfileOut( $profName );
440 wfProfileOut( "$fname-callback" );
441 $this->mFunctionCache
[$cacheKey] = $val;
443 $this->error( "Call of undefined function \"$func\"", $p );
446 if ( !is_object( $val ) ) {
447 $val = new CBTValue((string)$val);
454 // If the output was a template, execute it
455 $val->execute( $this );
457 if ( $this->mCompiling
) {
458 // Escape any braces so that the output will be a valid template
459 $val->templateEscape();
461 $val->removeDeps( $this->mIgnorableDeps
);
462 $ret->addDeps( $val );
463 $ret->setText( $val->getText() );
466 wfDebug( "doFunction $func args = "
467 . var_export( $tokens, true )
468 . "unexpanded return = "
469 . var_export( $unexpanded, true )
470 . "expanded return = "
471 . var_export( $ret, true )
475 wfProfileOut( $fname );
480 * Set a flag indicating that an error has been found.
482 function error( $text, $pos = false ) {
483 $this->mLastError
= $text;
484 if ( $pos === false ) {
485 $this->mErrorPos
= $this->mCurrentPos
;
487 $this->mErrorPos
= $pos;
491 function getLastError() {
492 return $this->mLastError
;
495 /** 'if' built-in function */
496 function bi_if( $condition, $trueBlock, $falseBlock = null ) {
497 if ( is_null( $condition ) ) {
498 $this->error( "Missing condition in if" );
502 if ( $condition->getText() != '' ) {
503 return new CBTValue( $trueBlock->getText(),
504 array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
505 $trueBlock->mIsTemplate
);
507 if ( !is_null( $falseBlock ) ) {
508 return new CBTValue( $falseBlock->getText(),
509 array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
510 $falseBlock->mIsTemplate
);
512 return new CBTValue( '', $condition->getDeps() );
517 /** 'true' built-in function */
522 /** left brace built-in */
523 function bi_lbrace() {
527 /** right brace built-in */
528 function bi_rbrace() {
534 * Escape text for inclusion in an HTML attribute
536 function bi_escape( $val ) {
537 return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );