]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/cbt/CBTCompiler.php
MediaWiki 1.14.0
[autoinstallsdev/mediawiki.git] / includes / cbt / CBTCompiler.php
1 <?php
2
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  */
8
9
10 require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
11
12 /**
13  * Push a value onto the stack
14  * Argument 1: value
15  */
16 define( 'CBT_PUSH', 1 );
17
18 /**
19  * Pop, concatenate argument, push
20  * Argument 1: value
21  */
22 define( 'CBT_CAT', 2 );
23
24 /**
25  * Concatenate where the argument is on the stack, instead of immediate
26  */
27 define( 'CBT_CATS', 3 );
28
29 /**
30  * Call a function, push the return value onto the stack and put it in the cache
31  * Argument 1: argument count
32  *
33  * The arguments to the function are on the stack
34  */
35 define( 'CBT_CALL', 4 );
36
37 /**
38  * Pop, htmlspecialchars, push
39  */
40 define( 'CBT_HX', 5 );
41
42 class CBTOp {
43         var $opcode;
44         var $arg1;
45         var $arg2;
46
47         function CBTOp( $opcode, $arg1, $arg2 ) {
48                 $this->opcode = $opcode;
49                 $this->arg1 = $arg1;
50                 $this->arg2 = $arg2;
51         }
52
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',
60                 );
61                 return $opcodeNames[$this->opcode];
62         }
63 };
64
65 class CBTCompiler {
66         var $mOps = array();
67         var $mCode;
68
69         function CBTCompiler( $text ) {
70                 $this->mText = $text;
71         }
72
73         /**
74          * Compile the text.
75          * Returns true on success, error message on failure
76          */
77         function compile() {
78                 $this->mLastError = false;
79                 $this->mOps = array();
80
81                 $this->doText( 0, strlen( $this->mText ) );
82
83                 if ( $this->mLastError !== false ) {
84                         $pos = $this->mErrorPos;
85
86                         // Find the line number at which the error occurred
87                         $startLine = 0;
88                         $endLine = 0;
89                         $line = 0;
90                         do {
91                                 if ( $endLine ) {
92                                         $startLine = $endLine + 1;
93                                 }
94                                 $endLine = strpos( $this->mText, "\n", $startLine );
95                                 ++$line;
96                         } while ( $endLine !== false && $endLine < $pos );
97
98                         $text = "Template error at line $line: $this->mLastError\n<pre>\n";
99
100                         $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
101                         $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
102                 } else {
103                         $text = true;
104                 }
105
106                 return $text;
107         }
108
109         /** Shortcut for doOpenText( $start, $end, false */
110         function doText( $start, $end ) {
111                 return $this->doOpenText( $start, $end, false );
112         }
113
114         function phpQuote( $text ) {
115                 return "'" . strtr( $text, array( "\\" => "\\\\", "'" => "\\'" ) ) . "'";
116         }
117
118         function op( $opcode, $arg1 = null, $arg2 = null) {
119                 return new CBTOp( $opcode, $arg1, $arg2 );
120         }
121
122         /**
123          * Recursive workhorse for text mode.
124          *
125          * Processes text mode starting from offset $p, until either $end is
126          * reached or a closing brace is found. If $needClosing is false, a
127          * closing brace will flag an error, if $needClosing is true, the lack
128          * of a closing brace will flag an error.
129          *
130          * The parameter $p is advanced to the position after the closing brace,
131          * or after the end. A CBTValue is returned.
132          *
133          * @private
134          */
135         function doOpenText( &$p, $end, $needClosing = true ) {
136                 $in =& $this->mText;
137                 $start = $p;
138                 $atStart = true;
139
140                 $foundClosing = false;
141                 while ( $p < $end ) {
142                         $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
143                         $pToken = $p + $matchLength;
144
145                         if ( $pToken >= $end ) {
146                                 // No more braces, output remainder
147                                 if ( $atStart ) {
148                                         $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p ) );
149                                         $atStart = false;
150                                 } else {
151                                         $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p ) );
152                                 }
153                                 $p = $end;
154                                 break;
155                         }
156
157                         // Output the text before the brace
158                         if ( $atStart ) {
159                                 $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $matchLength ) );
160                                 $atStart = false;
161                         } else {
162                                 $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) );
163                         }
164
165                         // Advance the pointer
166                         $p = $pToken + 1;
167
168                         // Check for closing brace
169                         if ( $in[$pToken] == '}' ) {
170                                 $foundClosing = true;
171                                 break;
172                         }
173
174                         // Handle the "{fn}" special case
175                         if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
176                                 $this->doOpenFunction( $p, $end );
177                                 if ( $p < $end && $in[$p] == '"' ) {
178                                         $this->mOps[] = $this->op( CBT_HX );
179                                 }
180                         } else {
181                                 $this->doOpenFunction( $p, $end );
182                         }
183                         if ( $atStart ) {
184                                 $atStart = false;
185                         } else {
186                                 $this->mOps[] = $this->op( CBT_CATS );
187                         }
188                 }
189                 if ( $foundClosing && !$needClosing ) {
190                         $this->error( 'Errant closing brace', $p );
191                 } elseif ( !$foundClosing && $needClosing ) {
192                         $this->error( 'Unclosed text section', $start );
193                 } else {
194                         if ( $atStart ) {
195                                 $this->mOps[] = $this->op( CBT_PUSH, '' );
196                         }
197                 }
198         }
199
200         /**
201          * Recursive workhorse for function mode.
202          *
203          * Processes function mode starting from offset $p, until either $end is
204          * reached or a closing brace is found. If $needClosing is false, a
205          * closing brace will flag an error, if $needClosing is true, the lack
206          * of a closing brace will flag an error.
207          *
208          * The parameter $p is advanced to the position after the closing brace,
209          * or after the end. A CBTValue is returned.
210          *
211          * @private
212          */
213         function doOpenFunction( &$p, $end, $needClosing = true ) {
214                 $in =& $this->mText;
215                 $start = $p;
216                 $argCount = 0;
217
218                 $foundClosing = false;
219                 while ( $p < $end ) {
220                         $char = $in[$p];
221                         if ( $char == '{' ) {
222                                 // Switch to text mode
223                                 ++$p;
224                                 $this->doOpenText( $p, $end );
225                                 ++$argCount;
226                         } elseif ( $char == '}' ) {
227                                 // Block end
228                                 ++$p;
229                                 $foundClosing = true;
230                                 break;
231                         } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
232                                 // Whitespace
233                                 // Consume the rest of the whitespace
234                                 $p += strspn( $in, CBT_WHITE, $p, $end - $p );
235                         } else {
236                                 // Token, find the end of it
237                                 $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
238                                 $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $tokenLength ) );
239
240                                 // Execute the token as a function if it's not the function name
241                                 if ( $argCount ) {
242                                         $this->mOps[] = $this->op( CBT_CALL, 1 );
243                                 }
244
245                                 $p += $tokenLength;
246                                 ++$argCount;
247                         }
248                 }
249                 if ( !$foundClosing && $needClosing ) {
250                         $this->error( 'Unclosed function', $start );
251                         return '';
252                 }
253
254                 $this->mOps[] = $this->op( CBT_CALL, $argCount );
255         }
256
257         /**
258          * Set a flag indicating that an error has been found.
259          */
260         function error( $text, $pos = false ) {
261                 $this->mLastError = $text;
262                 if ( $pos === false ) {
263                         $this->mErrorPos = $this->mCurrentPos;
264                 } else {
265                         $this->mErrorPos = $pos;
266                 }
267         }
268
269         function getLastError() {
270                 return $this->mLastError;
271         }
272
273         function opsToString() {
274                 $s = '';
275                 foreach( $this->mOps as $op ) {
276                         $s .= $op->name();
277                         if ( !is_null( $op->arg1 ) ) {
278                                 $s .= ' ' . var_export( $op->arg1, true );
279                         }
280                         if ( !is_null( $op->arg2 ) ) {
281                                 $s .= ' ' . var_export( $op->arg2, true );
282                         }
283                         $s .= "\n";
284                 }
285                 return $s;
286         }
287
288         function generatePHP( $functionObj ) {
289                 $fname = 'CBTCompiler::generatePHP';
290                 wfProfileIn( $fname );
291                 $stack = array();
292
293                 foreach( $this->mOps as $op ) {
294                         switch( $op->opcode ) {
295                                 case CBT_PUSH:
296                                         $stack[] = $this->phpQuote( $op->arg1 );
297                                         break;
298                                 case CBT_CAT:
299                                         $val = array_pop( $stack );
300                                         array_push( $stack, "$val . " . $this->phpQuote( $op->arg1 ) );
301                                         break;
302                                 case CBT_CATS:
303                                         $right = array_pop( $stack );
304                                         $left = array_pop( $stack );
305                                         array_push( $stack, "$left . $right" );
306                                         break;
307                                 case CBT_CALL:
308                                         $args = array_slice( $stack, count( $stack ) - $op->arg1, $op->arg1 );
309                                         $stack = array_slice( $stack, 0, count( $stack ) - $op->arg1 );
310
311                                         // Some special optimised expansions
312                                         if ( $op->arg1 == 0 ) {
313                                                 $result = '';
314                                         } else {
315                                                 $func = array_shift( $args );
316                                                 if ( substr( $func, 0, 1 ) == "'" &&  substr( $func, -1 ) == "'" ) {
317                                                         $func = substr( $func, 1, strlen( $func ) - 2 );
318                                                         if ( $func == "if" ) {
319                                                                 if ( $op->arg1 < 3 ) {
320                                                                         // This should have been caught during processing
321                                                                         return "Not enough arguments to if";
322                                                                 } elseif ( $op->arg1 == 3 ) {
323                                                                         $result = "(({$args[0]} != '') ? ({$args[1]}) : '')";
324                                                                 } else {
325                                                                         $result = "(({$args[0]} != '') ? ({$args[1]}) : ({$args[2]}))";
326                                                                 }
327                                                         } elseif ( $func == "true" ) {
328                                                                 $result = "true";
329                                                         } elseif( $func == "lbrace" || $func == "{" ) {
330                                                                 $result = "{";
331                                                         } elseif( $func == "rbrace" || $func == "}" ) {
332                                                                 $result = "}";
333                                                         } elseif ( $func == "escape" || $func == "~" ) {
334                                                                 $result = "htmlspecialchars({$args[0]})";
335                                                         } else {
336                                                                 // Known function name
337                                                                 $result = "{$functionObj}->{$func}(" . implode( ', ', $args ) . ')';
338                                                         }
339                                                 } else {
340                                                         // Unknown function name
341                                                         $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )';
342                                                 }
343                                         }
344                                         array_push( $stack, $result );
345                                         break;
346                                 case CBT_HX:
347                                         $val = array_pop( $stack );
348                                         array_push( $stack, "htmlspecialchars( $val )" );
349                                         break;
350                                 default:
351                                         return "Unknown opcode {$op->opcode}\n";
352                         }
353                 }
354                 wfProfileOut( $fname );
355                 if ( count( $stack ) !== 1 ) {
356                         return "Error, stack count incorrect\n";
357                 }
358                 return '
359                         global $cbtExecutingGenerated;
360                         ++$cbtExecutingGenerated;
361                         $output = ' . $stack[0] . ';
362                         --$cbtExecutingGenerated;
363                         return $output;
364                         ';
365         }
366 }