Implemented PHP code generation for CBT templates. With this method, the new skin...
authorTim Starling <tstarling@users.mediawiki.org>
Sun, 19 Mar 2006 08:11:19 +0000 (08:11 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Sun, 19 Mar 2006 08:11:19 +0000 (08:11 +0000)
includes/CBTProcessor.php [deleted file]
includes/cbt/CBTCompiler.php [new file with mode: 0644]
includes/cbt/CBTProcessor.php [new file with mode: 0644]
includes/cbt/README [new file with mode: 0644]
skins/disabled/MonoBookCBT.php

diff --git a/includes/CBTProcessor.php b/includes/CBTProcessor.php
deleted file mode 100644 (file)
index 0840bb2..0000000
+++ /dev/null
@@ -1,568 +0,0 @@
-<?php
-
-/**
- * PHP version of the callback template processor
- * This is currently used as a test rig and is likely to be used for 
- * compatibility purposes later, where the C++ extension is not available.
- *
- * Template syntax
- * ---------------
- *
- * There are two modes: text mode and function mode. The brace characters "{" 
- * and "}" are the only reserved characters. Either one of them will switch from
- * text mode to function mode wherever they appear, no exceptions. 
- *
- * In text mode, all characters are passed through to the output. In function
- * mode, text is split into tokens, delimited either by whitespace or by 
- * matching pairs of braces. The first token is taken to be a function name. The
- * other tokens are first processed in function mode themselves, then they are 
- * passed to the named function as parameters. The return value of the function
- * is passed through to the output.
- *
- * Example:
- *    {escape {"hello"}}
- *
- * First brace switches to function mode. The function name is escape, the first
- * and only parameter is {"hello"}. This parameter is executed. The braces around
- * the parameter cause the parser to switch to text mode, thus the string "hello",
- * including the quotes, is passed back and used as an argument to the escape 
- * function. 
- *
- * Example:
- *    {if title {<h1>{title}</h1>}}
- *
- * The function name is "if". The first parameter is the result of calling the 
- * function "title". The second parameter is a level 1 HTML heading containing
- * the result of the function "title". "if" is a built-in function which will 
- * return the second parameter only if the first is non-blank, so the effect of
- * this is to return a heading element only if a title exists.
- *
- * As a shortcut for generation of HTML attributes, if a function mode segment is
- * surrounded by double quotes, quote characters in the return value will be 
- * escaped. This only applies if the quote character immediately precedes the 
- * opening brace, and immediately follows the closing brace, with no whitespace.
- *
- * User callback functions are defined by passing a function object to the 
- * template processor. Function names appearing in the text are first checked
- * against built-in function names, then against the method names in the function
- * object. The function object forms a sandbox for execution of the template, so 
- * security-conscious users may wish to avoid including functions that allow
- * arbitrary filesystem access or code execution.
- *
- * The callback function will receive its parameters as strings. If the 
- * result of the function depends only on the arguments, and certain things 
- * understood to be "static", such as the source code, then the callback function
- * should return a string. If the result depends on other things, then the function
- * should return an object:
- *
- *    return new TplValue( $text, $deps );
- *
- * where $deps is an array of string tokens, each one naming a dependency. As a 
- * shortcut, if there is only one dependency, $deps may be a string.
- * 
- */
-
-
-define( 'TP_WHITE', " \t\r\n" );
-define( 'TP_BRACE', '{}' );
-define( 'TP_DELIM', TP_WHITE . TP_BRACE );
-define( 'TP_DEBUG', 0 );
-
-/**
- * Attempting to be a MediaWiki-independent module
- */
-if ( !function_exists( 'wfProfileIn' ) ) {
-       function wfProfileIn() {}
-}
-if ( !function_exists( 'wfProfileOut' ) ) {
-       function wfProfileOut() {}
-}
-
-/**
- * Escape text for inclusion in template
- */
-function templateEscape( $text ) {
-       return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
-}
-
-/**
- * A dependency-tracking value class
- * Callback functions should return one of these, unless they have 
- * no dependencies in which case they can return a string.
- */ 
-class TplValue {
-       var $mText, $mDeps, $mIsTemplate;
-
-       /**
-        * Create a new value
-        * @param string $text
-        * @param array $deps What this value depends on
-        * @param bool $isTemplate whether the result needs compilation/execution
-        */
-       function TplValue( $text = '', $deps = array(), $isTemplate = false ) {
-               $this->mText = $text;
-               if ( !is_array( $deps ) ) {
-                       $this->mDeps = array( $deps ) ;
-               } else {
-                       $this->mDeps = $deps;
-               }
-               $this->mIsTemplate = $isTemplate;
-       }
-
-       /** Concatenate two values, merging their dependencies */
-       function cat( $val ) {
-               if ( is_object( $val ) ) {
-                       $this->addDeps( $val );
-                       $this->mText .= $val->mText;
-               } else {
-                       $this->mText .= $val;
-               }
-       }
-
-       /** Add the dependencies of another value to this one */
-       function addDeps( $values ) {
-               if ( !is_array( $values ) ) {
-                       $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
-               } else {
-                       foreach ( $values as $val ) {
-                               if ( !is_object( $val ) ) {
-                                       var_dump( debug_backtrace() );
-                                       exit;
-                               }
-                               $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
-                       }
-               }
-       }
-
-       /** Remove a list of dependencies */
-       function removeDeps( $deps ) {
-               $this->mDeps = array_diff( $this->mDeps, $deps );
-       }
-
-       function setText( $text ) {
-               $this->mText = $text;
-       }
-
-       function getText() {
-               return $this->mText;
-       }
-
-       function getDeps() {
-               return $this->mDeps;
-       }
-
-       /** If the value is a template, execute it */
-       function execute( &$processor ) {
-               if ( $this->mIsTemplate ) {
-                       $myProcessor = new CBTProcessor( $this->mText,  $processor->mFunctionObj, $processor->mIgnorableDeps );
-                       $myProcessor->mCompiling = $processor->mCompiling;
-                       $val = $myProcessor->doText( 0, strlen( $this->mText ) );
-                       $this->mText = $val->mText;
-                       $this->addDeps( $val );
-                       if ( !$processor->mCompiling ) {
-                               $this->mIsTemplate = false;
-                       }
-               }
-       }
-
-       /** If the value is plain text, escape it for inclusion in a template */
-       function templateEscape() {
-               if ( !$this->mIsTemplate ) {
-                       $this->mText = templateEscape( $this->mText );
-               }
-       }
-
-       /** Return true if the value has no dependencies */
-       function isStatic() {
-               return count( $this->mDeps ) == 0;
-       }
-}
-
-/**
- * Template processor, for compilation and execution
- */
-class CBTProcessor {
-       var $mText,                     # The text being processed. This is immutable.
-               $mFunctionObj,              # The object containing callback functions
-               $mCompiling = false,        # True if compiling to a template, false if executing to text
-               $mIgnorableDeps = array(),  # Dependency names which should be treated as static
-               $mFunctionCache = array(),  # A cache of function results keyed by argument hash
-
-               /** Built-in functions */
-               $mBuiltins = array(
-               'if'       => 'bi_if',
-               'true'     => 'bi_true',
-               '['        => 'bi_lbrace',
-               'lbrace'   => 'bi_lbrace',
-               ']'        => 'bi_rbrace',
-               'rbrace'   => 'bi_rbrace',
-               'escape'   => 'bi_escape',
-               '~'        => 'bi_escape',
-       );
-
-       /**
-        * Create a template processor for a given text, callback object and static dependency list
-        */
-       function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
-               $this->mText = $text;
-               $this->mFunctionObj = $functionObj;
-               $this->mIgnorableDeps = $ignorableDeps;
-       }
-
-       /**
-        * Execute the template.
-        * If $compile is true, produces an optimised template where functions with static 
-        * dependencies have been replaced by their return values.
-        */
-       function execute( $compile = false ) {
-               $fname = 'CBTProcessor::execute';
-               wfProfileIn( $fname );
-               $this->mCompiling = $compile;
-               $this->mLastError = false;
-               $val = $this->doText( 0, strlen( $this->mText ) );
-               $text = $val->getText();
-               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";
-               } 
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /** Shortcut for execute(true) */
-       function compile() {
-               $fname = 'CBTProcessor::compile';
-               wfProfileIn( $fname );
-               $s = $this->execute( true );
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /** Shortcut for doOpenText( $start, $end, false */
-       function doText( $start, $end ) {
-               return $this->doOpenText( $start, $end, false );
-       }
-
-       /** 
-        * Escape text for a template if we are producing a template. Do nothing
-        * if we are producing plain text.
-        */
-        function templateEscape( $text ) {
-               if ( $this->mCompiling ) {
-                       return templateEscape( $text );
-               } else {
-                       return $text;
-               }
-       }
-
-       /**
-        * 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 TplValue is returned.
-        *
-        * @private
-        */
-       function doOpenText( &$p, $end, $needClosing = true ) {
-               $in =& $this->mText;
-               $start = $p;
-               $ret = new TplValue( '', array(), $this->mCompiling );
-               
-               $foundClosing = false;
-               while ( $p < $end ) {
-                       $matchLength = strcspn( $in, TP_BRACE, $p, $end - $p );
-                       $pToken = $p + $matchLength;
-
-                       if ( $pToken >= $end ) {
-                               // No more braces, output remainder
-                               $ret->cat( substr( $in, $p ) );
-                               $p = $end;
-                               break;
-                       }
-
-                       // Output the text before the brace
-                       $ret->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] == '"' ) {
-                               $val = $this->doOpenFunction( $p, $end );
-                               if ( $p < $end && $in[$p] == '"' ) {
-                                       $val->setText( htmlspecialchars( $val->getText() ) );
-                               }
-                               $ret->cat( $val );
-                       } else {
-                               // Process the function mode component
-                               $ret->cat( $this->doOpenFunction( $p, $end ) );
-                       }
-               }
-               if ( $foundClosing && !$needClosing ) {
-                       $this->error( 'Errant closing brace', $p );
-               } elseif ( !$foundClosing && $needClosing ) {
-                       $this->error( 'Unclosed text section', $start );
-               }
-               return $ret;
-       }
-
-       /**
-        * 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 TplValue is returned.
-        *
-        * @private
-        */
-       function doOpenFunction( &$p, $end, $needClosing = true ) {
-               $in =& $this->mText;
-               $start = $p;
-               $tokens = array();
-               $unexecutedTokens = array();
-
-               $foundClosing = false;
-               while ( $p < $end ) {
-                       $char = $in[$p];
-                       if ( $char == '{' ) {
-                               // Switch to text mode
-                               ++$p;
-                               $tokenStart = $p;
-                               $token = $this->doOpenText( $p, $end );
-                               $tokens[] = $token;
-                               $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
-                       } elseif ( $char == '}' ) {
-                               // Block end
-                               ++$p;
-                               $foundClosing = true;
-                               break;
-                       } elseif ( false !== strpos( TP_WHITE, $char ) ) {
-                               // Whitespace
-                               // Consume the rest of the whitespace
-                               $p += strspn( $in, TP_WHITE, $p, $end - $p );
-                       } else {
-                               // Token, find the end of it
-                               $tokenLength = strcspn( $in, TP_DELIM, $p, $end - $p );
-                               $token = new TplValue( substr( $in, $p, $tokenLength ) );
-                               // Execute the token as a function if it's not the function name
-                               if ( count( $tokens ) ) {
-                                       $tokens[] = $this->doFunction( array( $token ), $p );
-                               } else {
-                                       $tokens[] = $token;
-                               }
-                               $unexecutedTokens[] = $token->getText();
-
-                               $p += $tokenLength;
-                       }
-               }
-               if ( !$foundClosing && $needClosing ) {
-                       $this->error( 'Unclosed function', $start );
-                       return '';
-               }
-
-               $val = $this->doFunction( $tokens, $start );
-               if ( $this->mCompiling && !$val->isStatic() ) {
-                       $compiled = '';
-                       $first = true;
-                       foreach( $tokens as $i => $token ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $compiled .= ' ';
-                               }
-                               if ( $token->isStatic() ) {
-                                       if ( $i !== 0 ) {
-                                               $compiled .= '{' . $token->getText() . '}';
-                                       } else {
-                                               $compiled .= $token->getText();
-                                       }
-                               } else {
-                                       $compiled .= $unexecutedTokens[$i];
-                               }
-                       }
-
-                       // The dynamic parts of the string are still represented as functions, and 
-                       // function invocations have no dependencies. Thus the compiled result has 
-                       // no dependencies.
-                       $val = new TplValue( "{{$compiled}}", array(), true );
-               }
-               return $val;
-       }
-
-       /**
-        * Execute a function, caching and returning the result value.
-        * $tokens is an array of TplValue objects. $tokens[0] is the function 
-        * name, the others are arguments. $p is the string position, and is used
-        * for error messages only.
-        */
-       function doFunction( $tokens, $p ) {
-               if ( count( $tokens ) == 0 ) {
-                       return new TplValue;
-               }
-               $fname = 'CBTProcessor::doFunction';
-               wfProfileIn( $fname );
-               
-               $ret = new TplValue;
-               
-               // All functions implicitly depend on their arguments, and the function name
-               // While this is not strictly necessary for all functions, it's true almost 
-               // all the time and so convenient to do automatically. 
-               $ret->addDeps( $tokens );
-
-               $this->mCurrentPos = $p;
-               $func = array_shift( $tokens );
-               $func = $func->getText();
-
-               // Extract the text component from all the tokens
-               // And convert any templates to plain text
-               $textArgs = array();
-               foreach ( $tokens as $token ) {
-                       $token->execute( $this );
-                       $textArgs[] = $token->getText();
-               }
-
-               // Try the local cache
-               $cacheKey = $func . "\n" . implode( "\n", $textArgs );
-               if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
-                       $val = $this->mFunctionCache[$cacheKey];
-               } elseif ( isset( $this->mBuiltins[$func] ) ) {
-                       $func = $this->mBuiltins[$func];
-                       $val = call_user_func_array( array( &$this, $func ), $tokens );
-                       $this->mFunctionCache[$cacheKey] = $val;
-               } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
-                       $profName = get_class( $this->mFunctionObj ) . '::' . $func;
-                       wfProfileIn( "$fname-callback" );
-                       wfProfileIn( $profName );
-                       $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
-                       wfProfileOut( $profName );
-                       wfProfileOut( "$fname-callback" );
-                       $this->mFunctionCache[$cacheKey] = $val;
-               } else {
-                       $this->error( "Call of undefined function \"$func\"", $p );
-                       $val = new TplValue;
-               }
-               if ( !is_object( $val ) ) {
-                       $val = new TplValue((string)$val);
-               }
-
-               if ( TP_DEBUG ) {
-                       $unexpanded = $val;
-               }
-
-               // If the output was a template, execute it
-               $val->execute( $this );
-               
-               if ( $this->mCompiling ) {
-                       // Escape any braces so that the output will be a valid template
-                       $val->templateEscape();
-               } 
-               $val->removeDeps( $this->mIgnorableDeps );
-               $ret->addDeps( $val );
-               $ret->setText( $val->getText() );
-
-               if ( TP_DEBUG ) {
-                       print "doFunction $func args = ";
-                       var_dump( $tokens );
-                       print "unexpanded return = ";
-                       var_dump( $unexpanded );
-                       print "expanded return = ";
-                       var_dump( $ret );
-               }
-               
-               wfProfileOut( $fname );
-               return $ret;
-       }
-
-       /**
-        * 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;
-       }
-
-       /** 'if' built-in function */
-       function bi_if( $condition, $trueBlock, $falseBlock = null ) {
-               if ( is_null( $condition ) ) {
-                       $this->error( "Missing condition in if" );
-                       return '';
-               }
-
-               if ( $condition->getText() != '' ) {
-                       return new TplValue( $trueBlock->getText(), 
-                               array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
-                               $trueBlock->mIsTemplate );
-               } else {
-                       if ( !is_null( $falseBlock ) ) {
-                               return new TplValue( $falseBlock->getText(), 
-                                       array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
-                                       $falseBlock->mIsTemplate );
-                       } else {
-                               return new TplValue( '', $condition->getDeps() );
-                       }
-               }
-       }
-
-       /** 'true' built-in function */
-       function bi_true() {
-               return "true";
-       }
-
-       /** left brace built-in */
-       function bi_lbrace() {
-               return '{';
-       }
-
-       /** right brace built-in */
-       function bi_rbrace() {
-               return '}';
-       }
-
-       /** 
-        * escape built-in.
-        * Escape text for inclusion in an HTML attribute 
-        */
-       function bi_escape( $val ) {
-               return new TplValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
-       }
-}
-?>
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
new file mode 100644 (file)
index 0000000..4b5bac0
--- /dev/null
@@ -0,0 +1,356 @@
+<?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;
+       }
+
+       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 ) {
+               $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 ) . ')->mText';
+                                                       }
+                                               } else {
+                                                       // Unknown function name
+                                                       $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )->mText';
+                                               }
+                                       }
+                                       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";
+                       }
+               }
+               if ( count( $stack ) !== 1 ) {
+                       return "Error, stack count incorrect\n";
+               }
+               return $stack[0];
+       }
+}
+?>
diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php
new file mode 100644 (file)
index 0000000..f6096e1
--- /dev/null
@@ -0,0 +1,519 @@
+<?php
+
+/**
+ * PHP version of the callback template processor
+ * This is currently used as a test rig and is likely to be used for 
+ * compatibility purposes later, where the C++ extension is not available.
+ */
+
+define( 'CBT_WHITE', " \t\r\n" );
+define( 'CBT_BRACE', '{}' );
+define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
+define( 'CBT_DEBUG', 0 );
+
+/**
+ * Attempting to be a MediaWiki-independent module
+ */
+if ( !function_exists( 'wfProfileIn' ) ) {
+       function wfProfileIn() {}
+}
+if ( !function_exists( 'wfProfileOut' ) ) {
+       function wfProfileOut() {}
+}
+
+/**
+ * Escape text for inclusion in template
+ */
+function templateEscape( $text ) {
+       return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
+}
+
+/**
+ * A dependency-tracking value class
+ * Callback functions should return one of these, unless they have 
+ * no dependencies in which case they can return a string.
+ */ 
+class CBTValue {
+       var $mText, $mDeps, $mIsTemplate;
+
+       /**
+        * Create a new value
+        * @param string $text
+        * @param array $deps What this value depends on
+        * @param bool $isTemplate whether the result needs compilation/execution
+        */
+       function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
+               $this->mText = $text;
+               if ( !is_array( $deps ) ) {
+                       $this->mDeps = array( $deps ) ;
+               } else {
+                       $this->mDeps = $deps;
+               }
+               $this->mIsTemplate = $isTemplate;
+       }
+
+       /** Concatenate two values, merging their dependencies */
+       function cat( $val ) {
+               if ( is_object( $val ) ) {
+                       $this->addDeps( $val );
+                       $this->mText .= $val->mText;
+               } else {
+                       $this->mText .= $val;
+               }
+       }
+
+       /** Add the dependencies of another value to this one */
+       function addDeps( $values ) {
+               if ( !is_array( $values ) ) {
+                       $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
+               } else {
+                       foreach ( $values as $val ) {
+                               if ( !is_object( $val ) ) {
+                                       var_dump( debug_backtrace() );
+                                       exit;
+                               }
+                               $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
+                       }
+               }
+       }
+
+       /** Remove a list of dependencies */
+       function removeDeps( $deps ) {
+               $this->mDeps = array_diff( $this->mDeps, $deps );
+       }
+
+       function setText( $text ) {
+               $this->mText = $text;
+       }
+
+       function getText() {
+               return $this->mText;
+       }
+
+       function getDeps() {
+               return $this->mDeps;
+       }
+
+       /** If the value is a template, execute it */
+       function execute( &$processor ) {
+               if ( $this->mIsTemplate ) {
+                       $myProcessor = new CBTProcessor( $this->mText,  $processor->mFunctionObj, $processor->mIgnorableDeps );
+                       $myProcessor->mCompiling = $processor->mCompiling;
+                       $val = $myProcessor->doText( 0, strlen( $this->mText ) );
+                       $this->mText = $val->mText;
+                       $this->addDeps( $val );
+                       if ( !$processor->mCompiling ) {
+                               $this->mIsTemplate = false;
+                       }
+               }
+       }
+
+       /** If the value is plain text, escape it for inclusion in a template */
+       function templateEscape() {
+               if ( !$this->mIsTemplate ) {
+                       $this->mText = templateEscape( $this->mText );
+               }
+       }
+
+       /** Return true if the value has no dependencies */
+       function isStatic() {
+               return count( $this->mDeps ) == 0;
+       }
+}
+
+/**
+ * Template processor, for compilation and execution
+ */
+class CBTProcessor {
+       var $mText,                     # The text being processed. This is immutable.
+               $mFunctionObj,              # The object containing callback functions
+               $mCompiling = false,        # True if compiling to a template, false if executing to text
+               $mIgnorableDeps = array(),  # Dependency names which should be treated as static
+               $mFunctionCache = array(),  # A cache of function results keyed by argument hash
+
+               /** Built-in functions */
+               $mBuiltins = array(
+               'if'       => 'bi_if',
+               'true'     => 'bi_true',
+               '['        => 'bi_lbrace',
+               'lbrace'   => 'bi_lbrace',
+               ']'        => 'bi_rbrace',
+               'rbrace'   => 'bi_rbrace',
+               'escape'   => 'bi_escape',
+               '~'        => 'bi_escape',
+       );
+
+       /**
+        * Create a template processor for a given text, callback object and static dependency list
+        */
+       function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
+               $this->mText = $text;
+               $this->mFunctionObj = $functionObj;
+               $this->mIgnorableDeps = $ignorableDeps;
+       }
+
+       /**
+        * Execute the template.
+        * If $compile is true, produces an optimised template where functions with static 
+        * dependencies have been replaced by their return values.
+        */
+       function execute( $compile = false ) {
+               $fname = 'CBTProcessor::execute';
+               wfProfileIn( $fname );
+               $this->mCompiling = $compile;
+               $this->mLastError = false;
+               $val = $this->doText( 0, strlen( $this->mText ) );
+               $text = $val->getText();
+               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";
+               } 
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /** Shortcut for execute(true) */
+       function compile() {
+               $fname = 'CBTProcessor::compile';
+               wfProfileIn( $fname );
+               $s = $this->execute( true );
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /** Shortcut for doOpenText( $start, $end, false */
+       function doText( $start, $end ) {
+               return $this->doOpenText( $start, $end, false );
+       }
+
+       /** 
+        * Escape text for a template if we are producing a template. Do nothing
+        * if we are producing plain text.
+        */
+        function templateEscape( $text ) {
+               if ( $this->mCompiling ) {
+                       return templateEscape( $text );
+               } else {
+                       return $text;
+               }
+       }
+
+       /**
+        * 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 ) {
+               $fname = 'CBTProcessor::doOpenText';
+               wfProfileIn( $fname );
+               $in =& $this->mText;
+               $start = $p;
+               $ret = new CBTValue( '', array(), $this->mCompiling );
+               
+               $foundClosing = false;
+               while ( $p < $end ) {
+                       $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
+                       $pToken = $p + $matchLength;
+
+                       if ( $pToken >= $end ) {
+                               // No more braces, output remainder
+                               $ret->cat( substr( $in, $p ) );
+                               $p = $end;
+                               break;
+                       }
+
+                       // Output the text before the brace
+                       $ret->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] == '"' ) {
+                               wfProfileOut( $fname );
+                               $val = $this->doOpenFunction( $p, $end );
+                               wfProfileIn( $fname );
+                               if ( $p < $end && $in[$p] == '"' ) {
+                                       $val->setText( htmlspecialchars( $val->getText() ) );
+                               }
+                               $ret->cat( $val );
+                       } else {
+                               // Process the function mode component
+                               wfProfileOut( $fname );                         
+                               $ret->cat( $this->doOpenFunction( $p, $end ) );
+                               wfProfileIn( $fname );
+                       }
+               }
+               if ( $foundClosing && !$needClosing ) {
+                       $this->error( 'Errant closing brace', $p );
+               } elseif ( !$foundClosing && $needClosing ) {
+                       $this->error( 'Unclosed text section', $start );
+               }
+               wfProfileOut( $fname );                         
+               return $ret;
+       }
+
+       /**
+        * 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;
+               $tokens = array();
+               $unexecutedTokens = array();
+
+               $foundClosing = false;
+               while ( $p < $end ) {
+                       $char = $in[$p];
+                       if ( $char == '{' ) {
+                               // Switch to text mode
+                               ++$p;
+                               $tokenStart = $p;
+                               $token = $this->doOpenText( $p, $end );
+                               $tokens[] = $token;
+                               $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
+                       } 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 );
+                               $token = new CBTValue( substr( $in, $p, $tokenLength ) );
+                               // Execute the token as a function if it's not the function name
+                               if ( count( $tokens ) ) {
+                                       $tokens[] = $this->doFunction( array( $token ), $p );
+                               } else {
+                                       $tokens[] = $token;
+                               }
+                               $unexecutedTokens[] = $token->getText();
+
+                               $p += $tokenLength;
+                       }
+               }
+               if ( !$foundClosing && $needClosing ) {
+                       $this->error( 'Unclosed function', $start );
+                       return '';
+               }
+
+               $val = $this->doFunction( $tokens, $start );
+               if ( $this->mCompiling && !$val->isStatic() ) {
+                       $compiled = '';
+                       $first = true;
+                       foreach( $tokens as $i => $token ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $compiled .= ' ';
+                               }
+                               if ( $token->isStatic() ) {
+                                       if ( $i !== 0 ) {
+                                               $compiled .= '{' . $token->getText() . '}';
+                                       } else {
+                                               $compiled .= $token->getText();
+                                       }
+                               } else {
+                                       $compiled .= $unexecutedTokens[$i];
+                               }
+                       }
+
+                       // The dynamic parts of the string are still represented as functions, and 
+                       // function invocations have no dependencies. Thus the compiled result has 
+                       // no dependencies.
+                       $val = new CBTValue( "{{$compiled}}", array(), true );
+               }
+               return $val;
+       }
+
+       /**
+        * Execute a function, caching and returning the result value.
+        * $tokens is an array of CBTValue objects. $tokens[0] is the function 
+        * name, the others are arguments. $p is the string position, and is used
+        * for error messages only.
+        */
+       function doFunction( $tokens, $p ) {
+               if ( count( $tokens ) == 0 ) {
+                       return new CBTValue;
+               }
+               $fname = 'CBTProcessor::doFunction';
+               wfProfileIn( $fname );
+               
+               $ret = new CBTValue;
+               
+               // All functions implicitly depend on their arguments, and the function name
+               // While this is not strictly necessary for all functions, it's true almost 
+               // all the time and so convenient to do automatically. 
+               $ret->addDeps( $tokens );
+
+               $this->mCurrentPos = $p;
+               $func = array_shift( $tokens );
+               $func = $func->getText();
+
+               // Extract the text component from all the tokens
+               // And convert any templates to plain text
+               $textArgs = array();
+               foreach ( $tokens as $token ) {
+                       $token->execute( $this );
+                       $textArgs[] = $token->getText();
+               }
+
+               // Try the local cache
+               $cacheKey = $func . "\n" . implode( "\n", $textArgs );
+               if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
+                       $val = $this->mFunctionCache[$cacheKey];
+               } elseif ( isset( $this->mBuiltins[$func] ) ) {
+                       $func = $this->mBuiltins[$func];
+                       $val = call_user_func_array( array( &$this, $func ), $tokens );
+                       $this->mFunctionCache[$cacheKey] = $val;
+               } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
+                       $profName = get_class( $this->mFunctionObj ) . '::' . $func;
+                       wfProfileIn( "$fname-callback" );
+                       wfProfileIn( $profName );
+                       $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
+                       wfProfileOut( $profName );
+                       wfProfileOut( "$fname-callback" );
+                       $this->mFunctionCache[$cacheKey] = $val;
+               } else {
+                       $this->error( "Call of undefined function \"$func\"", $p );
+                       $val = new CBTValue;
+               }
+               if ( !is_object( $val ) ) {
+                       $val = new CBTValue((string)$val);
+               }
+
+               if ( CBT_DEBUG ) {
+                       $unexpanded = $val;
+               }
+
+               // If the output was a template, execute it
+               $val->execute( $this );
+               
+               if ( $this->mCompiling ) {
+                       // Escape any braces so that the output will be a valid template
+                       $val->templateEscape();
+               } 
+               $val->removeDeps( $this->mIgnorableDeps );
+               $ret->addDeps( $val );
+               $ret->setText( $val->getText() );
+
+               if ( CBT_DEBUG ) {
+                       wfDebug( "doFunction $func args = " 
+                               . var_export( $tokens, true ) 
+                               . "unexpanded return = " 
+                               . var_export( $unexpanded, true )
+                               . "expanded return = " 
+                               . var_export( $ret, true ) 
+                       );
+               }
+               
+               wfProfileOut( $fname );
+               return $ret;
+       }
+
+       /**
+        * 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;
+       }
+
+       /** 'if' built-in function */
+       function bi_if( $condition, $trueBlock, $falseBlock = null ) {
+               if ( is_null( $condition ) ) {
+                       $this->error( "Missing condition in if" );
+                       return '';
+               }
+
+               if ( $condition->getText() != '' ) {
+                       return new CBTValue( $trueBlock->getText(), 
+                               array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
+                               $trueBlock->mIsTemplate );
+               } else {
+                       if ( !is_null( $falseBlock ) ) {
+                               return new CBTValue( $falseBlock->getText(), 
+                                       array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
+                                       $falseBlock->mIsTemplate );
+                       } else {
+                               return new CBTValue( '', $condition->getDeps() );
+                       }
+               }
+       }
+
+       /** 'true' built-in function */
+       function bi_true() {
+               return "true";
+       }
+
+       /** left brace built-in */
+       function bi_lbrace() {
+               return '{';
+       }
+
+       /** right brace built-in */
+       function bi_rbrace() {
+               return '}';
+       }
+
+       /** 
+        * escape built-in.
+        * Escape text for inclusion in an HTML attribute 
+        */
+       function bi_escape( $val ) {
+               return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
+       }
+}
+?>
diff --git a/includes/cbt/README b/includes/cbt/README
new file mode 100644 (file)
index 0000000..8be27d1
--- /dev/null
@@ -0,0 +1,55 @@
+Template syntax
+---------------
+
+There are two modes: text mode and function mode. The brace characters "{" 
+and "}" are the only reserved characters. Either one of them will switch from
+text mode to function mode wherever they appear, no exceptions. 
+
+In text mode, all characters are passed through to the output. In function
+mode, text is split into tokens, delimited either by whitespace or by 
+matching pairs of braces. The first token is taken to be a function name. The
+other tokens are first processed in function mode themselves, then they are 
+passed to the named function as parameters. The return value of the function
+is passed through to the output.
+
+Example:
+   {escape {"hello"}}
+
+First brace switches to function mode. The function name is escape, the first
+and only parameter is {"hello"}. This parameter is executed. The braces around
+the parameter cause the parser to switch to text mode, thus the string "hello",
+including the quotes, is passed back and used as an argument to the escape 
+function. 
+
+Example:
+   {if title {<h1>{title}</h1>}}
+
+The function name is "if". The first parameter is the result of calling the 
+function "title". The second parameter is a level 1 HTML heading containing
+the result of the function "title". "if" is a built-in function which will 
+return the second parameter only if the first is non-blank, so the effect of
+this is to return a heading element only if a title exists.
+
+As a shortcut for generation of HTML attributes, if a function mode segment is
+surrounded by double quotes, quote characters in the return value will be 
+escaped. This only applies if the quote character immediately precedes the 
+opening brace, and immediately follows the closing brace, with no whitespace.
+
+User callback functions are defined by passing a function object to the 
+template processor. Function names appearing in the text are first checked
+against built-in function names, then against the method names in the function
+object. The function object forms a sandbox for execution of the template, so 
+security-conscious users may wish to avoid including functions that allow
+arbitrary filesystem access or code execution.
+
+The callback function will receive its parameters as strings. If the 
+result of the function depends only on the arguments, and certain things 
+understood to be "static", such as the source code, then the callback function
+should return a string. If the result depends on other things, then the function
+should return an object:
+
+   return new CBTValue( $text, $deps );
+
+where $deps is an array of string tokens, each one naming a dependency. As a 
+shortcut, if there is only one dependency, $deps may be a string.
+
index a5d5b54..a0dace6 100644 (file)
@@ -4,7 +4,8 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        die( "This file is part of MediaWiki, it is not a valid entry point\n" );
 }
 
