]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/cbt/CBTProcessor.php
MediaWiki 1.14.0
[autoinstallsdev/mediawiki.git] / includes / cbt / CBTProcessor.php
1 <?php
2
3 /**
4  * PHP version of the callback template processor
5  * This is currently used as a test rig and is likely to be used for
6  * compatibility purposes later, where the C++ extension is not available.
7  */
8
9 define( 'CBT_WHITE', " \t\r\n" );
10 define( 'CBT_BRACE', '{}' );
11 define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
12 define( 'CBT_DEBUG', 0 );
13
14 $GLOBALS['cbtExecutingGenerated'] = 0;
15
16 /**
17  * Attempting to be a MediaWiki-independent module
18  */
19 if ( !function_exists( 'wfProfileIn' ) ) {
20         function wfProfileIn() {}
21 }
22 if ( !function_exists( 'wfProfileOut' ) ) {
23         function wfProfileOut() {}
24 }
25
26 /**
27  * Escape text for inclusion in template
28  */
29 function cbt_escape( $text ) {
30         return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
31 }
32
33 /**
34  * Create a CBTValue
35  */
36 function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
37         global $cbtExecutingGenerated;
38         if ( $cbtExecutingGenerated ) {
39                 return $text;
40         } else {
41                 return new CBTValue( $text, $deps, $isTemplate );
42         }
43 }
44
45 /**
46  * A dependency-tracking value class
47  * Callback functions should return one of these, unless they have
48  * no dependencies in which case they can return a string.
49  */
50 class CBTValue {
51         var $mText, $mDeps, $mIsTemplate;
52
53         /**
54          * Create a new value
55          * @param $text String: , default ''.
56          * @param $deps Array: what this value depends on
57          * @param $isTemplate Bool: whether the result needs compilation/execution, default 'false'.
58          */
59         function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
60                 $this->mText = $text;
61                 if ( !is_array( $deps ) ) {
62                         $this->mDeps = array( $deps ) ;
63                 } else {
64                         $this->mDeps = $deps;
65                 }
66                 $this->mIsTemplate = $isTemplate;
67         }
68
69         /** Concatenate two values, merging their dependencies */
70         function cat( $val ) {
71                 if ( is_object( $val ) ) {
72                         $this->addDeps( $val );
73                         $this->mText .= $val->mText;
74                 } else {
75                         $this->mText .= $val;
76                 }
77         }
78
79         /** Add the dependencies of another value to this one */
80         function addDeps( $values ) {
81                 if ( !is_array( $values ) ) {
82                         $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
83                 } else {
84                         foreach ( $values as $val ) {
85                                 if ( !is_object( $val ) ) {
86                                         var_dump( debug_backtrace() );
87                                         exit;
88                                 }
89                                 $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
90                         }
91                 }
92         }
93
94         /** Remove a list of dependencies */
95         function removeDeps( $deps ) {
96                 $this->mDeps = array_diff( $this->mDeps, $deps );
97         }
98
99         function setText( $text ) {
100                 $this->mText = $text;
101         }
102
103         function getText() {
104                 return $this->mText;
105         }
106
107         function getDeps() {
108                 return $this->mDeps;
109         }
110
111         /** If the value is a template, execute it */
112         function execute( &$processor ) {
113                 if ( $this->mIsTemplate ) {
114                         $myProcessor = new CBTProcessor( $this->mText,  $processor->mFunctionObj, $processor->mIgnorableDeps );
115                         $myProcessor->mCompiling = $processor->mCompiling;
116                         $val = $myProcessor->doText( 0, strlen( $this->mText ) );
117                         if ( $myProcessor->getLastError() ) {
118                                 $processor->error( $myProcessor->getLastError() );
119                                 $this->mText = '';
120                         } else {
121                                 $this->mText = $val->mText;
122                                 $this->addDeps( $val );
123                         }
124                         if ( !$processor->mCompiling ) {
125                                 $this->mIsTemplate = false;
126                         }
127                 }
128         }
129
130         /** If the value is plain text, escape it for inclusion in a template */
131         function templateEscape() {
132                 if ( !$this->mIsTemplate ) {
133                         $this->mText = cbt_escape( $this->mText );
134                 }
135         }
136
137         /** Return true if the value has no dependencies */
138         function isStatic() {
139                 return count( $this->mDeps ) == 0;
140         }
141 }
142
143 /**
144  * Template processor, for compilation and execution
145  */
146 class CBTProcessor {
147         var $mText,                     # The text being processed
148                 $mFunctionObj,              # The object containing callback functions
149                 $mCompiling = false,        # True if compiling to a template, false if executing to text
150                 $mIgnorableDeps = array(),  # Dependency names which should be treated as static
151                 $mFunctionCache = array(),  # A cache of function results keyed by argument hash
152                 $mLastError = false,        # Last error message or false for no error
153                 $mErrorPos = 0,             # Last error position
154
155                 /** Built-in functions */
156                 $mBuiltins = array(
157                 'if'       => 'bi_if',
158                 'true'     => 'bi_true',
159                 '['        => 'bi_lbrace',
160                 'lbrace'   => 'bi_lbrace',
161                 ']'        => 'bi_rbrace',
162                 'rbrace'   => 'bi_rbrace',
163                 'escape'   => 'bi_escape',
164                 '~'        => 'bi_escape',
165         );
166
167         /**
168          * Create a template processor for a given text, callback object and static dependency list
169          */
170         function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
171                 $this->mText = $text;
172                 $this->mFunctionObj = $functionObj;
173                 $this->mIgnorableDeps = $ignorableDeps;
174         }
175
176         /**
177          * Execute the template.
178          * If $compile is true, produces an optimised template where functions with static
179          * dependencies have been replaced by their return values.
180          */
181         function execute( $compile = false ) {
182                 $fname = 'CBTProcessor::execute';
183                 wfProfileIn( $fname );
184                 $this->mCompiling = $compile;
185                 $this->mLastError = false;
186                 $val = $this->doText( 0, strlen( $this->mText ) );
187                 $text = $val->getText();
188                 if ( $this->mLastError !== false ) {
189                         $pos = $this->mErrorPos;
190
191                         // Find the line number at which the error occurred
192                         $startLine = 0;
193                         $endLine = 0;
194                         $line = 0;
195                         do {
196                                 if ( $endLine ) {
197                                         $startLine = $endLine + 1;
198                                 }
199                                 $endLine = strpos( $this->mText, "\n", $startLine );
200                                 ++$line;
201                         } while ( $endLine !== false && $endLine < $pos );
202
203                         $text = "Template error at line $line: $this->mLastError\n<pre>\n";
204
205                         $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
206                         $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
207                 }
208                 wfProfileOut( $fname );
209                 return $text;
210         }
211
212         /** Shortcut for execute(true) */
213         function compile() {
214                 $fname = 'CBTProcessor::compile';
215                 wfProfileIn( $fname );
216                 $s = $this->execute( true );
217                 wfProfileOut( $fname );
218                 return $s;
219         }
220
221         /** Shortcut for doOpenText( $start, $end, false */
222         function doText( $start, $end ) {
223                 return $this->doOpenText( $start, $end, false );
224         }
225
226         /**
227          * Escape text for a template if we are producing a template. Do nothing
228          * if we are producing plain text.
229          */
230          function templateEscape( $text ) {
231                 if ( $this->mCompiling ) {
232                         return cbt_escape( $text );
233                 } else {
234                         return $text;
235                 }
236         }
237
238         /**
239          * Recursive workhorse for text mode.
240          *
241          * Processes text mode starting from offset $p, until either $end is
242          * reached or a closing brace is found. If $needClosing is false, a
243          * closing brace will flag an error, if $needClosing is true, the lack
244          * of a closing brace will flag an error.
245          *
246          * The parameter $p is advanced to the position after the closing brace,
247          * or after the end. A CBTValue is returned.
248          *
249          * @private
250          */
251         function doOpenText( &$p, $end, $needClosing = true ) {
252                 $fname = 'CBTProcessor::doOpenText';
253                 wfProfileIn( $fname );
254                 $in =& $this->mText;
255                 $start = $p;
256                 $ret = new CBTValue( '', array(), $this->mCompiling );
257
258                 $foundClosing = false;
259                 while ( $p < $end ) {
260                         $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
261                         $pToken = $p + $matchLength;
262
263                         if ( $pToken >= $end ) {
264                                 // No more braces, output remainder
265                                 $ret->cat( substr( $in, $p ) );
266                                 $p = $end;
267                                 break;
268                         }
269
270                         // Output the text before the brace
271                         $ret->cat( substr( $in, $p, $matchLength ) );
272
273                         // Advance the pointer
274                         $p = $pToken + 1;
275
276                         // Check for closing brace
277                         if ( $in[$pToken] == '}' ) {
278                                 $foundClosing = true;
279                                 break;
280                         }
281
282                         // Handle the "{fn}" special case
283                         if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
284                                 wfProfileOut( $fname );
285                                 $val = $this->doOpenFunction( $p, $end );
286                                 wfProfileIn( $fname );
287                                 if ( $p < $end && $in[$p] == '"' ) {
288                                         $val->setText( htmlspecialchars( $val->getText() ) );
289                                 }
290                                 $ret->cat( $val );
291                         } else {
292                                 // Process the function mode component
293                                 wfProfileOut( $fname );
294                                 $ret->cat( $this->doOpenFunction( $p, $end ) );
295                                 wfProfileIn( $fname );
296                         }
297                 }
298                 if ( $foundClosing && !$needClosing ) {
299                         $this->error( 'Errant closing brace', $p );
300                 } elseif ( !$foundClosing && $needClosing ) {
301                         $this->error( 'Unclosed text section', $start );
302                 }
303                 wfProfileOut( $fname );
304                 return $ret;
305         }
306
307         /**
308          * Recursive workhorse for function mode.
309          *
310          * Processes function mode starting from offset $p, until either $end is
311          * reached or a closing brace is found. If $needClosing is false, a
312          * closing brace will flag an error, if $needClosing is true, the lack
313          * of a closing brace will flag an error.
314          *
315          * The parameter $p is advanced to the position after the closing brace,
316          * or after the end. A CBTValue is returned.
317          *
318          * @private
319          */
320         function doOpenFunction( &$p, $end, $needClosing = true ) {
321                 $in =& $this->mText;
322                 $start = $p;
323                 $tokens = array();
324                 $unexecutedTokens = array();
325
326                 $foundClosing = false;
327                 while ( $p < $end ) {
328                         $char = $in[$p];
329                         if ( $char == '{' ) {
330                                 // Switch to text mode
331                                 ++$p;
332                                 $tokenStart = $p;
333                                 $token = $this->doOpenText( $p, $end );
334                                 $tokens[] = $token;
335                                 $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
336                         } elseif ( $char == '}' ) {
337                                 // Block end
338                                 ++$p;
339                                 $foundClosing = true;
340                                 break;
341                         } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
342                                 // Whitespace
343                                 // Consume the rest of the whitespace
344                                 $p += strspn( $in, CBT_WHITE, $p, $end - $p );
345                         } else {
346                                 // Token, find the end of it
347                                 $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
348                                 $token = new CBTValue( substr( $in, $p, $tokenLength ) );
349                                 // Execute the token as a function if it's not the function name
350                                 if ( count( $tokens ) ) {
351                                         $tokens[] = $this->doFunction( array( $token ), $p );
352                                 } else {
353                                         $tokens[] = $token;
354                                 }
355                                 $unexecutedTokens[] = $token->getText();
356
357                                 $p += $tokenLength;
358                         }
359                 }
360                 if ( !$foundClosing && $needClosing ) {
361                         $this->error( 'Unclosed function', $start );
362                         return '';
363                 }
364
365                 $val = $this->doFunction( $tokens, $start );
366                 if ( $this->mCompiling && !$val->isStatic() ) {
367                         $compiled = '';
368                         $first = true;
369                         foreach( $tokens as $i => $token ) {
370                                 if ( $first ) {
371                                         $first = false;
372                                 } else {
373                                         $compiled .= ' ';
374                                 }
375                                 if ( $token->isStatic() ) {
376                                         if ( $i !== 0 ) {
377                                                 $compiled .= '{' . $token->getText() . '}';
378                                         } else {
379                                                 $compiled .= $token->getText();
380                                         }
381                                 } else {
382                                         $compiled .= $unexecutedTokens[$i];
383                                 }
384                         }
385
386                         // The dynamic parts of the string are still represented as functions, and
387                         // function invocations have no dependencies. Thus the compiled result has
388                         // no dependencies.
389                         $val = new CBTValue( "{{$compiled}}", array(), true );
390                 }
391                 return $val;
392         }
393
394         /**
395          * Execute a function, caching and returning the result value.
396          * $tokens is an array of CBTValue objects. $tokens[0] is the function
397          * name, the others are arguments. $p is the string position, and is used
398          * for error messages only.
399          */
400         function doFunction( $tokens, $p ) {
401                 if ( count( $tokens ) == 0 ) {
402                         return new CBTValue;
403                 }
404                 $fname = 'CBTProcessor::doFunction';
405                 wfProfileIn( $fname );
406
407                 $ret = new CBTValue;
408
409                 // All functions implicitly depend on their arguments, and the function name
410                 // While this is not strictly necessary for all functions, it's true almost
411                 // all the time and so convenient to do automatically.
412                 $ret->addDeps( $tokens );
413
414                 $this->mCurrentPos = $p;
415                 $func = array_shift( $tokens );
416                 $func = $func->getText();
417
418                 // Extract the text component from all the tokens
419                 // And convert any templates to plain text
420                 $textArgs = array();
421                 foreach ( $tokens as $token ) {
422                         $token->execute( $this );
423                         $textArgs[] = $token->getText();
424                 }
425
426                 // Try the local cache
427                 $cacheKey = $func . "\n" . implode( "\n", $textArgs );
428                 if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
429                         $val = $this->mFunctionCache[$cacheKey];
430                 } elseif ( isset( $this->mBuiltins[$func] ) ) {
431                         $func = $this->mBuiltins[$func];
432                         $val = call_user_func_array( array( &$this, $func ), $tokens );
433                         $this->mFunctionCache[$cacheKey] = $val;
434                 } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
435                         $profName = get_class( $this->mFunctionObj ) . '::' . $func;
436                         wfProfileIn( "$fname-callback" );
437                         wfProfileIn( $profName );
438                         $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
439                         wfProfileOut( $profName );
440                         wfProfileOut( "$fname-callback" );
441                         $this->mFunctionCache[$cacheKey] = $val;
442                 } else {
443                         $this->error( "Call of undefined function \"$func\"", $p );
444                         $val = new CBTValue;
445                 }
446                 if ( !is_object( $val ) ) {
447                         $val = new CBTValue((string)$val);
448                 }
449
450                 if ( CBT_DEBUG ) {
451                         $unexpanded = $val;
452                 }
453
454                 // If the output was a template, execute it
455                 $val->execute( $this );
456
457                 if ( $this->mCompiling ) {
458                         // Escape any braces so that the output will be a valid template
459                         $val->templateEscape();
460                 }
461                 $val->removeDeps( $this->mIgnorableDeps );
462                 $ret->addDeps( $val );
463                 $ret->setText( $val->getText() );
464
465                 if ( CBT_DEBUG ) {
466                         wfDebug( "doFunction $func args = "
467                                 . var_export( $tokens, true )
468                                 . "unexpanded return = "
469                                 . var_export( $unexpanded, true )
470                                 . "expanded return = "
471                                 . var_export( $ret, true )
472                         );
473                 }
474
475                 wfProfileOut( $fname );
476                 return $ret;
477         }
478
479         /**
480          * Set a flag indicating that an error has been found.
481          */
482         function error( $text, $pos = false ) {
483                 $this->mLastError = $text;
484                 if ( $pos === false ) {
485                         $this->mErrorPos = $this->mCurrentPos;
486                 } else {
487                         $this->mErrorPos = $pos;
488                 }
489         }
490
491         function getLastError() {
492                 return $this->mLastError;
493         }
494
495         /** 'if' built-in function */
496         function bi_if( $condition, $trueBlock, $falseBlock = null ) {
497                 if ( is_null( $condition ) ) {
498                         $this->error( "Missing condition in if" );
499                         return '';
500                 }
501
502                 if ( $condition->getText() != '' ) {
503                         return new CBTValue( $trueBlock->getText(),
504                                 array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
505                                 $trueBlock->mIsTemplate );
506                 } else {
507                         if ( !is_null( $falseBlock ) ) {
508                                 return new CBTValue( $falseBlock->getText(),
509                                         array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
510                                         $falseBlock->mIsTemplate );
511                         } else {
512                                 return new CBTValue( '', $condition->getDeps() );
513                         }
514                 }
515         }
516
517         /** 'true' built-in function */
518         function bi_true() {
519                 return "true";
520         }
521
522         /** left brace built-in */
523         function bi_lbrace() {
524                 return '{';
525         }
526
527         /** right brace built-in */
528         function bi_rbrace() {
529                 return '}';
530         }
531
532         /**
533          * escape built-in.
534          * Escape text for inclusion in an HTML attribute
535          */
536         function bi_escape( $val ) {
537                 return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
538         }
539 }