API * Optimized backlinking query (still needs an index change)
[mediawiki.git] / includes / cbt / CBTCompiler.php
blob4ef8ee4a95edd0f990894edb54db83b4ed8aac50
1 <?php
3 /**
4 * This file contains functions to convert callback templates to other languages.
5 * The template should first be pre-processed with CBTProcessor to remove static
6 * sections.
7 */
10 require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
12 /**
13 * Push a value onto the stack
14 * Argument 1: value
16 define( 'CBT_PUSH', 1 );
18 /**
19 * Pop, concatenate argument, push
20 * Argument 1: value
22 define( 'CBT_CAT', 2 );
24 /**
25 * Concatenate where the argument is on the stack, instead of immediate
27 define( 'CBT_CATS', 3 );
29 /**
30 * Call a function, push the return value onto the stack and put it in the cache
31 * Argument 1: argument count
33 * The arguments to the function are on the stack
35 define( 'CBT_CALL', 4 );
37 /**
38 * Pop, htmlspecialchars, push
40 define( 'CBT_HX', 5 );
42 class CBTOp {
43 var $opcode;
44 var $arg1;
45 var $arg2;
47 function CBTOp( $opcode, $arg1, $arg2 ) {
48 $this->opcode = $opcode;
49 $this->arg1 = $arg1;
50 $this->arg2 = $arg2;
53 function name() {
54 $opcodeNames = array(
55 CBT_PUSH => 'PUSH',
56 CBT_CAT => 'CAT',
57 CBT_CATS => 'CATS',
58 CBT_CALL => 'CALL',
59 CBT_HX => 'HX',
61 return $opcodeNames[$this->opcode];
65 class CBTCompiler {
66 var $mOps = array();
67 var $mCode;
69 function CBTCompiler( $text ) {
70 $this->mText = $text;
73 /**
74 * Compile the text.
75 * Returns true on success, error message on failure
77 function compile() {
78 $fname = 'CBTProcessor::compile';
79 $this->mLastError = false;
80 $this->mOps = array();
82 $this->doText( 0, strlen( $this->mText ) );
84 if ( $this->mLastError !== false ) {
85 $pos = $this->mErrorPos;
87 // Find the line number at which the error occurred
88 $startLine = 0;
89 $endLine = 0;
90 $line = 0;
91 do {
92 if ( $endLine ) {
93 $startLine = $endLine + 1;
95 $endLine = strpos( $this->mText, "\n", $startLine );
96 ++$line;
97 } while ( $endLine !== false && $endLine < $pos );
99 $text = "Template error at line $line: $this->mLastError\n<pre>\n";
101 $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
102 $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
103 } else {
104 $text = true;
107 return $text;
110 /** Shortcut for doOpenText( $start, $end, false */
111 function doText( $start, $end ) {
112 return $this->doOpenText( $start, $end, false );
115 function phpQuote( $text ) {
116 return "'" . strtr( $text, array( "\\" => "\\\\", "'" => "\\'" ) ) . "'";
119 function op( $opcode, $arg1 = null, $arg2 = null) {
120 return new CBTOp( $opcode, $arg1, $arg2 );
124 * Recursive workhorse for text mode.
126 * Processes text mode starting from offset $p, until either $end is
127 * reached or a closing brace is found. If $needClosing is false, a
128 * closing brace will flag an error, if $needClosing is true, the lack
129 * of a closing brace will flag an error.
131 * The parameter $p is advanced to the position after the closing brace,
132 * or after the end. A CBTValue is returned.
134 * @private
136 function doOpenText( &$p, $end, $needClosing = true ) {
137 $in =& $this->mText;
138 $start = $p;
139 $atStart = true;
141 $foundClosing = false;
142 while ( $p < $end ) {
143 $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
144 $pToken = $p + $matchLength;
146 if ( $pToken >= $end ) {
147 // No more braces, output remainder
148 if ( $atStart ) {
149 $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p ) );
150 $atStart = false;
151 } else {
152 $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p ) );
154 $p = $end;
155 break;
158 // Output the text before the brace
159 if ( $atStart ) {
160 $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $matchLength ) );
161 $atStart = false;
162 } else {
163 $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) );
166 // Advance the pointer
167 $p = $pToken + 1;
169 // Check for closing brace
170 if ( $in[$pToken] == '}' ) {
171 $foundClosing = true;
172 break;
175 // Handle the "{fn}" special case
176 if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
177 $this->doOpenFunction( $p, $end );
178 if ( $p < $end && $in[$p] == '"' ) {
179 $this->mOps[] = $this->op( CBT_HX );
181 } else {
182 $this->doOpenFunction( $p, $end );
184 if ( $atStart ) {
185 $atStart = false;
186 } else {
187 $this->mOps[] = $this->op( CBT_CATS );
190 if ( $foundClosing && !$needClosing ) {
191 $this->error( 'Errant closing brace', $p );
192 } elseif ( !$foundClosing && $needClosing ) {
193 $this->error( 'Unclosed text section', $start );
194 } else {
195 if ( $atStart ) {
196 $this->mOps[] = $this->op( CBT_PUSH, '' );
202 * Recursive workhorse for function mode.
204 * Processes function mode starting from offset $p, until either $end is
205 * reached or a closing brace is found. If $needClosing is false, a
206 * closing brace will flag an error, if $needClosing is true, the lack
207 * of a closing brace will flag an error.
209 * The parameter $p is advanced to the position after the closing brace,
210 * or after the end. A CBTValue is returned.
212 * @private
214 function doOpenFunction( &$p, $end, $needClosing = true ) {
215 $in =& $this->mText;
216 $start = $p;
217 $argCount = 0;
219 $foundClosing = false;
220 while ( $p < $end ) {
221 $char = $in[$p];
222 if ( $char == '{' ) {
223 // Switch to text mode
224 ++$p;
225 $tokenStart = $p;
226 $this->doOpenText( $p, $end );
227 ++$argCount;
228 } elseif ( $char == '}' ) {
229 // Block end
230 ++$p;
231 $foundClosing = true;
232 break;
233 } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
234 // Whitespace
235 // Consume the rest of the whitespace
236 $p += strspn( $in, CBT_WHITE, $p, $end - $p );
237 } else {
238 // Token, find the end of it
239 $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
240 $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $tokenLength ) );
242 // Execute the token as a function if it's not the function name
243 if ( $argCount ) {
244 $this->mOps[] = $this->op( CBT_CALL, 1 );
247 $p += $tokenLength;
248 ++$argCount;
251 if ( !$foundClosing && $needClosing ) {
252 $this->error( 'Unclosed function', $start );
253 return '';
256 $this->mOps[] = $this->op( CBT_CALL, $argCount );
260 * Set a flag indicating that an error has been found.
262 function error( $text, $pos = false ) {
263 $this->mLastError = $text;
264 if ( $pos === false ) {
265 $this->mErrorPos = $this->mCurrentPos;
266 } else {
267 $this->mErrorPos = $pos;
271 function getLastError() {
272 return $this->mLastError;
275 function opsToString() {
276 $s = '';
277 foreach( $this->mOps as $op ) {
278 $s .= $op->name();
279 if ( !is_null( $op->arg1 ) ) {
280 $s .= ' ' . var_export( $op->arg1, true );
282 if ( !is_null( $op->arg2 ) ) {
283 $s .= ' ' . var_export( $op->arg2, true );
285 $s .= "\n";
287 return $s;
290 function generatePHP( $functionObj ) {
291 $fname = 'CBTCompiler::generatePHP';
292 wfProfileIn( $fname );
293 $stack = array();
295 foreach( $this->mOps as $index => $op ) {
296 switch( $op->opcode ) {
297 case CBT_PUSH:
298 $stack[] = $this->phpQuote( $op->arg1 );
299 break;
300 case CBT_CAT:
301 $val = array_pop( $stack );
302 array_push( $stack, "$val . " . $this->phpQuote( $op->arg1 ) );
303 break;
304 case CBT_CATS:
305 $right = array_pop( $stack );
306 $left = array_pop( $stack );
307 array_push( $stack, "$left . $right" );
308 break;
309 case CBT_CALL:
310 $args = array_slice( $stack, count( $stack ) - $op->arg1, $op->arg1 );
311 $stack = array_slice( $stack, 0, count( $stack ) - $op->arg1 );
313 // Some special optimised expansions
314 if ( $op->arg1 == 0 ) {
315 $result = '';
316 } else {
317 $func = array_shift( $args );
318 if ( substr( $func, 0, 1 ) == "'" && substr( $func, -1 ) == "'" ) {
319 $func = substr( $func, 1, strlen( $func ) - 2 );
320 if ( $func == "if" ) {
321 if ( $op->arg1 < 3 ) {
322 // This should have been caught during processing
323 return "Not enough arguments to if";
324 } elseif ( $op->arg1 == 3 ) {
325 $result = "(({$args[0]} != '') ? ({$args[1]}) : '')";
326 } else {
327 $result = "(({$args[0]} != '') ? ({$args[1]}) : ({$args[2]}))";
329 } elseif ( $func == "true" ) {
330 $result = "true";
331 } elseif( $func == "lbrace" || $func == "{" ) {
332 $result = "{";
333 } elseif( $func == "rbrace" || $func == "}" ) {
334 $result = "}";
335 } elseif ( $func == "escape" || $func == "~" ) {
336 $result = "htmlspecialchars({$args[0]})";
337 } else {
338 // Known function name
339 $result = "{$functionObj}->{$func}(" . implode( ', ', $args ) . ')';
341 } else {
342 // Unknown function name
343 $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )';
346 array_push( $stack, $result );
347 break;
348 case CBT_HX:
349 $val = array_pop( $stack );
350 array_push( $stack, "htmlspecialchars( $val )" );
351 break;
352 default:
353 return "Unknown opcode {$op->opcode}\n";
356 wfProfileOut( $fname );
357 if ( count( $stack ) !== 1 ) {
358 return "Error, stack count incorrect\n";
360 return '
361 global $cbtExecutingGenerated;
362 ++$cbtExecutingGenerated;
363 $output = ' . $stack[0] . ';
364 --$cbtExecutingGenerated;
365 return $output;