-require_once( dirname(__FILE__) . '/../includes/CBTProcessor.php' );
+require_once( dirname(__FILE__) . '/../includes/cbt/CBTProcessor.php' );
+require_once( dirname(__FILE__) . '/../includes/cbt/CBTCompiler.php' );
 require_once( dirname(__FILE__) . '/../includes/SkinTemplate.php' );
 
 
@@ -26,8 +27,6 @@ require_once( dirname(__FILE__) . '/../includes/SkinTemplate.php' );
  * much as possible. In fact, the only SkinTemplate dependencies I know of at the 
  * moment are the functions to generate the gen=css and gen=js files. 
  * 
- * It assumes that $wgMemc is valid, if not, performance will be glacial as it will 
- * compile the skin on every invocation. 
  */
 class SkinMonoBookCBT extends SkinTemplate {
        var $mOut, $mTitle;
@@ -62,7 +61,9 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $text = $this->executeTemplate( $template );
                } else {
                        $compiled = $this->getCompiledTemplate( $sourceFile );
-                       $text = $this->executeTemplate( $compiled );
+                       
+                       #$text = $this->executeTemplate( $compiled );
+                       $text = eval( $compiled );
                }
                wfProfileOut( $fname );
                return $text;
@@ -94,9 +95,9 @@ class SkinMonoBookCBT extends SkinTemplate {
 
                $recompile = $wgRequest->getVal( 'recompile' );
                if ( !$recompile ) { 
-                       $compiled = $parserMemc->get( $cacheKey );
+                       $php = $parserMemc->get( $cacheKey );
                }
-               if ( $recompile || !$compiled ) {
+               if ( $recompile || !$php ) {
                        $template = file_get_contents( $sourceFile );
 
                        $ignore = array( 'lang', 'loggedin', 'user' );
@@ -124,10 +125,22 @@ class SkinMonoBookCBT extends SkinTemplate {
                        // more sure it is safe here.
                        $compiled = preg_replace( '/^[ \t]+/m', '', $compiled );
                        $compiled = preg_replace( '/[\r\n]+/', "\n", $compiled );
-                       $parserMemc->set( $cacheKey, $compiled, 3600 );
+
+                       // Compile to PHP
+                       $compiler = new CBTCompiler( $compiled );
+                       $compiler->compile();
+                       $php = 'return ' . $compiler->generatePHP( '$this' ) . ";\n";
+                       /*
+                       if ( !php_check_syntax( $php, $error ) ) {
+                               print "$error <pre>" . htmlspecialchars( $php )  . '</pre>';
+                               exit;
+                       }*/
+                               
+
+                       $parserMemc->set( $cacheKey, $php, 3600 );
                }
                wfProfileOut( $fname );
-               return $compiled;
+               return $php;
        }
 
        function executeTemplate( $template ) {
@@ -155,14 +168,14 @@ class SkinMonoBookCBT extends SkinTemplate {
        function mimetype() { return $GLOBALS['wgMimeType']; }
        function charset() { return $GLOBALS['wgOutputEncoding']; }
        function headlinks() { 
-               return new TplValue( $this->mOut->getHeadLinks(), 'dynamic' );
+               return new CBTValue( $this->mOut->getHeadLinks(), 'dynamic' );
        }
        function headscripts() { 
-               return new TplValue( $this->mOut->getScript(), 'dynamic' );
+               return new CBTValue( $this->mOut->getScript(), 'dynamic' );
        }
        
        function pagetitle() { 
-               return new TplValue( $this->mOut->getHTMLTitle(), array( 'title', 'lang' ) ); 
+               return new CBTValue( $this->mOut->getHTMLTitle(), array( 'title', 'lang' ) ); 
        }
        
        function stylepath() { return $GLOBALS['wgStylePath']; }
@@ -170,7 +183,7 @@ class SkinMonoBookCBT extends SkinTemplate {
        
        function notprintable() {
                global $wgRequest;
-               return new TplValue( !$wgRequest->getBool( 'printable' ), 'nonview dynamic' );
+               return new CBTValue( !$wgRequest->getBool( 'printable' ), 'nonview dynamic' );
        }
        
        function jsmimetype() { return $GLOBALS['wgJsMimeType']; }
@@ -184,7 +197,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $url = $this->makeUrl('-','action=raw&gen=js');
                }
-               return new TplValue( $url, 'loggedin' );
+               return new CBTValue( $url, 'loggedin' );
        }
        
        function pagecss() {
@@ -194,7 +207,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) );
 
                // Unknown dependencies
-               return new TplValue( $out, 'dynamic' );
+               return new CBTValue( $out, 'dynamic' );
        }
        
        function usercss() {
@@ -207,7 +220,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                }
 
                // Dynamic when not an ordinary page view, also depends on the username
-               return new TplValue( $usercss, array( 'nonview dynamic', 'user' ) );
+               return new CBTValue( $usercss, array( 'nonview dynamic', 'user' ) );
        }
        
        function sitecss() {
@@ -257,12 +270,12 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else { 
                        $deps = array();
                }
-               return new TplValue( $link, $deps, $isTemplate );
+               return new CBTValue( $link, $deps, $isTemplate );
        }
        
        function user_touched() {
                global $wgUser;
-               return new TplValue( $wgUser->mTouched, 'dynamic' );
+               return new CBTValue( $wgUser->mTouched, 'dynamic' );
        }
                
        function userjs() {
@@ -274,7 +287,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $url = $this->makeUrl($this->getUserPageText().'/'.$this->mStyleName.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s');
                }
-               return new TplValue( $url, array( 'nonview dynamic', 'user' ) );
+               return new CBTValue( $url, array( 'nonview dynamic', 'user' ) );
        }
        
        function userjsprev() {
@@ -285,7 +298,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $js = '';
                }
