00001 <?php
00002
00009 define( 'CBT_WHITE', " \t\r\n" );
00010 define( 'CBT_BRACE', '{}' );
00011 define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
00012 define( 'CBT_DEBUG', 0 );
00013
00014 $GLOBALS['cbtExecutingGenerated'] = 0;
00015
00019 if ( !function_exists( 'wfProfileIn' ) ) {
00020 function wfProfileIn() {}
00021 }
00022 if ( !function_exists( 'wfProfileOut' ) ) {
00023 function wfProfileOut() {}
00024 }
00025
00029 function cbt_escape( $text ) {
00030 return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
00031 }
00032
00036 function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
00037 global $cbtExecutingGenerated;
00038 if ( $cbtExecutingGenerated ) {
00039 return $text;
00040 } else {
00041 return new CBTValue( $text, $deps, $isTemplate );
00042 }
00043 }
00044
00050 class CBTValue {
00051 var $mText, $mDeps, $mIsTemplate;
00052
00059 function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
00060 $this->mText = $text;
00061 if ( !is_array( $deps ) ) {
00062 $this->mDeps = array( $deps ) ;
00063 } else {
00064 $this->mDeps = $deps;
00065 }
00066 $this->mIsTemplate = $isTemplate;
00067 }
00068
00070 function cat( $val ) {
00071 if ( is_object( $val ) ) {
00072 $this->addDeps( $val );
00073 $this->mText .= $val->mText;
00074 } else {
00075 $this->mText .= $val;
00076 }
00077 }
00078
00080 function addDeps( $values ) {
00081 if ( !is_array( $values ) ) {
00082 $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
00083 } else {
00084 foreach ( $values as $val ) {
00085 if ( !is_object( $val ) ) {
00086 var_dump( debug_backtrace() );
00087 exit;
00088 }
00089 $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
00090 }
00091 }
00092 }
00093
00095 function removeDeps( $deps ) {
00096 $this->mDeps = array_diff( $this->mDeps, $deps );
00097 }
00098
00099 function setText( $text ) {
00100 $this->mText = $text;
00101 }
00102
00103 function getText() {
00104 return $this->mText;
00105 }
00106
00107 function getDeps() {
00108 return $this->mDeps;
00109 }
00110
00112 function execute( &$processor ) {
00113 if ( $this->mIsTemplate ) {
00114 $myProcessor = new CBTProcessor( $this->mText, $processor->mFunctionObj, $processor->mIgnorableDeps );
00115 $myProcessor->mCompiling = $processor->mCompiling;
00116 $val = $myProcessor->doText( 0, strlen( $this->mText ) );
00117 if ( $myProcessor->getLastError() ) {
00118 $processor->error( $myProcessor->getLastError() );
00119 $this->mText = '';
00120 } else {
00121 $this->mText = $val->mText;
00122 $this->addDeps( $val );
00123 }
00124 if ( !$processor->mCompiling ) {
00125 $this->mIsTemplate = false;
00126 }
00127 }
00128 }
00129
00131 function templateEscape() {
00132 if ( !$this->mIsTemplate ) {
00133 $this->mText = cbt_escape( $this->mText );
00134 }
00135 }
00136
00138 function isStatic() {
00139 return count( $this->mDeps ) == 0;
00140 }
00141 }
00142
00146 class CBTProcessor {
00147 var $mText, # The text being processed
00148 $mFunctionObj, # The object containing callback functions
00149 $mCompiling = false, # True if compiling to a template, false if executing to text
00150 $mIgnorableDeps = array(), # Dependency names which should be treated as static
00151 $mFunctionCache = array(), # A cache of function results keyed by argument hash
00152 $mLastError = false, # Last error message or false for no error
00153 $mErrorPos = 0, # Last error position
00154
00156 $mBuiltins = array(
00157 'if' => 'bi_if',
00158 'true' => 'bi_true',
00159 '[' => 'bi_lbrace',
00160 'lbrace' => 'bi_lbrace',
00161 ']' => 'bi_rbrace',
00162 'rbrace' => 'bi_rbrace',
00163 'escape' => 'bi_escape',
00164 '~' => 'bi_escape',
00165 );
00166
00170 function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
00171 $this->mText = $text;
00172 $this->mFunctionObj = $functionObj;
00173 $this->mIgnorableDeps = $ignorableDeps;
00174 }
00175
00181 function execute( $compile = false ) {
00182 $fname = 'CBTProcessor::execute';
00183 wfProfileIn( $fname );
00184 $this->mCompiling = $compile;
00185 $this->mLastError = false;
00186 $val = $this->doText( 0, strlen( $this->mText ) );
00187 $text = $val->getText();
00188 if ( $this->mLastError !== false ) {
00189 $pos = $this->mErrorPos;
00190
00191
00192 $startLine = 0;
00193 $endLine = 0;
00194 $line = 0;
00195 do {
00196 if ( $endLine ) {
00197 $startLine = $endLine + 1;
00198 }
00199 $endLine = strpos( $this->mText, "\n", $startLine );
00200 ++$line;
00201 } while ( $endLine !== false && $endLine < $pos );
00202
00203 $text = "Template error at line $line: $this->mLastError\n<pre>\n";
00204
00205 $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
00206 $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
00207 }
00208 wfProfileOut( $fname );
00209 return $text;
00210 }
00211
00213 function compile() {
00214 $fname = 'CBTProcessor::compile';
00215 wfProfileIn( $fname );
00216 $s = $this->execute( true );
00217 wfProfileOut( $fname );
00218 return $s;
00219 }
00220
00222 function doText( $start, $end ) {
00223 return $this->doOpenText( $start, $end, false );
00224 }
00225
00230 function templateEscape( $text ) {
00231 if ( $this->mCompiling ) {
00232 return cbt_escape( $text );
00233 } else {
00234 return $text;
00235 }
00236 }
00237
00251 function doOpenText( &$p, $end, $needClosing = true ) {
00252 $fname = 'CBTProcessor::doOpenText';
00253 wfProfileIn( $fname );
00254 $in =& $this->mText;
00255 $start = $p;
00256 $ret = new CBTValue( '', array(), $this->mCompiling );
00257
00258 $foundClosing = false;
00259 while ( $p < $end ) {
00260 $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
00261 $pToken = $p + $matchLength;
00262
00263 if ( $pToken >= $end ) {
00264
00265 $ret->cat( substr( $in, $p ) );
00266 $p = $end;
00267 break;
00268 }
00269
00270
00271 $ret->cat( substr( $in, $p, $matchLength ) );
00272
00273
00274 $p = $pToken + 1;
00275
00276
00277 if ( $in[$pToken] == '}' ) {
00278 $foundClosing = true;
00279 break;
00280 }
00281
00282
00283 if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
00284 wfProfileOut( $fname );
00285 $val = $this->doOpenFunction( $p, $end );
00286 wfProfileIn( $fname );
00287 if ( $p < $end && $in[$p] == '"' ) {
00288 $val->setText( htmlspecialchars( $val->getText() ) );
00289 }
00290 $ret->cat( $val );
00291 } else {
00292
00293 wfProfileOut( $fname );
00294 $ret->cat( $this->doOpenFunction( $p, $end ) );
00295 wfProfileIn( $fname );
00296 }
00297 }
00298 if ( $foundClosing && !$needClosing ) {
00299 $this->error( 'Errant closing brace', $p );
00300 } elseif ( !$foundClosing && $needClosing ) {
00301 $this->error( 'Unclosed text section', $start );
00302 }
00303 wfProfileOut( $fname );
00304 return $ret;
00305 }
00306
00320 function doOpenFunction( &$p, $end, $needClosing = true ) {
00321 $in =& $this->mText;
00322 $start = $p;
00323 $tokens = array();
00324 $unexecutedTokens = array();
00325
00326 $foundClosing = false;
00327 while ( $p < $end ) {
00328 $char = $in[$p];
00329 if ( $char == '{' ) {
00330
00331 ++$p;
00332 $tokenStart = $p;
00333 $token = $this->doOpenText( $p, $end );
00334 $tokens[] = $token;
00335 $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
00336 } elseif ( $char == '}' ) {
00337
00338 ++$p;
00339 $foundClosing = true;
00340 break;
00341 } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
00342
00343
00344 $p += strspn( $in, CBT_WHITE, $p, $end - $p );
00345 } else {
00346
00347 $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
00348 $token = new CBTValue( substr( $in, $p, $tokenLength ) );
00349
00350 if ( count( $tokens ) ) {
00351 $tokens[] = $this->doFunction( array( $token ), $p );
00352 } else {
00353 $tokens[] = $token;
00354 }
00355 $unexecutedTokens[] = $token->getText();
00356
00357 $p += $tokenLength;
00358 }
00359 }
00360 if ( !$foundClosing && $needClosing ) {
00361 $this->error( 'Unclosed function', $start );
00362 return '';
00363 }
00364
00365 $val = $this->doFunction( $tokens, $start );
00366 if ( $this->mCompiling && !$val->isStatic() ) {
00367 $compiled = '';
00368 $first = true;
00369 foreach( $tokens as $i => $token ) {
00370 if ( $first ) {
00371 $first = false;
00372 } else {
00373 $compiled .= ' ';
00374 }
00375 if ( $token->isStatic() ) {
00376 if ( $i !== 0 ) {
00377 $compiled .= '{' . $token->getText() . '}';
00378 } else {
00379 $compiled .= $token->getText();
00380 }
00381 } else {
00382 $compiled .= $unexecutedTokens[$i];
00383 }
00384 }
00385
00386
00387
00388
00389 $val = new CBTValue( "{{$compiled}}", array(), true );
00390 }
00391 return $val;
00392 }
00393
00400 function doFunction( $tokens, $p ) {
00401 if ( count( $tokens ) == 0 ) {
00402 return new CBTValue;
00403 }
00404 $fname = 'CBTProcessor::doFunction';
00405 wfProfileIn( $fname );
00406
00407 $ret = new CBTValue;
00408
00409
00410
00411
00412 $ret->addDeps( $tokens );
00413
00414 $this->mCurrentPos = $p;
00415 $func = array_shift( $tokens );
00416 $func = $func->getText();
00417
00418
00419
00420 $textArgs = array();
00421 foreach ( $tokens as $token ) {
00422 $token->execute( $this );
00423 $textArgs[] = $token->getText();
00424 }
00425
00426
00427 $cacheKey = $func . "\n" . implode( "\n", $textArgs );
00428 if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
00429 $val = $this->mFunctionCache[$cacheKey];
00430 } elseif ( isset( $this->mBuiltins[$func] ) ) {
00431 $func = $this->mBuiltins[$func];
00432 $val = call_user_func_array( array( &$this, $func ), $tokens );
00433 $this->mFunctionCache[$cacheKey] = $val;
00434 } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
00435 $profName = get_class( $this->mFunctionObj ) . '::' . $func;
00436 wfProfileIn( "$fname-callback" );
00437 wfProfileIn( $profName );
00438 $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
00439 wfProfileOut( $profName );
00440 wfProfileOut( "$fname-callback" );
00441 $this->mFunctionCache[$cacheKey] = $val;
00442 } else {
00443 $this->error( "Call of undefined function \"$func\"", $p );
00444 $val = new CBTValue;
00445 }
00446 if ( !is_object( $val ) ) {
00447 $val = new CBTValue((string)$val);
00448 }
00449
00450 if ( CBT_DEBUG ) {
00451 $unexpanded = $val;
00452 }
00453
00454
00455 $val->execute( $this );
00456
00457 if ( $this->mCompiling ) {
00458
00459 $val->templateEscape();
00460 }
00461 $val->removeDeps( $this->mIgnorableDeps );
00462 $ret->addDeps( $val );
00463 $ret->setText( $val->getText() );
00464
00465 if ( CBT_DEBUG ) {
00466 wfDebug( "doFunction $func args = "
00467 . var_export( $tokens, true )
00468 . "unexpanded return = "
00469 . var_export( $unexpanded, true )
00470 . "expanded return = "
00471 . var_export( $ret, true )
00472 );
00473 }
00474
00475 wfProfileOut( $fname );
00476 return $ret;
00477 }
00478
00482 function error( $text, $pos = false ) {
00483 $this->mLastError = $text;
00484 if ( $pos === false ) {
00485 $this->mErrorPos = $this->mCurrentPos;
00486 } else {
00487 $this->mErrorPos = $pos;
00488 }
00489 }
00490
00491 function getLastError() {
00492 return $this->mLastError;
00493 }
00494
00496 function bi_if( $condition, $trueBlock, $falseBlock = null ) {
00497 if ( is_null( $condition ) ) {
00498 $this->error( "Missing condition in if" );
00499 return '';
00500 }
00501
00502 if ( $condition->getText() != '' ) {
00503 return new CBTValue( $trueBlock->getText(),
00504 array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
00505 $trueBlock->mIsTemplate );
00506 } else {
00507 if ( !is_null( $falseBlock ) ) {
00508 return new CBTValue( $falseBlock->getText(),
00509 array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
00510 $falseBlock->mIsTemplate );
00511 } else {
00512 return new CBTValue( '', $condition->getDeps() );
00513 }
00514 }
00515 }
00516
00518 function bi_true() {
00519 return "true";
00520 }
00521
00523 function bi_lbrace() {
00524 return '{';
00525 }
00526
00528 function bi_rbrace() {
00529 return '}';
00530 }
00531
00536 function bi_escape( $val ) {
00537 return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
00538 }
00539 }