* (bug 13040) Gender-aware user namespace aliases
[lhc/web/wiklou.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 }