-               return new TplValue( $js, array( 'nonview dynamic' ) );
+               return new CBTValue( $js, array( 'nonview dynamic' ) );
        }
        
        function trackbackhtml() {
@@ -297,7 +310,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $tb = '';
                }
-               return new TplValue( $tb, 'dynamic' );
+               return new CBTValue( $tb, 'dynamic' );
        }
        
        function body_ondblclick() {
@@ -309,10 +322,10 @@ class SkinMonoBookCBT extends SkinTemplate {
                }
 
                if ( User::getDefaultOption('editondblclick') ) {
-                       return new TplValue( $js, 'user', 'title' );
+                       return new CBTValue( $js, 'user', 'title' );
                } else {
                        // Optimise away for logged-out users
-                       return new TplValue( $js, 'loggedin dynamic' );
+                       return new CBTValue( $js, 'loggedin dynamic' );
                }
        }
        
@@ -323,29 +336,29 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $js = '';
                }
-               return new TplValue( $js, 'loggedin dynamic' );
+               return new CBTValue( $js, 'loggedin dynamic' );
        }
        
        function nsclass() {
-               return new TplValue( 'ns-' . $this->mTitle->getNamespace(), 'title' );
+               return new CBTValue( 'ns-' . $this->mTitle->getNamespace(), 'title' );
        }
        
        function sitenotice() {
                // Perhaps this could be given special dependencies using our knowledge of what 
                // wfGetSiteNotice() depends on.
-               return new TplValue( wfGetSiteNotice(), 'dynamic' );
+               return new CBTValue( wfGetSiteNotice(), 'dynamic' );
        }
        
        function title() {
-               return new TplValue( $this->mOut->getPageTitle(), array( 'title', 'lang' ) );
+               return new CBTValue( $this->mOut->getPageTitle(), array( 'title', 'lang' ) );
        }
 
        function title_urlform() {
-               return new TplValue( $this->getThisTitleUrlForm(), 'title' );
+               return new CBTValue( $this->getThisTitleUrlForm(), 'title' );
        }
 
        function title_userurl() {
-               return new TplValue( urlencode( $this->mTitle->getDBkey() ), 'title' );
+               return new CBTValue( urlencode( $this->mTitle->getDBkey() ), 'title' );
        }
 
        function subtitle() {
@@ -355,11 +368,11 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = $this->mOut->getSubtitle();
                }
