Index: trunk/extensions/ParserFunctions/Expr.php |
— | — | @@ -9,42 +9,74 @@ |
10 | 10 | define( 'EXPR_NUMBER_CLASS', '0123456789.' );
|
11 | 11 |
|
12 | 12 | // Token types
|
13 | | -define( 'EXPR_WHITE', 'WHITE' );
|
14 | | -define( 'EXPR_NUMBER', 'NUMBER' );
|
15 | | -define( 'EXPR_NEGATIVE', 'NEGATIVE' );
|
16 | | -define( 'EXPR_POSITIVE', 'POSITIVE' );
|
17 | | -define( 'EXPR_PLUS', 'PLUS' );
|
18 | | -define( 'EXPR_MINUS', 'MINUS' );
|
19 | | -define( 'EXPR_TIMES', 'TIMES' );
|
20 | | -define( 'EXPR_DIVIDE', 'DIVIDE' );
|
21 | | -define( 'EXPR_MOD', 'MOD' );
|
22 | | -define( 'EXPR_OPEN', 'OPEN' );
|
23 | | -define( 'EXPR_CLOSE', 'CLOSE' );
|
24 | | -define( 'EXPR_AND', 'AND' );
|
25 | | -define( 'EXPR_OR', 'OR' );
|
26 | | -define( 'EXPR_NOT', 'NOT' );
|
27 | | -define( 'EXPR_EQUALITY', 'EQUALITY' );
|
28 | | -define( 'EXPR_ROUND', 'ROUND' );
|
| 13 | +define( 'EXPR_WHITE', 1 );
|
| 14 | +define( 'EXPR_NUMBER', 2 );
|
| 15 | +define( 'EXPR_NEGATIVE', 3 );
|
| 16 | +define( 'EXPR_POSITIVE', 4 );
|
| 17 | +define( 'EXPR_PLUS', 5 );
|
| 18 | +define( 'EXPR_MINUS', 6 );
|
| 19 | +define( 'EXPR_TIMES', 7 );
|
| 20 | +define( 'EXPR_DIVIDE', 8 );
|
| 21 | +define( 'EXPR_MOD', 9 );
|
| 22 | +define( 'EXPR_OPEN', 10 );
|
| 23 | +define( 'EXPR_CLOSE', 11 );
|
| 24 | +define( 'EXPR_AND', 12 );
|
| 25 | +define( 'EXPR_OR', 13 );
|
| 26 | +define( 'EXPR_NOT', 14 );
|
| 27 | +define( 'EXPR_EQUALITY', 15 );
|
| 28 | +define( 'EXPR_LESS', 16 );
|
| 29 | +define( 'EXPR_GREATER', 17 );
|
| 30 | +define( 'EXPR_LESSEQ', 18 );
|
| 31 | +define( 'EXPR_GREATEREQ', 19 );
|
| 32 | +define( 'EXPR_NOTEQ', 20 );
|
| 33 | +define( 'EXPR_ROUND', 21 );
|
29 | 34 |
|
30 | 35 | class ExprParser {
|
31 | | - var $expr, $end;
|
| 36 | + var $maxStackSize = 1000;
|
32 | 37 |
|
33 | 38 | var $precedence = array(
|
34 | | - EXPR_NEGATIVE => 9,
|
35 | | - EXPR_POSITIVE => 9,
|
36 | | - EXPR_TIMES => 8,
|
37 | | - EXPR_DIVIDE => 8,
|
38 | | - EXPR_MOD => 8,
|
39 | | - EXPR_PLUS => 6,
|
40 | | - EXPR_MINUS => 6,
|
41 | | - EXPR_ROUND => 5,
|
42 | | - EXPR_EQUALITY => 4,
|
43 | | - EXPR_AND => 3,
|
44 | | - EXPR_OR => 2,
|
45 | | - EXPR_OPEN => -1,
|
46 | | - EXPR_CLOSE => -1
|
47 | | - );
|
| 39 | + EXPR_NEGATIVE => 9,
|
| 40 | + EXPR_POSITIVE => 9,
|
| 41 | + EXPR_NOT => 9,
|
| 42 | + EXPR_TIMES => 8,
|
| 43 | + EXPR_DIVIDE => 8,
|
| 44 | + EXPR_MOD => 8,
|
| 45 | + EXPR_PLUS => 6,
|
| 46 | + EXPR_MINUS => 6,
|
| 47 | + EXPR_ROUND => 5,
|
| 48 | + EXPR_EQUALITY => 4,
|
| 49 | + EXPR_LESS => 4,
|
| 50 | + EXPR_GREATER => 4,
|
| 51 | + EXPR_LESSEQ => 4,
|
| 52 | + EXPR_GREATEREQ => 4,
|
| 53 | + EXPR_NOTEQ => 4,
|
| 54 | + EXPR_AND => 3,
|
| 55 | + EXPR_OR => 2,
|
| 56 | + EXPR_OPEN => -1,
|
| 57 | + EXPR_CLOSE => -1
|
| 58 | + );
|
48 | 59 |
|
| 60 | + var $names = array(
|
| 61 | + EXPR_NEGATIVE => '-',
|
| 62 | + EXPR_POSITIVE => '+',
|
| 63 | + EXPR_NOT => 'not',
|
| 64 | + EXPR_TIMES => '*',
|
| 65 | + EXPR_DIVIDE => '/',
|
| 66 | + EXPR_MOD => 'mod',
|
| 67 | + EXPR_PLUS => '+',
|
| 68 | + EXPR_MINUS => '-',
|
| 69 | + EXPR_ROUND => 'round',
|
| 70 | + EXPR_EQUALITY => '=',
|
| 71 | + EXPR_LESS => '<',
|
| 72 | + EXPR_GREATER => '>',
|
| 73 | + EXPR_LESSEQ => '<=',
|
| 74 | + EXPR_GREATEREQ => '>=',
|
| 75 | + EXPR_NOTEQ => '<>',
|
| 76 | + EXPR_AND => 'and',
|
| 77 | + EXPR_OR => 'or',
|
| 78 | + );
|
| 79 | +
|
| 80 | +
|
49 | 81 | var $words = array(
|
50 | 82 | 'mod' => EXPR_MOD,
|
51 | 83 | 'and' => EXPR_AND,
|
— | — | @@ -53,10 +85,37 @@ |
54 | 86 | 'round' => EXPR_ROUND,
|
55 | 87 | );
|
56 | 88 |
|
57 | | - function error( $msg ) {
|
58 | | - $this->lastError = $msg;
|
| 89 | +
|
| 90 | + /**
|
| 91 | + * Add expression messages to the message cache
|
| 92 | + * @static
|
| 93 | + */
|
| 94 | + function addMessages() {
|
| 95 | + global $wgMessageCache;
|
| 96 | + $wgMessageCache->addMessages( array(
|
| 97 | + 'expr_stack_exhausted' => 'Expression error: stack exhausted',
|
| 98 | + 'expr_unexpected_number' => 'Expression error: unexpected number',
|
| 99 | + 'expr_preg_match_failure' => 'Expression error: unexpected preg_match failure',
|
| 100 | + 'expr_unrecognised_word' => 'Expression error: unrecognised word "$1"',
|
| 101 | + 'expr_unexpected_operator' => 'Expression error: unexpected $1 operator',
|
| 102 | + 'expr_missing_operand' => 'Expression error: Missing operand for $1',
|
| 103 | + 'expr_unexpected_closing_bracket' => 'Expression error: unexpected closing bracket',
|
| 104 | + 'expr_unrecognised_punctuation' => 'Expression error: unrecognised punctuation character "$1"',
|
| 105 | + 'expr_unclosed_bracket' => 'Expression error: unclosed bracket',
|
| 106 | + ));
|
59 | 107 | }
|
| 108 | +
|
60 | 109 |
|
| 110 | + function error( $msg, $parameter = false ) {
|
| 111 | + $this->lastErrorKey = $msg;
|
| 112 | + $this->lastErrorParameter = $parameter;
|
| 113 | + if ( $parameter === false ) {
|
| 114 | + $this->lastErrorMessage = wfMsg( "expr_$msg" );
|
| 115 | + } else {
|
| 116 | + $this->lastErrorMessage = wfMsg( "expr_$msg", htmlspecialchars( $parameter ) );
|
| 117 | + }
|
| 118 | + }
|
| 119 | +
|
61 | 120 | /**
|
62 | 121 | * Evaluate a mathematical expression
|
63 | 122 | *
|
— | — | @@ -67,16 +126,27 @@ |
68 | 127 | function doExpression( $expr ) {
|
69 | 128 | $operands = array();
|
70 | 129 | $operators = array();
|
| 130 | +
|
| 131 | + # Unescape inequality operators
|
| 132 | + $expr = strtr( $expr, array( '<' => '<', '>' => '>' ) );
|
| 133 | +
|
71 | 134 | $p = 0;
|
72 | 135 | $end = strlen( $expr );
|
73 | 136 | $expecting = 'expression';
|
74 | 137 |
|
| 138 | +
|
75 | 139 | while ( $p < $end ) {
|
| 140 | + if ( count( $operands ) > $this->maxStackSize || count( $operands ) > $this->maxStackSize ) {
|
| 141 | + $this->error( 'stack_exhausted' );
|
| 142 | + }
|
76 | 143 | $char = $expr[$p];
|
77 | | -
|
| 144 | + $char2 = substr( $expr, $p, 2 );
|
| 145 | +
|
78 | 146 | // Mega if-elseif-else construct
|
79 | 147 | // Only binary operators fall through for processing at the bottom, the rest
|
80 | 148 | // finish their processing and continue
|
| 149 | +
|
| 150 | + // First the unlimited length classes
|
81 | 151 |
|
82 | 152 | if ( false !== strpos( EXPR_WHITE_CLASS, $char ) ) {
|
83 | 153 | // Whitespace
|
— | — | @@ -85,7 +155,7 @@ |
86 | 156 | } elseif ( false !== strpos( EXPR_NUMBER_CLASS, $char ) ) {
|
87 | 157 | // Number
|
88 | 158 | if ( $expecting != 'expression' ) {
|
89 | | - $this->error( 'Unexpected number' );
|
| 159 | + $this->error( 'unexpected_number' );
|
90 | 160 | return false;
|
91 | 161 | }
|
92 | 162 |
|
— | — | @@ -102,7 +172,7 @@ |
103 | 173 | $remaining = substr( $expr, $p );
|
104 | 174 | if ( !preg_match( '/^[A-Za-z]*/', $remaining, $matches ) ) {
|
105 | 175 | // This should be unreachable
|
106 | | - $this->error( 'Unexpected preg_match failure' );
|
| 176 | + $this->error( 'preg_match_failure' );
|
107 | 177 | return false;
|
108 | 178 | }
|
109 | 179 | $word = strtolower( $matches[0] );
|
— | — | @@ -110,26 +180,42 @@ |
111 | 181 |
|
112 | 182 | // Interpret the word
|
113 | 183 | if ( !isset( $this->words[$word] ) ){
|
114 | | - $this->error( 'Unrecognised word' );
|
| 184 | + $this->error( 'unrecognised_word', $word );
|
115 | 185 | return false;
|
116 | 186 | }
|
117 | 187 | $op = $this->words[$word];
|
118 | 188 | if ( $op == EXPR_NOT ) {
|
119 | 189 | // Unary operator
|
120 | 190 | if ( $expecting != 'expression' ) {
|
121 | | - $this->error( "Unexpected $op operator" );
|
| 191 | + $this->error( 'unexpected_operator', $word );
|
122 | 192 | return false;
|
123 | 193 | }
|
124 | 194 | $operators[] = $op;
|
125 | 195 | continue;
|
126 | | - } else {
|
127 | | - // Binary operator
|
128 | | - if ( $expecting == 'expression' ) {
|
129 | | - $this->error( "Unexpected $op operator" );
|
130 | | - return false;
|
131 | | - }
|
132 | 196 | }
|
133 | | - } elseif ( $char == '+' ) {
|
| 197 | + // Binary operator, fall through
|
| 198 | + $name = $word;
|
| 199 | + }
|
| 200 | +
|
| 201 | + // Next the two-character operators
|
| 202 | +
|
| 203 | + elseif ( $char2 == '<=' ) {
|
| 204 | + $name = $char2;
|
| 205 | + $op = EXPR_LESSEQ;
|
| 206 | + $p += 2;
|
| 207 | + } elseif ( $char2 == '>=' ) {
|
| 208 | + $name = $char2;
|
| 209 | + $op = EXPR_GREATEREQ;
|
| 210 | + $p += 2;
|
| 211 | + } elseif ( $char2 == '<>' || $char2 == '!=' ) {
|
| 212 | + $name = $char2;
|
| 213 | + $op = EXPR_NOTEQ;
|
| 214 | + $p += 2;
|
| 215 | + }
|
| 216 | +
|
| 217 | + // Finally the single-character operators
|
| 218 | +
|
| 219 | + elseif ( $char == '+' ) {
|
134 | 220 | ++$p;
|
135 | 221 | if ( $expecting == 'expression' ) {
|
136 | 222 | // Unary plus
|
— | — | @@ -150,24 +236,18 @@ |
151 | 237 | $op = EXPR_MINUS;
|
152 | 238 | }
|
153 | 239 | } elseif ( $char == '*' ) {
|
154 | | - if ( $expecting == 'expression' ) {
|
155 | | - $this->error( 'Unexpected * operator' );
|
156 | | - return false;
|
157 | | - }
|
| 240 | + $name = $char;
|
158 | 241 | $op = EXPR_TIMES;
|
159 | 242 | ++$p;
|
160 | 243 | } elseif ( $char == '/' ) {
|
161 | | - if ( $expecting == 'expression' ) {
|
162 | | - $this->error( 'Unexpected / operator' );
|
163 | | - return false;
|
164 | | - }
|
| 244 | + $name = $char;
|
165 | 245 | $op = EXPR_DIVIDE;
|
166 | 246 | ++$p;
|
167 | 247 | } elseif ( $char == '(' ) {
|
168 | 248 | if ( $expecting == 'operator' ) {
|
169 | | - $this->error( 'Unexpected opening bracket' );
|
| 249 | + $this->error( 'unexpected_operator', '(' );
|
170 | 250 | return false;
|
171 | | - }
|
| 251 | + }
|
172 | 252 | $operators[] = EXPR_OPEN;
|
173 | 253 | ++$p;
|
174 | 254 | continue;
|
— | — | @@ -175,7 +255,7 @@ |
176 | 256 | $lastOp = end( $operators );
|
177 | 257 | while ( $lastOp && $lastOp != EXPR_OPEN ) {
|
178 | 258 | if ( !$this->doOperation( $lastOp, $operands ) ) {
|
179 | | - $this->error( "Missing operand for $lastOp" );
|
| 259 | + $this->error( 'missing_operand', $this->names[$lastOp] );
|
180 | 260 | return false;
|
181 | 261 | }
|
182 | 262 | array_pop( $operators );
|
— | — | @@ -184,29 +264,40 @@ |
185 | 265 | if ( $lastOp ) {
|
186 | 266 | array_pop( $operators );
|
187 | 267 | } else {
|
188 | | - $this->error( "Unexpected closing bracket" );
|
| 268 | + $this->error( "unexpected_closing_bracket" );
|
189 | 269 | return false;
|
190 | 270 | }
|
191 | 271 | $expecting = 'operator';
|
192 | 272 | ++$p;
|
193 | 273 | continue;
|
194 | | - } elseif ( $char = '=' ) {
|
195 | | - if ( $expecting == 'expression' ) {
|
196 | | - $this->error( 'Unexpected = operator' );
|
197 | | - return false;
|
198 | | - }
|
| 274 | + } elseif ( $char == '=' ) {
|
| 275 | + $name = $char;
|
199 | 276 | $op = EXPR_EQUALITY;
|
200 | 277 | ++$p;
|
| 278 | + } elseif ( $char == '<' ) {
|
| 279 | + $name = $char;
|
| 280 | + $op = EXPR_LESS;
|
| 281 | + ++$p;
|
| 282 | + } elseif ( $char == '>' ) {
|
| 283 | + $name = $char;
|
| 284 | + $op = EXPR_GREATER;
|
| 285 | + ++$p;
|
201 | 286 | } else {
|
202 | | - $this->error( "Unrecognised punctuation character" );
|
| 287 | + $this->error( 'unrecognised_punctuation', $char );
|
203 | 288 | return false;
|
204 | 289 | }
|
205 | 290 |
|
206 | | - // Shunting yard magic for binary operators
|
| 291 | + // Binary operator processing
|
| 292 | + if ( $expecting == 'expression' ) {
|
| 293 | + $this->error( 'unexpected_operator', $name );
|
| 294 | + return false;
|
| 295 | + }
|
| 296 | +
|
| 297 | + // Shunting yard magic
|
207 | 298 | $lastOp = end( $operators );
|
208 | 299 | while ( $lastOp && $this->precedence[$op] <= $this->precedence[$lastOp] ) {
|
209 | 300 | if ( !$this->doOperation( $lastOp, $operands ) ) {
|
210 | | - $this->error( "Missing operand for $lastOp" );
|
| 301 | + $this->error( 'missing_operand', $this->names[$lastOp] );
|
211 | 302 | return false;
|
212 | 303 | }
|
213 | 304 | array_pop( $operators );
|
— | — | @@ -219,11 +310,11 @@ |
220 | 311 | // Finish off the operator array
|
221 | 312 | while ( $op = array_pop( $operators ) ) {
|
222 | 313 | if ( $op == EXPR_OPEN ) {
|
223 | | - $this->error( "Unclosed bracket" );
|
| 314 | + $this->error( 'unclosed_bracket' );
|
224 | 315 | return false;
|
225 | 316 | }
|
226 | 317 | if ( !$this->doOperation( $op, $operands ) ) {
|
227 | | - $this->error( "Missing operand for $lastOp" );
|
| 318 | + $this->error( 'missing_operand', $this->names[$op] );
|
228 | 319 | return false;
|
229 | 320 | }
|
230 | 321 | }
|
— | — | @@ -300,7 +391,36 @@ |
301 | 392 | $value = array_pop( $stack );
|
302 | 393 | $stack[] = round( $value, $digits );
|
303 | 394 | break;
|
304 | | -
|
| 395 | + case EXPR_LESS:
|
| 396 | + if ( count( $stack ) < 2 ) return false;
|
| 397 | + $right = array_pop( $stack );
|
| 398 | + $left = array_pop( $stack );
|
| 399 | + $stack[] = ( $left < $right ) ? 1 : 0;
|
| 400 | + break;
|
| 401 | + case EXPR_GREATER:
|
| 402 | + if ( count( $stack ) < 2 ) return false;
|
| 403 | + $right = array_pop( $stack );
|
| 404 | + $left = array_pop( $stack );
|
| 405 | + $stack[] = ( $left > $right ) ? 1 : 0;
|
| 406 | + break;
|
| 407 | + case EXPR_LESSEQ:
|
| 408 | + if ( count( $stack ) < 2 ) return false;
|
| 409 | + $right = array_pop( $stack );
|
| 410 | + $left = array_pop( $stack );
|
| 411 | + $stack[] = ( $left <= $right ) ? 1 : 0;
|
| 412 | + break;
|
| 413 | + case EXPR_GREATEREQ:
|
| 414 | + if ( count( $stack ) < 2 ) return false;
|
| 415 | + $right = array_pop( $stack );
|
| 416 | + $left = array_pop( $stack );
|
| 417 | + $stack[] = ( $left >= $right ) ? 1 : 0;
|
| 418 | + break;
|
| 419 | + case EXPR_NOTEQ:
|
| 420 | + if ( count( $stack ) < 2 ) return false;
|
| 421 | + $right = array_pop( $stack );
|
| 422 | + $left = array_pop( $stack );
|
| 423 | + $stack[] = ( $left != $right ) ? 1 : 0;
|
| 424 | + break;
|
305 | 425 | }
|
306 | 426 | return true;
|
307 | 427 | }
|
Index: trunk/extensions/ParserFunctions/ParserFunctions.php |
— | — | @@ -9,16 +9,17 @@ |
10 | 10 | class ExtParserFunctions {
|
11 | 11 | var $mExprParser;
|
12 | 12 |
|
13 | | - function exprHook( &$parser, $expr = '' ) {
|
| 13 | + function expr( &$parser, $expr = '' ) {
|
14 | 14 | if ( !isset( $this->mExpr ) ) {
|
15 | 15 | if ( !class_exists( 'ExprParser' ) ) {
|
16 | | - require_once( dirname( __FILE__ ) . '/Expr.php' );
|
| 16 | + require( dirname( __FILE__ ) . '/Expr.php' );
|
| 17 | + ExprParser::addMessages();
|
17 | 18 | }
|
18 | 19 | $this->mExprParser = new ExprParser;
|
19 | 20 | }
|
20 | 21 | $result = $this->mExprParser->doExpression( $expr );
|
21 | 22 | if ( $result === false ) {
|
22 | | - return wfMsg( 'expr_parse_error' );
|
| 23 | + return $this->mExprParser->lastErrorMessage;
|
23 | 24 | } else {
|
24 | 25 | return $result;
|
25 | 26 | }
|
— | — | @@ -32,13 +33,17 @@ |
33 | 34 | }
|
34 | 35 | }
|
35 | 36 |
|
36 | | - function ifeqHook( &$parser, $left = '', $right = '', $then = '', $else = '' ) {
|
| 37 | + function ifeq( &$parser, $left = '', $right = '', $then = '', $else = '' ) {
|
37 | 38 | if ( trim( $left ) == trim( $right ) ) {
|
38 | 39 | return $then;
|
39 | 40 | } else {
|
40 | 41 | return $else;
|
41 | 42 | }
|
42 | 43 | }
|
| 44 | +
|
| 45 | + function rand( &$parser, $min = 1, $max = 100 ) {
|
| 46 | + return mt_rand( $min, $max );
|
| 47 | + }
|
43 | 48 | }
|
44 | 49 |
|
45 | 50 | function wfSetupParserFunctions() {
|
— | — | @@ -46,11 +51,10 @@ |
47 | 52 |
|
48 | 53 | $wgExtParserFunctions = new ExtParserFunctions;
|
49 | 54 |
|
50 | | - $wgParser->setFunctionHook( 'expr', array( &$wgExtParserFunctions, 'exprHook' ) );
|
51 | | - $wgParser->setFunctionHook( 'if', array( &$wgExtParserFunctions, 'ifHook' ) ) ;
|
52 | | - $wgParser->setFunctionHook( 'ifeq', array( &$wgExtParserFunctions, 'ifeqHook' ) ) ;
|
53 | | -
|
54 | | - $wgMessageCache->addMessage( 'expr_parse_error', 'Invalid expression' );
|
| 55 | + $wgParser->setFunctionHook( 'expr', array( &$wgExtParserFunctions, 'expr' ) );
|
| 56 | + $wgParser->setFunctionHook( 'if', array( &$wgExtParserFunctions, 'ifHook' ) );
|
| 57 | + $wgParser->setFunctionHook( 'ifeq', array( &$wgExtParserFunctions, 'ifeq' ) );
|
| 58 | + $wgParser->setFunctionHook( 'rand', array( &$wgExtParserFunctions, 'rand' ) );
|
55 | 59 | }
|
56 | 60 |
|
57 | 61 | ?>
|