opcode = $opcode; $this->arg1 = $arg1; $this->arg2 = $arg2; } function name() { $opcodeNames = array( CBT_PUSH => 'PUSH', CBT_CAT => 'CAT', CBT_CATS => 'CATS', CBT_CALL => 'CALL', CBT_HX => 'HX', ); return $opcodeNames[$this->opcode]; } }; class CBTCompiler { var $mOps = array(); var $mCode; function CBTCompiler( $text ) { $this->mText = $text; } /** * Compile the text. * Returns true on success, error message on failure */ function compile() { $fname = 'CBTProcessor::compile'; $this->mLastError = false; $this->mOps = array(); $this->doText( 0, strlen( $this->mText ) ); if ( $this->mLastError !== false ) { $pos = $this->mErrorPos; // Find the line number at which the error occurred $startLine = 0; $endLine = 0; $line = 0; do { if ( $endLine ) { $startLine = $endLine + 1; } $endLine = strpos( $this->mText, "\n", $startLine ); ++$line; } while ( $endLine !== false && $endLine < $pos ); $text = "Template error at line $line: $this->mLastError\n
\n"; $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) ); $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n\n"; } else { $text = true; } return $text; } /** Shortcut for doOpenText( $start, $end, false */ function doText( $start, $end ) { return $this->doOpenText( $start, $end, false ); } function phpQuote( $text ) { return "'" . strtr( $text, array( "\\" => "\\\\", "'" => "\\'" ) ) . "'"; } function op( $opcode, $arg1 = null, $arg2 = null) { return new CBTOp( $opcode, $arg1, $arg2 ); } /** * Recursive workhorse for text mode. * * Processes text mode starting from offset $p, until either $end is * reached or a closing brace is found. If $needClosing is false, a * closing brace will flag an error, if $needClosing is true, the lack * of a closing brace will flag an error. * * The parameter $p is advanced to the position after the closing brace, * or after the end. A CBTValue is returned. * * @private */ function doOpenText( &$p, $end, $needClosing = true ) { $in =& $this->mText; $start = $p; $atStart = true; $foundClosing = false; while ( $p < $end ) { $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p ); $pToken = $p + $matchLength; if ( $pToken >= $end ) { // No more braces, output remainder if ( $atStart ) { $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p ) ); $atStart = false; } else { $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p ) ); } $p = $end; break; } // Output the text before the brace if ( $atStart ) { $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $matchLength ) ); $atStart = false; } else { $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) ); } // Advance the pointer $p = $pToken + 1; // Check for closing brace if ( $in[$pToken] == '}' ) { $foundClosing = true; break; } // Handle the "{fn}" special case if ( $pToken > 0 && $in[$pToken-1] == '"' ) { $this->doOpenFunction( $p, $end ); if ( $p < $end && $in[$p] == '"' ) { $this->mOps[] = $this->op( CBT_HX ); } } else { $this->doOpenFunction( $p, $end ); } if ( $atStart ) { $atStart = false; } else { $this->mOps[] = $this->op( CBT_CATS ); } } if ( $foundClosing && !$needClosing ) { $this->error( 'Errant closing brace', $p ); } elseif ( !$foundClosing && $needClosing ) { $this->error( 'Unclosed text section', $start ); } else { if ( $atStart ) { $this->mOps[] = $this->op( CBT_PUSH, '' ); } } } /** * Recursive workhorse for function mode. * * Processes function mode starting from offset $p, until either $end is * reached or a closing brace is found. If $needClosing is false, a * closing brace will flag an error, if $needClosing is true, the lack * of a closing brace will flag an error. * * The parameter $p is advanced to the position after the closing brace, * or after the end. A CBTValue is returned. * * @private */ function doOpenFunction( &$p, $end, $needClosing = true ) { $in =& $this->mText; $start = $p; $argCount = 0; $foundClosing = false; while ( $p < $end ) { $char = $in[$p]; if ( $char == '{' ) { // Switch to text mode ++$p; $tokenStart = $p; $this->doOpenText( $p, $end ); ++$argCount; } elseif ( $char == '}' ) { // Block end ++$p; $foundClosing = true; break; } elseif ( false !== strpos( CBT_WHITE, $char ) ) { // Whitespace // Consume the rest of the whitespace $p += strspn( $in, CBT_WHITE, $p, $end - $p ); } else { // Token, find the end of it $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p ); $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $tokenLength ) ); // Execute the token as a function if it's not the function name if ( $argCount ) { $this->mOps[] = $this->op( CBT_CALL, 1 ); } $p += $tokenLength; ++$argCount; } } if ( !$foundClosing && $needClosing ) { $this->error( 'Unclosed function', $start ); return ''; } $this->mOps[] = $this->op( CBT_CALL, $argCount ); } /** * Set a flag indicating that an error has been found. */ function error( $text, $pos = false ) { $this->mLastError = $text; if ( $pos === false ) { $this->mErrorPos = $this->mCurrentPos; } else { $this->mErrorPos = $pos; } } function getLastError() { return $this->mLastError; } function opsToString() { $s = ''; foreach( $this->mOps as $op ) { $s .= $op->name(); if ( !is_null( $op->arg1 ) ) { $s .= ' ' . var_export( $op->arg1, true ); } if ( !is_null( $op->arg2 ) ) { $s .= ' ' . var_export( $op->arg2, true ); } $s .= "\n"; } return $s; } function generatePHP( $functionObj ) { $fname = 'CBTCompiler::generatePHP'; wfProfileIn( $fname ); $stack = array(); foreach( $this->mOps as $index => $op ) { switch( $op->opcode ) { case CBT_PUSH: $stack[] = $this->phpQuote( $op->arg1 ); break; case CBT_CAT: $val = array_pop( $stack ); array_push( $stack, "$val . " . $this->phpQuote( $op->arg1 ) ); break; case CBT_CATS: $right = array_pop( $stack ); $left = array_pop( $stack ); array_push( $stack, "$left . $right" ); break; case CBT_CALL: $args = array_slice( $stack, count( $stack ) - $op->arg1, $op->arg1 ); $stack = array_slice( $stack, 0, count( $stack ) - $op->arg1 ); // Some special optimised expansions if ( $op->arg1 == 0 ) { $result = ''; } else { $func = array_shift( $args ); if ( substr( $func, 0, 1 ) == "'" && substr( $func, -1 ) == "'" ) { $func = substr( $func, 1, strlen( $func ) - 2 ); if ( $func == "if" ) { if ( $op->arg1 < 3 ) { // This should have been caught during processing return "Not enough arguments to if"; } elseif ( $op->arg1 == 3 ) { $result = "(({$args[0]} != '') ? ({$args[1]}) : '')"; } else { $result = "(({$args[0]} != '') ? ({$args[1]}) : ({$args[2]}))"; } } elseif ( $func == "true" ) { $result = "true"; } elseif( $func == "lbrace" || $func == "{" ) { $result = "{"; } elseif( $func == "rbrace" || $func == "}" ) { $result = "}"; } elseif ( $func == "escape" || $func == "~" ) { $result = "htmlspecialchars({$args[0]})"; } else { // Known function name $result = "{$functionObj}->{$func}(" . implode( ', ', $args ) . ')'; } } else { // Unknown function name $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )'; } } array_push( $stack, $result ); break; case CBT_HX: $val = array_pop( $stack ); array_push( $stack, "htmlspecialchars( $val )" ); break; default: return "Unknown opcode {$op->opcode}\n"; } } wfProfileOut( $fname ); if ( count( $stack ) !== 1 ) { return "Error, stack count incorrect\n"; } return ' global $cbtExecutingGenerated; ++$cbtExecutingGenerated; $output = ' . $stack[0] . '; --$cbtExecutingGenerated; return $output; '; } } ?>