-               return new TplValue( $s, array( 'title', 'nonview dynamic' ) );
+               return new CBTValue( $s, array( 'title', 'nonview dynamic' ) );
        }
        
        function undelete() {
-               return new TplValue( $this->getUndeleteLink(), array( 'title', 'lang' ) );
+               return new CBTValue( $this->getUndeleteLink(), array( 'title', 'lang' ) );
        }
        
        function newtalk() {
@@ -398,20 +411,20 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $ntl = '';
                }
-               return new TplValue( $ntl, 'dynamic' );
+               return new CBTValue( $ntl, 'dynamic' );
        }
        
        function showjumplinks() {
                global $wgUser;
-               return new TplValue( $wgUser->getOption( 'showjumplinks' ) ? 'true' : '', 'user' );
+               return new CBTValue( $wgUser->getOption( 'showjumplinks' ) ? 'true' : '', 'user' );
        }
        
        function bodytext() {
-               return new TplValue( $this->mOut->getHTML(), 'dynamic' );
+               return new CBTValue( $this->mOut->getHTML(), 'dynamic' );
        }
        
        function catlinks() {
-               return new TplValue( $this->getCategories(), 'dynamic' );
+               return new CBTValue( $this->getCategories(), 'dynamic' );
        }
        
        function extratabs( $itemTemplate ) {
@@ -434,19 +447,19 @@ class SkinMonoBookCBT extends SkinTemplate {
                                $vcount ++;
                        }
                }
