summaryrefslogtreecommitdiff
path: root/includes/cbt/CBTCompiler.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/cbt/CBTCompiler.php')
-rw-r--r--includes/cbt/CBTCompiler.php369
1 files changed, 369 insertions, 0 deletions
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
new file mode 100644
index 00000000..4ef8ee4a
--- /dev/null
+++ b/includes/cbt/CBTCompiler.php
@@ -0,0 +1,369 @@
+<?php
+
+/**
+ * This file contains functions to convert callback templates to other languages.
+ * The template should first be pre-processed with CBTProcessor to remove static
+ * sections.
+ */
+
+
+require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
+
+/**
+ * Push a value onto the stack
+ * Argument 1: value
+ */
+define( 'CBT_PUSH', 1 );
+
+/**
+ * Pop, concatenate argument, push
+ * Argument 1: value
+ */
+define( 'CBT_CAT', 2 );
+
+/**
+ * Concatenate where the argument is on the stack, instead of immediate
+ */
+define( 'CBT_CATS', 3 );
+
+/**
+ * Call a function, push the return value onto the stack and put it in the cache
+ * Argument 1: argument count
+ *
+ * The arguments to the function are on the stack
+ */
+define( 'CBT_CALL', 4 );
+
+/**
+ * Pop, htmlspecialchars, push
+ */
+define( 'CBT_HX', 5 );
+
+class CBTOp {
+ var $opcode;
+ var $arg1;
+ var $arg2;
+
+ function CBTOp( $opcode, $arg1, $arg2 ) {
+ $this->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<pre>\n";
+
+ $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
+ $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\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;
+ ';
+ }
+}
+?>