-               return new TplValue( $s, array(), true );
+               return new CBTValue( $s, array(), true );
        }
 
-       function is_special() { return new TplValue( $this->mTitle->getNamespace() == NS_SPECIAL, 'title' ); }
-       function can_edit() { return new TplValue( (string)($this->mTitle->userCanEdit()), 'dynamic' ); }
-       function can_move() { return new TplValue( (string)($this->mTitle->userCanMove()), 'dynamic' ); }
-       function is_talk() { return new TplValue( (string)($this->mTitle->isTalkPage()), 'title' ); }
-       function is_protected() { return new TplValue( (string)$this->mTitle->isProtected(), 'dynamic' ); }
-       function nskey() { return new TplValue( $this->mTitle->getNamespaceKey(), 'title' ); }
+       function is_special() { return new CBTValue( $this->mTitle->getNamespace() == NS_SPECIAL, 'title' ); }
+       function can_edit() { return new CBTValue( (string)($this->mTitle->userCanEdit()), 'dynamic' ); }
+       function can_move() { return new CBTValue( (string)($this->mTitle->userCanMove()), 'dynamic' ); }
+       function is_talk() { return new CBTValue( (string)($this->mTitle->isTalkPage()), 'title' ); }
+       function is_protected() { return new CBTValue( (string)$this->mTitle->isProtected(), 'dynamic' ); }
+       function nskey() { return new CBTValue( $this->mTitle->getNamespaceKey(), 'title' ); }
 
        function request_url() {
                global $wgRequest;
-               return new TplValue( $wgRequest->getRequestURL(), 'dynamic' );
+               return new CBTValue( $wgRequest->getRequestURL(), 'dynamic' );
        }
 
        function subject_url() { 
@@ -456,7 +469,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $url = $title->getLocalUrl( 'action=edit' );
                }
-               return new TplValue( $url, 'title' ); 
+               return new CBTValue( $url, 'title' ); 
        }
 
        function talk_url() {
@@ -466,19 +479,19 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $url = $title->getLocalUrl( 'action=edit' );
                }
-               return new TplValue( $url, 'title' );
+               return new CBTValue( $url, 'title' );
        }
 
        function edit_url() {
-               return new TplValue( $this->getEditUrl(), array( 'title', 'nonview dynamic' ) );
+               return new CBTValue( $this->getEditUrl(), array( 'title', 'nonview dynamic' ) );
        }
 
        function move_url() {
-               return new TplValue( $this->makeSpecialParamUrl( 'Movepage' ), array(), true );
+               return new CBTValue( $this->makeSpecialParamUrl( 'Movepage' ), array(), true );
        }
 
        function localurl( $query ) {
-               return new TplValue( $this->mTitle->getLocalURL( $query ), 'title' );
+               return new CBTValue( $this->mTitle->getLocalURL( $query ), 'title' );
        }
 
        function selecttab( $tab, $extraclass = '' ) {
@@ -528,19 +541,19 @@ class SkinMonoBookCBT extends SkinTemplate {
                                $s = '';
                        }
                }
-               return new TplValue( $s, array( 'nonview dynamic', 'title' ) );
+               return new CBTValue( $s, array( 'nonview dynamic', 'title' ) );
        }
 
        function subject_newclass() {
                $title = $this->getSubjectPage();
                $class = $title->exists() ? '' : 'new';
-               return new TplValue( $class, 'dynamic' );
+               return new CBTValue( $class, 'dynamic' );
        }
 
        function talk_newclass() {
                $title = $this->getTalkPage();
                $class = $title->exists() ? '' : 'new';
-               return new TplValue( $class, 'dynamic' );
+               return new CBTValue( $class, 'dynamic' );
        }       
 
        function ca_variant( $code, $name, $index, $template ) {
@@ -556,11 +569,11 @@ class SkinMonoBookCBT extends SkinTemplate {
                        '$text' => $name,
                        '$href' => htmlspecialchars( $this->mTitle->getLocalUrl( $actstr . 'variant=' . $code ) )
                ));
-               return new TplValue( $s, 'dynamic' );
+               return new CBTValue( $s, 'dynamic' );
        }
 
        function is_watching() {
-               return new TplValue( (string)$this->mTitle->userIsWatching(), array( 'dynamic' ) );
+               return new CBTValue( (string)$this->mTitle->userIsWatching(), array( 'dynamic' ) );
        }
 
        
@@ -592,7 +605,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $s .= "{login {{$etpl}}\n";
                }
                // No dependencies
-               return new TplValue( $s, array(), true /*this is a template*/ );
+               return new CBTValue( $s, array(), true /*this is a template*/ );
        }
 
        function userpage( $itemTemplate ) {
@@ -603,7 +616,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'user' );
+               return new CBTValue( $s, 'user' );
        }
        
        function mytalk( $itemTemplate ) {
@@ -615,7 +628,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'user' );
+               return new CBTValue( $s, 'user' );
        }
        
        function preferences( $itemTemplate ) {
@@ -625,7 +638,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, array( 'loggedin', 'lang' ) );
+               return new CBTValue( $s, array( 'loggedin', 'lang' ) );
        }
        
        function watchlist( $itemTemplate ) {
@@ -635,7 +648,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, array( 'loggedin', 'lang' ) );
+               return new CBTValue( $s, array( 'loggedin', 'lang' ) );
        }
        
        function mycontris( $itemTemplate ) {
@@ -646,7 +659,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'user' );
+               return new CBTValue( $s, 'user' );
        }
        
        function logout( $itemTemplate ) {
@@ -658,7 +671,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'loggedin dynamic' );
+               return new CBTValue( $s, 'loggedin dynamic' );
        }
        
        function anonuserpage( $itemTemplate ) {
@@ -669,7 +682,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $userPage = $this->getUserPageTitle();
                        $s = $this->makeTemplateLink( $itemTemplate, 'userpage', $userPage, $wgUser->getName() );
                }
-               return new TplValue( $s, '!loggedin dynamic' );
+               return new CBTValue( $s, '!loggedin dynamic' );
        }
        
        function anontalk( $itemTemplate ) {
@@ -680,7 +693,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $talkPage = $userPage->getTalkPage();
                        $s = $this->makeTemplateLink( $itemTemplate, 'mytalk', $talkPage, wfMsg('anontalk') );
                }
-               return new TplValue( $s, '!loggedin dynamic' );
+               return new CBTValue( $s, '!loggedin dynamic' );
        }
        
        function anonlogin( $itemTemplate ) {
@@ -690,7 +703,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $s = $this->makeSpecialTemplateLink( $itemTemplate, 'anonlogin', 'Userlogin', 
                                wfMsg( 'userlogin' ), 'returnto=' . urlencode( $this->getThisPDBK() ) );
                }
-               return new TplValue( $s, '!loggedin dynamic' );
+               return new CBTValue( $s, '!loggedin dynamic' );
        }
        
        function login( $itemTemplate ) {
@@ -700,7 +713,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        $s = $this->makeSpecialTemplateLink( $itemTemplate, 'login', 'Userlogin', 
                                wfMsg( 'userlogin' ), 'returnto=' . urlencode( $this->getThisPDBK() ) );
                }
-               return new TplValue( $s, '!loggedin dynamic' );
+               return new CBTValue( $s, '!loggedin dynamic' );
        }
        
        function logopath() { return $GLOBALS['wgLogo']; }
@@ -756,7 +769,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                }
 
                // Depends on user language only
-               return new TplValue( $s, 'lang' );
+               return new CBTValue( $s, 'lang' );
        }
        
        function searchaction() {
@@ -766,23 +779,23 @@ class SkinMonoBookCBT extends SkinTemplate {
        
        function search() {
                global $wgRequest;
-               return new TplValue( trim( $this->getSearch() ), 'special dynamic' );
+               return new CBTValue( trim( $this->getSearch() ), 'special dynamic' );
        }
        
        function notspecialpage() {
-               return new TplValue( $this->mTitle->getNamespace() != NS_SPECIAL, 'special' );
+               return new CBTValue( $this->mTitle->getNamespace() != NS_SPECIAL, 'special' );
        }
        
        function nav_whatlinkshere() {
-               return new TplValue( $this->makeSpecialParamUrl('Whatlinkshere' ), array(), true );
+               return new CBTValue( $this->makeSpecialParamUrl('Whatlinkshere' ), array(), true );
        }
 
        function article_exists() {
-               return new TplValue( (string)($this->mTitle->getArticleId() !== 0), 'title' );
+               return new CBTValue( (string)($this->mTitle->getArticleId() !== 0), 'title' );
        }
        
        function nav_recentchangeslinked() {
-               return new TplValue( $this->makeSpecialParamUrl('Recentchangeslinked' ), array(), true );
+               return new CBTValue( $this->makeSpecialParamUrl('Recentchangeslinked' ), array(), true );
        }
        
        function feeds( $itemTemplate = '' ) {
@@ -803,36 +816,36 @@ class SkinMonoBookCBT extends SkinTemplate {
                                        ) );
                        }
                }
-               return new TplValue( $feeds, 'special dynamic' );
+               return new CBTValue( $feeds, 'special dynamic' );
        }
 
        function is_userpage() {
                list( $id, $ip ) = $this->getUserPageIdIp();
-               return new TplValue( (string)($id || $ip), 'title' );
+               return new CBTValue( (string)($id || $ip), 'title' );
        }
 
        function is_ns_mediawiki() {
-               return new TplValue( (string)$this->mTitle->getNamespace() == NS_MEDIAWIKI, 'title' );
+               return new CBTValue( (string)$this->mTitle->getNamespace() == NS_MEDIAWIKI, 'title' );
        }
 
        function is_loggedin() {
                global $wgUser;
-               return new TplValue( (string)($wgUser->isLoggedIn()), 'loggedin' );
+               return new CBTValue( (string)($wgUser->isLoggedIn()), 'loggedin' );
        }
 
        function nav_contributions() {
                $url = $this->makeSpecialParamUrl( 'Contributions', '', '{title_userurl}' );
-               return new TplValue( $url, array(), true );
+               return new CBTValue( $url, array(), true );
        }
 
        function is_allowed( $right ) {
                global $wgUser;
-               return new TplValue( (string)$wgUser->isAllowed( $right ), 'user' );
+               return new CBTValue( (string)$wgUser->isAllowed( $right ), 'user' );
        }
        
        function nav_blockip() {
                $url = $this->makeSpecialParamUrl( 'Blockip', '', '{title_userurl}' );
-               return new TplValue( $url, array(), true );
+               return new CBTValue( $url, array(), true );
        }
        
        function nav_emailuser() {
@@ -840,7 +853,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                if ( !$wgEnableEmail || !$wgEnableUserEmail ) return '';
                
                $url = $this->makeSpecialParamUrl( 'Emailuser', '', '{title_userurl}' );
-               return new TplValue( $url, array(), true );
+               return new CBTValue( $url, array(), true );
        }
        
        function nav_upload() {
@@ -870,23 +883,23 @@ class SkinMonoBookCBT extends SkinTemplate {
                                $url = $wgRequest->appendQuery( 'printable=yes' );
                        }
                }
-               return new TplValue( $url, array( 'nonview dynamic', 'title' ) );
+               return new CBTValue( $url, array( 'nonview dynamic', 'title' ) );
        }
        
        function nav_permalink() {
                $url = (string)$this->getPermalink();
-               return new TplValue( $url, 'dynamic' );
+               return new CBTValue( $url, 'dynamic' );
        }
 
        function nav_trackbacklink() {
                global $wgUseTrackbacks;
                if ( !$wgUseTrackbacks ) return '';
 
-               return new TplValue( $this->mTitle->trackbackURL(), 'title' );
+               return new CBTValue( $this->mTitle->trackbackURL(), 'title' );
        }
        
        function is_permalink() {
-               return new TplValue( (string)($this->getPermalink() === false), 'nonview dynamic' );
+               return new CBTValue( (string)($this->getPermalink() === false), 'nonview dynamic' );
        }
        
        function toolboxend() {
@@ -915,7 +928,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                        }
                        $s = str_replace( '$body', $s, $outer );
                }
-               return new TplValue( $s, 'dynamic' );
+               return new CBTValue( $s, 'dynamic' );
        }
        
        function poweredbyico() { return $this->getPoweredBy(); }
@@ -930,7 +943,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'dynamic' );
+               return new CBTValue( $s, 'dynamic' );
        }
        
        function viewcount() {
@@ -948,7 +961,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $viewcount = '';
                }
-               return new TplValue( $viewcount, 'dynamic' );
+               return new CBTValue( $viewcount, 'dynamic' );
        }
        
        function numberofwatchingusers() {
@@ -968,7 +981,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $s = '';
                }
-               return new TplValue( $s, 'dynamic' );
+               return new CBTValue( $s, 'dynamic' );
        }
        
        function credits() {
@@ -982,7 +995,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                } else {
                        $credits = '';
                }
-               return new TplValue( $credits, 'view dynamic' );
+               return new CBTValue( $credits, 'view dynamic' );
        }
        
        function normalcopyright() {
@@ -995,7 +1008,7 @@ class SkinMonoBookCBT extends SkinTemplate {
 
        function is_currentview() {
                global $wgRequest;
-               return new TplValue( (string)$this->isCurrentArticleView(), 'view' );
+               return new CBTValue( (string)$this->isCurrentArticleView(), 'view' );
        }
 
        function usehistorycopyright() {
@@ -1005,17 +1018,17 @@ class SkinMonoBookCBT extends SkinTemplate {
                $oldid = $this->getOldId();
                $diff = $this->getDiff();
                $use = (string)(!is_null( $oldid ) && is_null( $diff ));
-               return new TplValue( $use, 'nonview dynamic' );
+               return new CBTValue( $use, 'nonview dynamic' );
        }
        
        function privacy() {
-               return new TplValue( $this->privacyLink(), 'lang' );
+               return new CBTValue( $this->privacyLink(), 'lang' );
        }
        function about() {
-               return new TplValue( $this->aboutLink(), 'lang' );
+               return new CBTValue( $this->aboutLink(), 'lang' );
        }
        function disclaimer() {
-               return new TplValue( $this->disclaimerLink(), 'lang' );
+               return new CBTValue( $this->disclaimerLink(), 'lang' );
        }
        function tagline() { 
                # A reference to this tag existed in the old MonoBook.php, but the
@@ -1023,11 +1036,11 @@ class SkinMonoBookCBT extends SkinTemplate {
                return ''; 
        }
        function reporttime() {
-               return new TplValue( $this->mOut->reportTime(), 'dynamic' );
+               return new CBTValue( $this->mOut->reportTime(), 'dynamic' );
        }
        
        function msg( $name ) {
-               return new TplValue( wfMsg( $name ), 'lang' );
+               return new CBTValue( wfMsg( $name ), 'lang' );
        }
        
        function fallbackmsg( $name, $fallback ) {
@@ -1035,7 +1048,7 @@ class SkinMonoBookCBT extends SkinTemplate {
                if ( wfEmptyMsg( $name, $text ) ) {
                        $text = $fallback;
                }
-               return new TplValue( $text,  'lang' );
+               return new CBTValue( $text,  'lang' );
        }
 
        /******************************************************