Index: trunk/extensions/InlineScripts/interpreter/ParserShuntingYard.php |
— | — | @@ -38,8 +38,8 @@ |
39 | 39 | |
40 | 40 | // Comments |
41 | 41 | if ( substr($this->mCode, $this->mPos, 2) == '/*' ) { |
42 | | - $end = strpos( $this->mCode, '*/', $this->mPos ); |
43 | | - return self::nextToken( $this->mCode, $end + 2 ); |
| 42 | + $this->mPos = strpos( $this->mCode, '*/', $this->mPos ) + 2; |
| 43 | + return self::nextToken(); |
44 | 44 | } |
45 | 45 | |
46 | 46 | // Braces |
— | — | @@ -222,7 +222,7 @@ |
223 | 223 | if( $type != ISToken::TNone ) { |
224 | 224 | $this->mTokensCount++; |
225 | 225 | if( !$this->mInterpreter->increaseTokensCount() ) |
226 | | - throw new ISUserVisibleException( 'toomanytokens', $ast->getPos() ); |
| 226 | + throw new ISUserVisibleException( 'toomanytokens', $this->mPos ); |
227 | 227 | } |
228 | 228 | |
229 | 229 | $token = new ISToken( $type, $val, $this->mPos ); |
— | — | @@ -295,8 +295,21 @@ |
296 | 296 | break; |
297 | 297 | } |
298 | 298 | } |
299 | | - array_push( $opStack, $op1 ); |
300 | | - $expecting = self::ExpectingData; |
| 299 | + |
| 300 | + if( $this->mCur->isOp( 'catch' ) ) { |
| 301 | + $this->move(); |
| 302 | + if( $this->mCur->type != ISToken::TID ) |
| 303 | + throw new ISUserVisibleException( 'cantchangeconst', $pos ); |
| 304 | + $op1->setData( $this->mCur->value ); |
| 305 | + } |
| 306 | + |
| 307 | + if( $op1->getArgsNumber() ) { |
| 308 | + array_push( $opStack, $op1 ); |
| 309 | + $expecting = self::ExpectingData; |
| 310 | + } else { |
| 311 | + $outputQueue[] = $op1; |
| 312 | + $expecting = self::ExpectingOperator; |
| 313 | + } |
301 | 314 | } |
302 | 315 | |
303 | 316 | /* Functions */ |
— | — | @@ -316,7 +329,7 @@ |
317 | 330 | $this->pushOp( $outputQueue, $opStack ); |
318 | 331 | } else { |
319 | 332 | $outputQueue[] = new ISDataNode( |
320 | | - new ISData( ISData::DList, array() ) ); |
| 333 | + new ISData( ISData::DList, array() ), $this->mPos ); |
321 | 334 | } |
322 | 335 | $expecting = self::ExpectingOperator; |
323 | 336 | continue; |
Index: trunk/extensions/InlineScripts/interpreter/Utils.php |
— | — | @@ -60,6 +60,12 @@ |
61 | 61 | const OElse = 'else'; |
62 | 62 | const ODo = 'do'; |
63 | 63 | const OForeach = 'foreach'; |
| 64 | + const OTry = 'try'; |
| 65 | + const OCatch = 'catch'; |
| 66 | + const OBreak = 'break'; |
| 67 | + const OContinue = 'continue'; |
| 68 | + const OIsset = 'isset'; |
| 69 | + const OUnset = 'unset'; |
64 | 70 | const OIn = 'in'; |
65 | 71 | const OInvert = '!'; |
66 | 72 | const OPow = '**'; |
— | — | @@ -82,13 +88,17 @@ |
83 | 89 | const OTrinary = '?'; |
84 | 90 | const OColon = ':'; |
85 | 91 | const OSet = '='; |
| 92 | + const OSetAdd = '+='; |
| 93 | + const OSetSub = '-='; |
| 94 | + const OSetMul = '*='; |
| 95 | + const OSetDiv = '/='; |
86 | 96 | const OComma = ','; |
87 | 97 | const OStatementSeperator = ';'; |
88 | 98 | const OLeftBrace = '('; |
89 | 99 | const OLeftCurly = '{'; |
90 | 100 | |
91 | 101 | static $precedence = array( |
92 | | - self::OFunction => 20, |
| 102 | + self::OFunction => 20, self::OIsset => 20, self::OUnset => 20, |
93 | 103 | self::OArrayElement => 19, self::OPositive => 19, self::ONegative => 19, |
94 | 104 | self::OIn => 18, |
95 | 105 | self::OInvert => 17, self::OPow => 17, |
— | — | @@ -98,27 +108,30 @@ |
99 | 109 | self::ONotEqualsToStrict => 13, self::OGreater => 13, self::OLess => 13, |
100 | 110 | self::OGreaterOrEq => 13, self::OLessOrEq => 13, |
101 | 111 | self::OAnd => 12, self::OOr => 12, self::OXor => 12, |
102 | | - self::OColon => 11, self::OTrinary => 10, self::OSet => 9, |
| 112 | + self::OColon => 11, self::OTrinary => 10, |
| 113 | + self::OSet => 9, self::OSetAdd => 9, self::OSetSub => 9, |
| 114 | + self::OSetMul => 9, self::OSetDiv => 9, |
103 | 115 | self::OIf => 6, self::OThen => 7, self::OElse => 8, |
104 | 116 | self::OForeach => 6, self::ODo => 7, |
| 117 | + self::OTry => 6, self::OCatch => 7, |
105 | 118 | self::OComma => 8, self::OStatementSeperator => 0, |
106 | 119 | ); |
107 | 120 | |
108 | 121 | static $unaryOperators = array( |
109 | | - self::OPositive, self::ONegative, self::OFunction, self::OArray, |
| 122 | + self::OPositive, self::ONegative, self::OFunction, self::OArray, self::OTry, |
110 | 123 | self::OIf, self::OForeach, self::OInvert, self::OArrayElementSingle, |
| 124 | + self::OIsset, self::OUnset |
111 | 125 | ); |
112 | 126 | |
113 | 127 | static function parseOperator( $op, $expecting, $pos ) { |
114 | 128 | if( $expecting == ISCodeParserShuntingYard::ExpectingData ) { |
| 129 | + $ops = array( '(', '{', 'if', '!', 'try', 'break', 'continue', |
| 130 | + 'isset', 'unset' ); |
115 | 131 | if( $op == '+' ) return new self( self::OPositive, $pos ); |
116 | 132 | if( $op == '-' ) return new self( self::ONegative, $pos ); |
117 | | - if( $op == self::OLeftBrace || |
118 | | - $op == self::OLeftCurly || |
119 | | - $op == self::OIf || |
120 | | - $op == self::OInvert ) |
| 133 | + if( $op == '[' ) return new self( self::OArray, $pos ); |
| 134 | + if( in_array( $op, $ops ) ) |
121 | 135 | return new self( $op, $pos ); |
122 | | - if( $op == '[' ) return new self( self::OArray, $pos ); |
123 | 136 | return null; |
124 | 137 | } else { |
125 | 138 | if( $op == '+' ) return new self( self::OSum, $pos ); |
— | — | @@ -155,6 +168,9 @@ |
156 | 169 | return $this->mNumArgs; |
157 | 170 | if( in_array( $this->mOperator, self::$unaryOperators ) ) |
158 | 171 | return 1; |
| 172 | + elseif( $this->mOperator == self::OBreak || |
| 173 | + $this->mOperator == self::OContinue ) |
| 174 | + return 0; |
159 | 175 | else |
160 | 176 | return 2; |
161 | 177 | } |
— | — | @@ -163,6 +179,10 @@ |
164 | 180 | $this->mNumArgs = $num; |
165 | 181 | } |
166 | 182 | |
| 183 | + public function setData( $data ) { |
| 184 | + $this->mData = $data; |
| 185 | + } |
| 186 | + |
167 | 187 | public function isRightAssociative() { |
168 | 188 | return $this->mOperator == self::OPow || |
169 | 189 | $this->mOperator == self::OSet; |
— | — | @@ -224,16 +244,16 @@ |
225 | 245 | |
226 | 246 | class ISData { |
227 | 247 | // Data types |
228 | | - const DInt = 'int'; |
| 248 | + const DInt = 'int'; |
229 | 249 | const DString = 'string'; |
230 | 250 | const DNull = 'null'; |
231 | 251 | const DBool = 'bool'; |
232 | 252 | const DFloat = 'float'; |
233 | 253 | const DList = 'list'; |
234 | | - |
| 254 | + |
235 | 255 | var $type; |
236 | 256 | var $data; |
237 | | - |
| 257 | + |
238 | 258 | public function __construct( $type = self::DNull, $val = null ) { |
239 | 259 | $this->type = $type; |
240 | 260 | $this->data = $val; |
— | — | @@ -350,7 +370,7 @@ |
351 | 371 | |
352 | 372 | public static function equals( $d1, $d2 ) { |
353 | 373 | return $d1->type != self::DList && $d2->type != self::DList && |
354 | | - $d1->toString() === $d2->toString(); |
| 374 | + $d1->data == $d2->data; |
355 | 375 | } |
356 | 376 | |
357 | 377 | public static function unaryMinus( $data ) { |
— | — | @@ -436,6 +456,37 @@ |
437 | 457 | return new ISData( self::DFloat, $a->toFloat() - $b->toFloat() ); |
438 | 458 | } |
439 | 459 | |
| 460 | + public function setValueByIndices( $val, $indices ) { |
| 461 | + if( $this->type == self::DNull && $indices[0] === null ) { |
| 462 | + $this->type = self::DList; |
| 463 | + $this->value = array(); |
| 464 | + $this->setValueByIndices( $val, $indices ); |
| 465 | + } elseif( $this->type == self::DList ) { |
| 466 | + if( $indices[0] === null ) { |
| 467 | + $this->data[] = $val; |
| 468 | + } else { |
| 469 | + $idx = $indices[0]->toInt(); |
| 470 | + if( $idx < 0 || $idx >= count( $this->data ) ) |
| 471 | + throw new ISUserVisibleException( 'outofbounds', 0, array( count( $this->data ), $index ) ); |
| 472 | + if( count( $indices ) > 1 ) |
| 473 | + $this->data[$idx]->setValueByIndices( $val, array_slice( $indices, 1 ) ); |
| 474 | + else |
| 475 | + $this->data[$idx] = $val; |
| 476 | + } |
| 477 | + } |
| 478 | + } |
| 479 | + |
| 480 | + public function checkIssetByIndices( $indices ) { |
| 481 | + if( $indices ) { |
| 482 | + $idx = array_shift( $indices ); |
| 483 | + if( $this->type != self::DList || $idx >= count( $this->data ) ) |
| 484 | + return false; |
| 485 | + return $this->checkIssetByIndices( $indices ); |
| 486 | + } else { |
| 487 | + return true; |
| 488 | + } |
| 489 | + } |
| 490 | + |
440 | 491 | /** Convert shorteners */ |
441 | 492 | public function toBool() { |
442 | 493 | return self::castTypes( $this, self::DBool )->data; |
— | — | @@ -476,9 +527,10 @@ |
477 | 528 | } |
478 | 529 | |
479 | 530 | public function appendTokenCount( &$interpr ) { |
| 531 | + global $wgInlineScriptsParserParams; |
480 | 532 | $interpr->mParser->is_tokensCount += $this->mTokensCount; |
481 | | - if( $interpr->mParser->is_tokensCount > $interpr->mLimits['tokens'] ) |
482 | | - throw new ISUserVisibleException( 'toomanytokens', $ast->getPos() ); |
| 533 | + if( $interpr->mParser->is_tokensCount > $wgInlineScriptsParserParams['limits']['tokens'] ) |
| 534 | + throw new ISUserVisibleException( 'toomanytokens', 0 ); |
483 | 535 | } |
484 | 536 | } |
485 | 537 | |
— | — | @@ -495,4 +547,8 @@ |
496 | 548 | $this->mPosition = $position; |
497 | 549 | $this->mParams = $params; |
498 | 550 | } |
| 551 | + |
| 552 | + public function getExceptionID() { |
| 553 | + return $this->mExceptionID; |
| 554 | + } |
499 | 555 | } |
Index: trunk/extensions/InlineScripts/interpreter/Interpreter.php |
— | — | @@ -14,15 +14,40 @@ |
15 | 15 | */ |
16 | 16 | const ParserVersion = 1; |
17 | 17 | |
18 | | - var $mVars, $mOut, $mParser, $mFrame, $mCodeParser, $mLimits; |
| 18 | + var $mVars, $mOut, $mParser, $mFrame, $mCodeParser; |
19 | 19 | |
20 | 20 | // length,lcase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count |
21 | 21 | static $mFunctions = array( |
| 22 | + 'out' => 'funcOut', |
| 23 | + |
| 24 | + /* String functions */ |
22 | 25 | 'lc' => 'funcLc', |
23 | | - 'out' => 'funcOut', |
| 26 | + 'uc' => 'funcUc', |
| 27 | + 'ucfirst' => 'funcUcFirst', |
| 28 | + 'urlencode' => 'funcUrlencode', |
| 29 | + 'grammar' => 'funcGrammar', |
| 30 | + 'plural' => 'funcPlural', |
| 31 | + 'anchorencode' => 'funcAnchorEncode', |
| 32 | + 'strlen' => 'funcStrlen', |
| 33 | + 'substr' => 'funcSubstr', |
| 34 | + 'strreplace' => 'funcStrreplace', |
| 35 | + 'split' => 'funcSplit', |
| 36 | + |
| 37 | + /* Array functions */ |
| 38 | + 'join' => 'funcJoin', |
| 39 | + 'count' => 'funcCount', |
| 40 | + |
| 41 | + /* Parser interaction functions */ |
24 | 42 | 'arg' => 'funcArg', |
25 | 43 | 'args' => 'funcArgs', |
26 | 44 | 'istranscluded' => 'funcIsTranscluded', |
| 45 | + 'parse' => 'funcParse', |
| 46 | + |
| 47 | + /* Cast functions */ |
| 48 | + 'string' => 'castString', |
| 49 | + 'int' => 'castInt', |
| 50 | + 'float' => 'castFloat', |
| 51 | + 'bool' => 'castBool', |
27 | 52 | ); |
28 | 53 | |
29 | 54 | // Order is important. The punctuation-matching regex requires that |
— | — | @@ -30,6 +55,8 @@ |
31 | 56 | // such errors. |
32 | 57 | static $mOps = array( |
33 | 58 | '!==', '!=', '!', // Inequality |
| 59 | + '+=', '-=', // Setting 1 |
| 60 | + '*=', '/=', // Setting 2 |
34 | 61 | '**', '*', // Multiplication/exponentiation |
35 | 62 | '/', '+', '-', '%', // Other arithmetic |
36 | 63 | '&', '|', '^', // Logic |
— | — | @@ -41,41 +68,45 @@ |
42 | 69 | '(', '[', '{', // Braces |
43 | 70 | ); |
44 | 71 | static $mKeywords = array( |
45 | | - 'in', 'true', 'false', 'null', 'contains', 'matches', |
46 | | - 'if', 'then', 'else', 'foreach', 'do', |
| 72 | + 'in', 'true', 'false', 'null', 'contains', 'break', |
| 73 | + 'if', 'then', 'else', 'foreach', 'do', 'try', 'catch', |
| 74 | + 'continue', 'isset', 'unset', |
47 | 75 | ); |
48 | 76 | |
49 | | - public function __construct( $params ) { |
| 77 | + public function __construct() { |
| 78 | + global $wgInlineScriptsParserParams; |
50 | 79 | $this->resetState(); |
51 | | - $this->mCodeParser = new $params['parserClass']( $this ); |
52 | | - $this->mLimits = $params['limits']; |
| 80 | + $this->mCodeParser = new $wgInlineScriptsParserParams['parserClass']( $this ); |
53 | 81 | } |
54 | 82 | |
55 | 83 | public function resetState() { |
56 | 84 | $this->mVars = array(); |
57 | | - $this->mCode = ''; |
58 | 85 | $this->mOut = ''; |
59 | 86 | } |
60 | 87 | |
61 | 88 | protected function checkRecursionLimit( $rec ) { |
| 89 | + global $wgInlineScriptsParserParams; |
62 | 90 | if( $rec > $this->mParser->is_maxDepth ) |
63 | 91 | $this->mParser->is_maxDepth = $rec; |
64 | | - return $rec <= $this->mLimits['depth']; |
| 92 | + return $rec <= $wgInlineScriptsParserParams['limits']['depth']; |
65 | 93 | } |
66 | 94 | |
67 | 95 | protected function increaseEvaluationsCount() { |
| 96 | + global $wgInlineScriptsParserParams; |
68 | 97 | $this->mParser->is_evalsCount++; |
69 | | - return $this->mParser->is_evalsCount <= $this->mLimits['evaluations']; |
| 98 | + return $this->mParser->is_evalsCount <= $wgInlineScriptsParserParams['limits']['evaluations']; |
70 | 99 | } |
71 | 100 | |
72 | 101 | public function increaseTokensCount() { |
| 102 | + global $wgInlineScriptsParserParams; |
73 | 103 | $this->mParser->is_tokensCount++; |
74 | | - return $this->mParser->is_tokensCount <= $this->mLimits['tokens']; |
| 104 | + return $this->mParser->is_tokensCount <= $wgInlineScriptsParserParams['limits']['tokens']; |
75 | 105 | } |
76 | 106 | |
77 | | - public function evaluateForOutput( $code, $parser, $frame ) { |
| 107 | + public function evaluateForOutput( $code, $parser, $frame, $resetState = true ) { |
78 | 108 | wfProfileIn( __METHOD__ ); |
79 | | - $this->resetState(); |
| 109 | + if( $resetState ) |
| 110 | + $this->resetState(); |
80 | 111 | $this->mParser = $parser; |
81 | 112 | $this->mFrame = $frame; |
82 | 113 | |
— | — | @@ -85,9 +116,10 @@ |
86 | 117 | return $this->mOut; |
87 | 118 | } |
88 | 119 | |
89 | | - public function evaluate( $code, $parser, $frame ) { |
| 120 | + public function evaluate( $code, $parser, $frame, $resetState = true ) { |
90 | 121 | wfProfileIn( __METHOD__ ); |
91 | | - $this->resetState(); |
| 122 | + if( $resetState ) |
| 123 | + $this->resetState(); |
92 | 124 | $this->mParser = $parser; |
93 | 125 | $this->mFrame = $frame; |
94 | 126 | |
— | — | @@ -97,18 +129,30 @@ |
98 | 130 | } |
99 | 131 | |
100 | 132 | public function getCodeAST( $code ) { |
101 | | - global $wgMemc; |
| 133 | + global $parserMemc; |
| 134 | + static $ASTCache; |
| 135 | + |
| 136 | + wfProfileIn( __METHOD__ ); |
102 | 137 | $code = trim( $code ); |
103 | 138 | |
104 | 139 | $memcKey = 'isparser:ast:' . md5( $code ); |
105 | | - $cached = $wgMemc->get( $memcKey ); |
106 | | - if( $cached instanceof ISParserOutput && !$cached->isOutOfDate() ) { |
| 140 | + if( isset( $ASTCache[$memcKey] ) ) { |
| 141 | + wfProfileOut( __METHOD__ ); |
| 142 | + return $ASTCache[$memcKey]; |
| 143 | + } |
| 144 | + |
| 145 | + $cached = $parserMemc->get( $memcKey ); |
| 146 | + if( @$cached instanceof ISParserOutput && !$cached->isOutOfDate() ) { |
107 | 147 | $cached->appendTokenCount( $this ); |
| 148 | + $ASTCache[$memcKey] = $cached->getAST(); |
| 149 | + wfProfileOut( __METHOD__ ); |
108 | 150 | return $cached->getAST(); |
109 | 151 | } |
110 | 152 | |
111 | 153 | $out = $this->mCodeParser->parse( $code ); |
112 | | - $wgMemc->set( $memcKey, $out ); |
| 154 | + $parserMemc->set( $memcKey, $out ); |
| 155 | + $ASTCache[$memcKey] = $out->getAST(); |
| 156 | + wfProfileOut( __METHOD__ ); |
113 | 157 | return $out->getAST(); |
114 | 158 | } |
115 | 159 | |
— | — | @@ -204,15 +248,34 @@ |
205 | 249 | |
206 | 250 | /* Variable assignment */ |
207 | 251 | case ISOperatorNode::OSet: |
208 | | - $data = $this->evaluateASTNode( $r, $rec + 1 ); |
209 | | - if( $l->getType() != ISASTNode::NodeData || $l->getType() == ISDataNode::DNData ) |
210 | | - throw new ISUserVisibleException( 'cantchangeconst', $pos ); |
211 | | - switch( $l->getDataType() ) { |
212 | | - case ISDataNode::DNVariable: |
213 | | - $this->mVars[$l->getVar()] = $data; |
214 | | - break; |
| 252 | + case ISOperatorNode::OSetAdd: |
| 253 | + case ISOperatorNode::OSetSub: |
| 254 | + case ISOperatorNode::OSetMul: |
| 255 | + case ISOperatorNode::OSetDiv: |
| 256 | + if( $l->isOp( ISOperatorNode::OArrayElement ) || $l->isOp( ISOperatorNode::OArrayElementSingle ) ) { |
| 257 | + $datanode = $r; |
| 258 | + $keys = array(); |
| 259 | + while( $l->isOp( ISOperatorNode::OArrayElement ) || $l->isOp( ISOperatorNode::OArrayElementSingle ) ) { |
| 260 | + @list( $l, $r ) = $l->getChildren(); |
| 261 | + array_unshift( $keys, $r ? $r : null ); |
| 262 | + } |
| 263 | + if( $l->getType() != ISASTNode::NodeData || $l->getType() == ISDataNode::DNData ) |
| 264 | + throw new ISUserVisibleException( 'cantchangeconst', $pos ); |
| 265 | + $array = $this->getDataNodeValue( new ISDataNode( $l->getVar(), 0 ) ); |
| 266 | + foreach( $keys as &$key ) |
| 267 | + if( $key ) |
| 268 | + $key = $this->evaluateASTNode( $key, $rec + 1 ); |
| 269 | + $val = $this->evaluateASTNode( $datanode, $rec + 1 ); |
| 270 | + $array->setValueByIndices( $val, $keys ); |
| 271 | + $this->mVars[$l->getVar()] = $array; |
| 272 | + return $val; |
| 273 | + } else { |
| 274 | + if( $l->getType() != ISASTNode::NodeData || $l->getType() == ISDataNode::DNData ) |
| 275 | + throw new ISUserVisibleException( 'cantchangeconst', $pos ); |
| 276 | + $val = $this->getValueForSetting( @$this->mVars[$l->getVar()], |
| 277 | + $this->evaluateASTNode( $r, $rec + 1 ), $op ); |
| 278 | + return $this->mVars[$l->getVar()] = $val; |
215 | 279 | } |
216 | | - return $data; |
217 | 280 | |
218 | 281 | /* Arrays */ |
219 | 282 | case ISOperatorNode::OArray: |
— | — | @@ -232,7 +295,7 @@ |
233 | 296 | if( $array->type != ISData::DList ) |
234 | 297 | throw new ISUserVisibleException( 'notanarray', $ast->getPos(), array( $array->type ) ); |
235 | 298 | if( count( $array->data ) <= $index ) |
236 | | - throw new ISUserVisibleException( 'outofbounds', $ast->getPos(), array( count( $array->data, $index ) ) ); |
| 299 | + throw new ISUserVisibleException( 'outofbounds', $ast->getPos(), array( count( $array->data ), $index ) ); |
237 | 300 | return $array->data[$index]; |
238 | 301 | |
239 | 302 | /* Flow control (if, foreach, etc) */ |
— | — | @@ -251,10 +314,13 @@ |
252 | 315 | list( $l, $r ) = $l->getChildren(); |
253 | 316 | if( $r->isOp( ISOperatorNode::OElse ) ) { |
254 | 317 | list( $onTrue, $onFalse ) = $r->getChildren(); |
255 | | - if( $this->evaluateASTNode( $l )->toBool() ) |
256 | | - $this->evaluateASTNode( $onTrue ); |
| 318 | + if( $this->evaluateASTNode( $l, $rec + 1 )->toBool() ) |
| 319 | + $this->evaluateASTNode( $onTrue, $rec + 1 ); |
257 | 320 | else |
258 | | - $this->evaluateASTNode( $onFalse ); |
| 321 | + $this->evaluateASTNode( $onFalse, $rec + 1 ); |
| 322 | + } else { |
| 323 | + if( $this->evaluateASTNode( $l, $rec + 1 )->toBool() ) |
| 324 | + $this->evaluateASTNode( $r, $rec + 1 ); |
259 | 325 | } |
260 | 326 | return new ISData(); |
261 | 327 | case ISOperatorNode::OForeach: |
— | — | @@ -265,11 +331,79 @@ |
266 | 332 | if( $array->type != ISData::DList ) |
267 | 333 | throw new ISUserVisibleException( 'invalidforeach', $ast->getPos(), array( $array->type ) ); |
268 | 334 | foreach( $array->data as $element ) { |
269 | | - $this->mVars[$ast->getData()] = $element; |
270 | | - $this->evaluateASTNode( $r, $rec + 1 ); |
| 335 | + try { |
| 336 | + $this->mVars[$ast->getData()] = $element; |
| 337 | + $this->evaluateASTNode( $r, $rec + 1 ); |
| 338 | + } catch( ISUserVisibleException $e ) { |
| 339 | + if( $e->getExceptionID() == 'break' ) |
| 340 | + break; |
| 341 | + elseif( $e->getExceptionID() == 'continue' ) |
| 342 | + continue; |
| 343 | + else |
| 344 | + throw $e; |
| 345 | + } |
271 | 346 | } |
272 | 347 | return new ISData(); |
273 | | - |
| 348 | + case ISOperatorNode::OTry: |
| 349 | + if( $l->isOp( ISOperatorNode::OCatch ) ) { |
| 350 | + list( $code, $errorHandler ) = $l->getChildren(); |
| 351 | + try { |
| 352 | + $val = $this->evaluateASTNode( $code, $rec + 1 ); |
| 353 | + } catch( ISUserVisibleException $e ) { |
| 354 | + if( in_array( $e->getExceptionID(), array( 'break', 'continue' ) ) ) |
| 355 | + throw $e; |
| 356 | + $varname = $l->getData(); |
| 357 | + $old = wfSetVar( $this->mVars[$varname], |
| 358 | + new ISData( ISData::DString, $e->getExceptionID() ) ); |
| 359 | + $val = $this->evaluateASTNode( $errorHandler, $rec + 1 ); |
| 360 | + $this->mVars[$varname] = $old; |
| 361 | + } |
| 362 | + return $val; |
| 363 | + } else { |
| 364 | + try { |
| 365 | + return $this->evaluateASTNode( $l, $rec + 1 ); |
| 366 | + } catch( ISUserVisibleException $e ) { |
| 367 | + return new ISData(); |
| 368 | + } |
| 369 | + } |
| 370 | + |
| 371 | + /* break/continue */ |
| 372 | + case ISOperatorNode::OBreak: |
| 373 | + throw new ISUserVisibleException( 'break', $ast->getPos() ); |
| 374 | + case ISOperatorNode::OContinue: |
| 375 | + throw new ISUserVisibleException( 'continue', $ast->getPos() ); |
| 376 | + |
| 377 | + /* isset/unset */ |
| 378 | + case ISOperatorNode::OUnset: |
| 379 | + if( $l->getType() == ISASTNode::NodeData && $l->getDataType() == ISDataNode::DNVariable ) { |
| 380 | + if( isset( $this->mVars[$l->getVar()] ) ) |
| 381 | + unset( $this->mVars[$l->getVar()] ); |
| 382 | + break; |
| 383 | + } else { |
| 384 | + throw new ISUserVisibleException( 'cantchangeconst', $ast->getPos() ); |
| 385 | + } |
| 386 | + case ISOperatorNode::OIsset: |
| 387 | + if( $l->getType() == ISASTNode::NodeData && $l->getDataType() == ISDataNode::DNVariable ) { |
| 388 | + return new ISData( ISData::DBool, isset( $this->mVars[$l->getVar()] ) ); |
| 389 | + } elseif( $l->isOp( ISOperatorNode::OArrayElement ) ) { |
| 390 | + $indices = array(); |
| 391 | + while( $l->isOp( ISOperatorNode::OArrayElement ) ) { |
| 392 | + list( $l, $r ) = $l->getChildren(); |
| 393 | + array_unshift( $indices, $r ); |
| 394 | + } |
| 395 | + if( !($l->getType() == ISASTNode::NodeData && $l->getDataType() == ISDataNode::DNVariable) ) |
| 396 | + throw new ISUserVisibleException( 'cantchangeconst', $ast->getPos() ); |
| 397 | + foreach( $indices as &$idx ) |
| 398 | + $idx = $this->evaluateASTNode( $idx )->toInt(); |
| 399 | + $var = $l->getVar(); |
| 400 | + |
| 401 | + if( !isset( $this->mVars[$var] ) ) |
| 402 | + return new ISData( ISData::DBool, false ); |
| 403 | + return new ISData( ISData::DBool, $this->mVars[$var]->checkIssetByIndices( $indices ) ); |
| 404 | + } else { |
| 405 | + throw new ISUserVisibleException( 'cantchangeconst', $ast->getPos() ); |
| 406 | + } |
| 407 | + |
274 | 408 | /* Functions */ |
275 | 409 | case ISOperatorNode::OFunction: |
276 | 410 | $args = array(); |
— | — | @@ -281,7 +415,7 @@ |
282 | 416 | array_unshift( $args, $l ); |
283 | 417 | } |
284 | 418 | foreach( $args as &$arg ) |
285 | | - $arg = $this->evaluateASTNode( $arg ); |
| 419 | + $arg = $this->evaluateASTNode( $arg, $rec + 1 ); |
286 | 420 | $funcName = self::$mFunctions[$ast->getData()]; |
287 | 421 | $result = $this->$funcName( $args, $ast->getPos() ); |
288 | 422 | return $result; |
— | — | @@ -290,7 +424,7 @@ |
291 | 425 | } |
292 | 426 | } |
293 | 427 | |
294 | | - function getDataNodeValue( $node ) { |
| 428 | + protected function getDataNodeValue( $node ) { |
295 | 429 | switch( $node->getDataType() ) { |
296 | 430 | case ISDataNode::DNData: |
297 | 431 | return $node->getData(); |
— | — | @@ -303,19 +437,30 @@ |
304 | 438 | } |
305 | 439 | } |
306 | 440 | |
307 | | - /** Functions */ |
308 | | - function funcLc( $args, $pos ) { |
309 | | - global $wgContLang; |
310 | | - if( !$args ) |
311 | | - throw new ISUserVisibleException( 'notenoughargs', $pos ); |
312 | | - |
313 | | - return new ISData( ISData::DString, $wgContLang->lc( $args[0]->toString() ) ); |
| 441 | + protected function getValueForSetting( $old, $new, $set ) { |
| 442 | + switch( $set ) { |
| 443 | + case ISOperatorNode::OSetAdd: |
| 444 | + return ISData::sum( $old, $new ); |
| 445 | + case ISOperatorNode::OSetSub: |
| 446 | + return ISData::sub( $old, $new ); |
| 447 | + case ISOperatorNode::OSetMul: |
| 448 | + return ISData::mulRel( $old, $new, '*', 0 ); |
| 449 | + case ISOperatorNode::OSetDiv: |
| 450 | + return ISData::mulRel( $old, $new, '/', 0 ); |
| 451 | + default: |
| 452 | + return $new; |
| 453 | + } |
314 | 454 | } |
315 | 455 | |
316 | | - function funcOut( $args, $pos ) { |
317 | | - if( !$args ) |
| 456 | + protected function checkParamsCount( $args, $pos, $count ) { |
| 457 | + if( count( $args ) < $count ) |
318 | 458 | throw new ISUserVisibleException( 'notenoughargs', $pos ); |
| 459 | + } |
319 | 460 | |
| 461 | + /** Functions */ |
| 462 | + protected function funcOut( $args, $pos ) { |
| 463 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 464 | + |
320 | 465 | for( $i = 0; $i < count( $args ); $i++ ) |
321 | 466 | $args[$i] = $args[$i]->toString(); |
322 | 467 | $str = implode( "\n", $args ); |
— | — | @@ -323,19 +468,149 @@ |
324 | 469 | return new ISData(); |
325 | 470 | } |
326 | 471 | |
327 | | - function funcArg( $args, $pos ) { |
328 | | - if( !$args ) |
329 | | - throw new ISUserVisibleException( 'notenoughargs', $pos ); |
| 472 | + protected function funcArg( $args, $pos ) { |
| 473 | + $this->checkParamsCount( $args, $pos, 1 ); |
330 | 474 | |
331 | 475 | $argName = $args[0]->toString(); |
332 | | - return new ISData( ISData::DString, $this->mFrame->getArgument( $argName ) ); |
| 476 | + $default = isset( $args[1] ) ? $args[1] : new ISData(); |
| 477 | + if( $this->mFrame->getArgument( $argName ) === false ) |
| 478 | + return $default; |
| 479 | + else |
| 480 | + return new ISData( ISData::DString, $this->mFrame->getArgument( $argName ) ); |
333 | 481 | } |
334 | 482 | |
335 | | - function funcArgs( $args, $pos ) { |
| 483 | + protected function funcArgs( $args, $pos ) { |
336 | 484 | return ISData::newFromPHPVar( $this->mFrame->getNumberedArguments() ); |
337 | 485 | } |
338 | 486 | |
339 | | - function funcIsTranscluded( $args, $pos ) { |
| 487 | + protected function funcIsTranscluded( $args, $pos ) { |
340 | 488 | return new ISData( ISData::DBool, $this->mFrame->isTemplate() ); |
341 | 489 | } |
| 490 | + |
| 491 | + protected function funcParse( $args, $pos ) { |
| 492 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 493 | + |
| 494 | + $text = $args[0]->toString(); |
| 495 | + $oldOT = $this->mParser->mOutputType; |
| 496 | + $this->mParser->setOutputType( Parser::OT_PREPROCESS ); |
| 497 | + $parsed = $this->mParser->replaceVariables( $text, $this->mFrame ); |
| 498 | + $parsed = $this->mParser->mStripState->unstripBoth( $parsed ); |
| 499 | + $this->mParser->setOutputType( $oldOT ); |
| 500 | + return new ISData( ISData::DString, $parsed ); |
| 501 | + } |
| 502 | + |
| 503 | + protected function funcLc( $args, $pos ) { |
| 504 | + global $wgContLang; |
| 505 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 506 | + return new ISData( ISData::DString, $wgContLang->lc( $args[0]->toString() ) ); |
| 507 | + } |
| 508 | + |
| 509 | + protected function funcUc( $args, $pos ) { |
| 510 | + global $wgContLang; |
| 511 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 512 | + return new ISData( ISData::DString, $wgContLang->uc( $args[0]->toString() ) ); |
| 513 | + } |
| 514 | + |
| 515 | + protected function funcUcFirst( $args, $pos ) { |
| 516 | + global $wgContLang; |
| 517 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 518 | + return new ISData( ISData::DString, $wgContLang->ucfirst( $args[0]->toString() ) ); |
| 519 | + } |
| 520 | + |
| 521 | + protected function funcUrlencode( $args, $pos ) { |
| 522 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 523 | + return new ISData( ISData::DString, urlencode( $args[0]->toString() ) ); |
| 524 | + } |
| 525 | + |
| 526 | + protected function funcAnchorEncode( $args, $pos ) { |
| 527 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 528 | + |
| 529 | + $s = urlencode( $args[0]->toString() ); |
| 530 | + $s = strtr( $s, array( '%' => '.', '+' => '_' ) ); |
| 531 | + $s = str_replace( '.3A', ':', $s ); |
| 532 | + |
| 533 | + return new ISData( ISData::DString, $s ); |
| 534 | + } |
| 535 | + |
| 536 | + protected function funcGrammar( $args, $pos ) { |
| 537 | + $this->checkParamsCount( $args, $pos, 2 ); |
| 538 | + list( $case, $word ) = $args; |
| 539 | + $res = $this->mParser->getFunctionLang()->convertGrammar( |
| 540 | + $word->toString(), $case->toString() ); |
| 541 | + return new ISData( ISData::DString, $res ); |
| 542 | + } |
| 543 | + |
| 544 | + protected function funcPlural( $args, $pos ) { |
| 545 | + $this->checkParamsCount( $args, $pos, 2 ); |
| 546 | + $num = $args[0]->toInt(); |
| 547 | + for( $i = 1; $i < count( $args ); $i++ ) |
| 548 | + $forms[] = $args[$i]->toString(); |
| 549 | + $res = $this->mParser->getFunctionLang()->convertPlural( $num, $forms ); |
| 550 | + return new ISData( ISData::DString, $res ); |
| 551 | + } |
| 552 | + |
| 553 | + protected function funcStrlen( $args, $pos ) { |
| 554 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 555 | + return new ISData( ISData::DInt, mb_strlen( $args[0]->toString() ) ); |
| 556 | + } |
| 557 | + |
| 558 | + protected function funcSubstr( $args, $pos ) { |
| 559 | + $this->checkParamsCount( $args, $pos, 3 ); |
| 560 | + $s = $args[0]->toString(); |
| 561 | + $start = $args[1]->toInt(); |
| 562 | + $end = $args[2]->toInt(); |
| 563 | + return new ISData( ISData::DString, mb_substr( $s, $start, $end ) ); |
| 564 | + } |
| 565 | + |
| 566 | + protected function funcStrreplace( $args, $pos ) { |
| 567 | + $this->checkParamsCount( $args, $pos, 3 ); |
| 568 | + $s = $args[0]->toString(); |
| 569 | + $old = $args[1]->toString(); |
| 570 | + $new = $args[2]->toString(); |
| 571 | + return new ISData( ISData::DString, str_replace( $old, $new, $s ) ); |
| 572 | + } |
| 573 | + |
| 574 | + protected function funcSplit( $args, $pos ) { |
| 575 | + $this->checkParamsCount( $args, $pos, 2 ); |
| 576 | + $list = explode( $args[0]->toString(), $args[1]->toString() ); |
| 577 | + return ISData::newFromPHPVar( $list ); |
| 578 | + } |
| 579 | + |
| 580 | + protected function funcJoin( $args, $pos ) { |
| 581 | + $this->checkParamsCount( $args, $pos, 2 ); |
| 582 | + $seperator = $args[0]->toString(); |
| 583 | + if( $args[1]->type == ISData::DList ) { |
| 584 | + $bits = $args[1]->data; |
| 585 | + } else { |
| 586 | + $bits = array_slice( $args, 1 ); |
| 587 | + } |
| 588 | + foreach( $bits as &$bit ) |
| 589 | + $bit = $bit->toString(); |
| 590 | + return new ISData( ISData::DString, implode( $seperator, $bits ) ); |
| 591 | + } |
| 592 | + |
| 593 | + protected function funcCount( $args, $pos ) { |
| 594 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 595 | + return new ISData( ISData::DInt, count( $args[0]->toList()->data ) ); |
| 596 | + } |
| 597 | + |
| 598 | + protected function castString( $args, $pos ) { |
| 599 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 600 | + return ISData::castTypes( $args[0], ISData::DString ); |
| 601 | + } |
| 602 | + |
| 603 | + protected function castInt( $args, $pos ) { |
| 604 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 605 | + return ISData::castTypes( $args[0], ISData::DInt ); |
| 606 | + } |
| 607 | + |
| 608 | + protected function castFloat( $args, $pos ) { |
| 609 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 610 | + return ISData::castTypes( $args[0], ISData::DFloat ); |
| 611 | + } |
| 612 | + |
| 613 | + protected function castBool( $args, $pos ) { |
| 614 | + $this->checkParamsCount( $args, $pos, 1 ); |
| 615 | + return ISData::castTypes( $args[0], ISData::DBool ); |
| 616 | + } |
342 | 617 | } |
Index: trunk/extensions/InlineScripts/interpreterTests.txt |
— | — | @@ -0,0 +1,396 @@ |
| 2 | +# Test cases for MediaWiki inline scripts engine |
| 3 | + |
| 4 | +!! test |
| 5 | +Basic mathematics |
| 6 | +!! input |
| 7 | +{{#inline:2 + 2 * 2 ** 2 - 3 * 7 % 5}} |
| 8 | +!! result |
| 9 | +<p>9 |
| 10 | +</p> |
| 11 | +!! end |
| 12 | + |
| 13 | +!! test |
| 14 | +String contecation and out() |
| 15 | +!! input |
| 16 | +<wikiscript> |
| 17 | +out( "foo" + "bar" ); |
| 18 | +</wikiscript> |
| 19 | +!! result |
| 20 | +<p>foobar |
| 21 | +</p> |
| 22 | +!! end |
| 23 | + |
| 24 | +!! test |
| 25 | +Multiple variable assignment |
| 26 | +!! input |
| 27 | +{{#inline: a = b = 3; a + b }} |
| 28 | +!! result |
| 29 | +<p>6 |
| 30 | +</p> |
| 31 | +!! end |
| 32 | + |
| 33 | +!! test |
| 34 | +Assigment with arithmetics (+=, -=, etc) |
| 35 | +!! input |
| 36 | +{{#inline: a = 2; a += 3; a -= 7; a }} |
| 37 | +!! result |
| 38 | +<p>-2 |
| 39 | +</p> |
| 40 | +!! end |
| 41 | + |
| 42 | +!! test |
| 43 | +Boolean shortcut |
| 44 | +!! input |
| 45 | +<wikiscript> |
| 46 | +!(b = 2) | (b = 3) | (b = 4); |
| 47 | +out( b ); |
| 48 | +</wikiscript> |
| 49 | +!! result |
| 50 | +<p>3 |
| 51 | +</p> |
| 52 | +!! end |
| 53 | + |
| 54 | +!! test |
| 55 | +Equality |
| 56 | +!! input |
| 57 | +{{#inline: "2" == 2 & "2" !== 2 & 4 === (2 + 2) & |
| 58 | +null == "" & false == null & 0 == "" }} |
| 59 | +!! result |
| 60 | +<p>1 |
| 61 | +</p> |
| 62 | +!! end |
| 63 | + |
| 64 | +!! test |
| 65 | +Comments |
| 66 | +!! input |
| 67 | +{{#inline: 2 + /* 2 + */ 2}} |
| 68 | +!! result |
| 69 | +<p>4 |
| 70 | +</p> |
| 71 | +!!end |
| 72 | + |
| 73 | +!! test |
| 74 | +Comparsions |
| 75 | +!! input |
| 76 | +{{#inline: 2 > 1 & 2 >= 2 & 2 <= 2 & 1 < 2}} |
| 77 | +!! result |
| 78 | +<p>1 |
| 79 | +</p> |
| 80 | +!! end |
| 81 | + |
| 82 | +!! test |
| 83 | +Tag integration |
| 84 | +!! input |
| 85 | +{{lc:<wikiscript>out("AA")</wikiscript>}} |
| 86 | +!! result |
| 87 | +<p>aa |
| 88 | +</p> |
| 89 | +!! end |
| 90 | + |
| 91 | +!! test |
| 92 | +Conditions (?) |
| 93 | +!! input |
| 94 | +{{#inline: 2 + 2 == 4 ? "a" : "b"}} |
| 95 | +!! result |
| 96 | +<p>a |
| 97 | +</p> |
| 98 | +!! end |
| 99 | + |
| 100 | +!! test |
| 101 | +Conditions (if-then, if-then-else) |
| 102 | +!! input |
| 103 | +<wikiscript> |
| 104 | +if 2 * 7 > 3 * 4 then |
| 105 | + a = 7 |
| 106 | +else { |
| 107 | + a = 10; |
| 108 | +}; |
| 109 | + |
| 110 | +if a ** 2 < 50 then |
| 111 | + out( "ok" ); |
| 112 | +</wikiscript> |
| 113 | +!! result |
| 114 | +<p>ok |
| 115 | +</p> |
| 116 | +!! end |
| 117 | + |
| 118 | +!! article |
| 119 | +Template:Bullets |
| 120 | +!! text |
| 121 | +<wikiscript> |
| 122 | +foreach a in args() do |
| 123 | + out( "* " + a + "\n" ); |
| 124 | +</wikiscript> |
| 125 | +!! endarticle |
| 126 | + |
| 127 | +!! test |
| 128 | +args() function |
| 129 | +!! input |
| 130 | +{{bullets|a|b|c}} |
| 131 | +!! result |
| 132 | +<ul><li> a |
| 133 | +</li><li> b |
| 134 | +</li><li> c |
| 135 | +</li></ul> |
| 136 | + |
| 137 | +!! end |
| 138 | + |
| 139 | +!! article |
| 140 | +Template:TranscludedSwitch |
| 141 | +!! text |
| 142 | +{{#inline: isTranscluded() ? arg(1) : "?!"}} |
| 143 | +!! endarticle |
| 144 | + |
| 145 | +!! test |
| 146 | +isTranscluded()/arg() check |
| 147 | +!! input |
| 148 | +{{TranscludedSwitch|11}} |
| 149 | +!! result |
| 150 | +<p>11 |
| 151 | +</p> |
| 152 | +!! end |
| 153 | + |
| 154 | +!! test |
| 155 | +Empty argument handling check |
| 156 | +!! input |
| 157 | +{{#inline: arg("test") === null}} |
| 158 | +!! result |
| 159 | +<p>1 |
| 160 | +</p> |
| 161 | +!! end |
| 162 | + |
| 163 | +!! test |
| 164 | +Casts |
| 165 | +!! input |
| 166 | +{{#inline: string(float(2)) === "2.0" & int(7.99) === 7}} |
| 167 | +!! result |
| 168 | +<p>1 |
| 169 | +</p> |
| 170 | +!! end |
| 171 | + |
| 172 | +!! test |
| 173 | +Exception handling |
| 174 | +!! input |
| 175 | +{{#inline: try 2 / 0 catch e e }} |
| 176 | +!! result |
| 177 | +<p>dividebyzero |
| 178 | +</p> |
| 179 | +!! end |
| 180 | + |
| 181 | +!! article |
| 182 | +Template:Numberofsomething |
| 183 | +!! text |
| 184 | +721 |
| 185 | +!! endarticle |
| 186 | + |
| 187 | +!! test |
| 188 | +Template access via parse() |
| 189 | +!! input |
| 190 | +<wikiscript noparse="1"> |
| 191 | +numofsmth = int( parse( '{{numberofsomething}}' ) ) + 279; |
| 192 | +out( '{{numberofsomething}}: ' + numofsmth ); |
| 193 | +</wikiscript> |
| 194 | +!! result |
| 195 | +<p>{{numberofsomething}}: 1000 |
| 196 | +</p> |
| 197 | +!! end |
| 198 | + |
| 199 | +!! test |
| 200 | +String functions 1 |
| 201 | +!! input |
| 202 | +{{#inline: lc( 'FOO' ) == 'foo' & uc( 'foo' ) == 'FOO' & |
| 203 | +ucfirst( 'bar' ) == 'Bar' & urlencode( 'a="b"' ) == "a%3D%22b%22" }} |
| 204 | +!! result |
| 205 | +<p>1 |
| 206 | +</p> |
| 207 | +!! end |
| 208 | + |
| 209 | +!! test |
| 210 | +String functions 2 |
| 211 | +!! input |
| 212 | +{{#inline: strlen( "тест" ) == 4 & substr( "слово", 1, 2 ) == "ло" & |
| 213 | + strreplace( "abcd", 'bc', 'ad' ) == 'aadd' |
| 214 | +}} |
| 215 | +!! result |
| 216 | +<p>1 |
| 217 | +</p> |
| 218 | +!! end |
| 219 | + |
| 220 | +!! test |
| 221 | +split()/join() |
| 222 | +!! input |
| 223 | +{{#inline: join( '!', split( ':', 'a:b:c:d' ) ) + join( ' ', '', 'e', 'f' ) }} |
| 224 | +!! result |
| 225 | +<p>a!b!c!d e f |
| 226 | +</p> |
| 227 | +!! end |
| 228 | + |
| 229 | +!! test |
| 230 | +isset/unset |
| 231 | +!! input |
| 232 | +<wikiscript> |
| 233 | +a = null; |
| 234 | +b = 1; |
| 235 | +unset( b ); |
| 236 | +out( 'a: ' + isset( a ) + '; b: ' + int( isset( b ) ) ); |
| 237 | +!! result |
| 238 | +<p>a: 1; b: 0 |
| 239 | +</p> |
| 240 | +!! end |
| 241 | + |
| 242 | +# |
| 243 | +## Lists |
| 244 | +# |
| 245 | +!! test |
| 246 | +Lists: basics |
| 247 | +!! input |
| 248 | +<wikiscript> |
| 249 | +a = [ b = "a", b = "b", b = "c" ]; |
| 250 | +out( a[1] + b ) |
| 251 | +</wikiscript> |
| 252 | +!! result |
| 253 | +<p>bc |
| 254 | +</p> |
| 255 | +!! end |
| 256 | + |
| 257 | +!! test |
| 258 | +Lists: foreach |
| 259 | +!! input |
| 260 | +<wikiscript> |
| 261 | +a = [ 1, 2, 3, 4, 5 ]; |
| 262 | +foreach n in a do |
| 263 | + out( n * n + "\n\n"); |
| 264 | +</wikiscript> |
| 265 | +!! result |
| 266 | +<p>1 |
| 267 | +</p><p>4 |
| 268 | +</p><p>9 |
| 269 | +</p><p>16 |
| 270 | +</p><p>25 |
| 271 | +</p> |
| 272 | +!! end |
| 273 | + |
| 274 | +!! test |
| 275 | +List merging |
| 276 | +!! input |
| 277 | +<wikiscript> |
| 278 | +foreach element in [ 7, 4 ] + [ 2, 8 ] do |
| 279 | + out( element ); |
| 280 | +</wikiscript> |
| 281 | +!! result |
| 282 | +<p>7428 |
| 283 | +</p> |
| 284 | +!! end |
| 285 | + |
| 286 | +!! test |
| 287 | +Lists: loop control (break/continue) |
| 288 | +!! input |
| 289 | +<wikiscript> |
| 290 | +a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]; |
| 291 | +foreach e in a do { |
| 292 | + if e >= 6 & e < 9 then continue; |
| 293 | + out( e ); |
| 294 | +}; |
| 295 | +foreach e in a do { |
| 296 | + if e == 3 then break; |
| 297 | + out( e ); |
| 298 | +} |
| 299 | +</wikiscript> |
| 300 | +!! result |
| 301 | +<p>12345912 |
| 302 | +</p> |
| 303 | +!! end |
| 304 | + |
| 305 | +!! test |
| 306 | +Lists: changing value of an element |
| 307 | +!! input |
| 308 | +<wikiscript> |
| 309 | +a = [ [ 2, 3 ], [ 5, 6 ], 7 ]; |
| 310 | +a[1][0] = 3; |
| 311 | +a[0][] = 1; |
| 312 | +out( a ); |
| 313 | +</wikiscript> |
| 314 | +!! result |
| 315 | +<p>2 |
| 316 | +3 |
| 317 | +1 |
| 318 | +</p><p>3 |
| 319 | +6 |
| 320 | +</p><p>7 |
| 321 | +</p> |
| 322 | +!! end |
| 323 | + |
| 324 | +!! test |
| 325 | +Lists: isset |
| 326 | +!! input |
| 327 | +<wikiscript> |
| 328 | +lst = [ 'a', 'b', 'c' ]; |
| 329 | +out( isset( lst[1] ) + isset( lst[2] ) + isset( list[3] ) ); |
| 330 | +</wikiscript> |
| 331 | +!! result |
| 332 | +<p>2 |
| 333 | +</p> |
| 334 | +!! end |
| 335 | + |
| 336 | +# |
| 337 | +## Error handling |
| 338 | +# |
| 339 | +!! test |
| 340 | +Error handling: unexcepted token |
| 341 | +!! input |
| 342 | +{{#inline: 2 2}} |
| 343 | +!! result |
| 344 | +<p><strong class="error">Unexpected token at char 3</strong> |
| 345 | +</p> |
| 346 | +!! end |
| 347 | + |
| 348 | +!! test |
| 349 | +Error handling: missing second argument |
| 350 | +!! input |
| 351 | +{{#inline: 2 + }} |
| 352 | +!! result |
| 353 | +<p><strong class="error">Not enough aruments for operator at char 3</strong> |
| 354 | +</p> |
| 355 | +!! end |
| 356 | + |
| 357 | +!! test |
| 358 | +Error handling: token limit |
| 359 | +!! config |
| 360 | +wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 'limits' => array( 'tokens' => 10, 'evaluations' => 1000, 'depth' => 100 ) ) |
| 361 | +!! input |
| 362 | +{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2}} |
| 363 | +!! result |
| 364 | +<p><strong class="error">Exceeded tokens limit</strong> |
| 365 | +</p> |
| 366 | +!! end |
| 367 | + |
| 368 | +!! test |
| 369 | +Error handling: evaluations limit |
| 370 | +!! config |
| 371 | +wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 'limits' => array( 'tokens' => 25000, 'evaluations' => 5, 'depth' => 100 ) ) |
| 372 | +!! input |
| 373 | +{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2}} |
| 374 | +!! result |
| 375 | +<p><strong class="error">Exceeded evaluations limit</strong> |
| 376 | +</p> |
| 377 | +!! end |
| 378 | + |
| 379 | +!! test |
| 380 | +Error handling: AST depth limit |
| 381 | +!! config |
| 382 | +wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 'limits' => array( 'tokens' => 25000, 'evaluations' => 1000, 'depth' => 5 ) ) |
| 383 | +!! input |
| 384 | +{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 * 2 + 2 + 2}} |
| 385 | +!! result |
| 386 | +<p><strong class="error">Too deep abstract syntax tree</strong> |
| 387 | +</p> |
| 388 | +!! end |
| 389 | + |
| 390 | +!! test |
| 391 | +Error handling: missing arguments |
| 392 | +!! input |
| 393 | +{{#inline: arg()}} |
| 394 | +!! result |
| 395 | +<p><strong class="error">Not enough arguments for function at char 3</strong> |
| 396 | +</p> |
| 397 | +!! end |
Property changes on: trunk/extensions/InlineScripts/interpreterTests.txt |
___________________________________________________________________ |
Name: svn:eol-style |
1 | 398 | + native |
Index: trunk/extensions/InlineScripts/InlineScripts.i18n.php |
— | — | @@ -29,6 +29,9 @@ |
30 | 30 | 'inlinescripts-exception-unexceptedop' => 'Unexpected operator $2', |
31 | 31 | 'inlinescripts-exception-notenoughargs' => 'Not enough arguments for function at char $1', |
32 | 32 | 'inlinescripts-exception-notenoughopargs' => 'Not enough aruments for operator at char $1', |
| 33 | + 'inlinescripts-exception-dividebyzero' => 'Division by zero at char $1', |
| 34 | + 'inlinescripts-exception-break' => '"break" called outside of foreach at char $1', |
| 35 | + 'inlinescripts-exception-continue' => '"continue" called outside of foreach at char $1', |
33 | 36 | ); |
34 | 37 | |
35 | 38 | // == Magic words == |
Index: trunk/extensions/InlineScripts/InlineScripts.php |
— | — | @@ -36,6 +36,7 @@ |
37 | 37 | $wgExtensionMessagesFiles['InlineScripts'] = $dir . 'InlineScripts.i18n.php'; |
38 | 38 | $wgAutoloadClasses['InlineScriptInterpreter'] = $dir . 'interpreter/Interpreter.php'; |
39 | 39 | $wgAutoloadClasses['ISCodeParserShuntingYard'] = $dir . 'interpreter/ParserShuntingYard.php'; |
| 40 | +$wgParserTestFiles[] = $dir . 'interpreterTests.txt'; |
40 | 41 | $wgHooks['ParserFirstCallInit'][] = 'InlineScriptsHooks::setupParserHook'; |
41 | 42 | $wgHooks['ParserClearState'][] = 'InlineScriptsHooks::clearState'; |
42 | 43 | $wgHooks['ParserLimitReport'][] = 'InlineScriptsHooks::reportLimits'; |
— | — | @@ -50,12 +51,12 @@ |
51 | 52 | * Maximal amount of tokens (strings, keywords, numbers, operators, |
52 | 53 | * but not whitespace) to be parsed. |
53 | 54 | */ |
54 | | - 'tokens' => 20000, |
| 55 | + 'tokens' => 25000, |
55 | 56 | /** |
56 | 57 | * Maximal amount of operations (multiplications, comarsionss, function |
57 | 58 | * calls) to be done. |
58 | 59 | */ |
59 | | - 'evaluations' => 5000, |
| 60 | + 'evaluations' => 10000, |
60 | 61 | /** |
61 | 62 | * Maximal depth of recursion when evaluating the parser tree. For |
62 | 63 | * example 2 + 2 * 2 ** 2 is parsed to (2 + (2 * (2 ** 2))) and needs |
— | — | @@ -72,7 +73,7 @@ |
73 | 74 | * Register parser hook |
74 | 75 | */ |
75 | 76 | public static function setupParserHook( &$parser ) { |
76 | | - $parser->setFunctionHook( 'script', 'InlineScriptsHooks::scriptHook', SFH_OBJECT_ARGS ); |
| 77 | + $parser->setFunctionTagHook( 'wikiscript', 'InlineScriptsHooks::scriptHook', SFH_OBJECT_ARGS ); |
77 | 78 | $parser->setFunctionHook( 'inline', 'InlineScriptsHooks::inlineHook', SFH_OBJECT_ARGS ); |
78 | 79 | return true; |
79 | 80 | } |
— | — | @@ -85,26 +86,34 @@ |
86 | 87 | } |
87 | 88 | |
88 | 89 | public static function inlineHook( &$parser, $frame, $args ) { |
| 90 | + wfProfileIn( __METHOD__ ); |
89 | 91 | $scriptParser = self::getParser(); |
90 | 92 | try { |
91 | 93 | $result = $scriptParser->evaluate( $parser->mStripState->unstripBoth( $args[0] ), |
92 | 94 | $parser, $frame ); |
93 | 95 | } catch( ISException $e ) { |
94 | 96 | $msg = nl2br( htmlspecialchars( $e->getMessage() ) ); |
| 97 | + wfProfileOut( __METHOD__ ); |
95 | 98 | return "<strong class=\"error\">{$msg}</strong>"; |
96 | 99 | } |
| 100 | + wfProfileOut( __METHOD__ ); |
97 | 101 | return trim( $result ); |
98 | 102 | } |
99 | 103 | |
100 | | - public static function scriptHook( &$parser, $frame, $args ) { |
| 104 | + public static function scriptHook( &$parser, $frame, $code, $attribs ) { |
| 105 | + wfProfileIn( __METHOD__ ); |
101 | 106 | $scriptParser = self::getParser(); |
102 | 107 | try { |
103 | | - $result = $scriptParser->evaluateForOutput( $parser->mStripState->unstripBoth( $args[0] ), |
104 | | - $parser, $frame ); |
| 108 | + $result = $scriptParser->evaluateForOutput( $code, $parser, $frame ); |
105 | 109 | } catch( ISException $e ) { |
106 | 110 | $msg = nl2br( htmlspecialchars( $e->getMessage() ) ); |
| 111 | + wfProfileOut( __METHOD__ ); |
107 | 112 | return "<strong class=\"error\">{$msg}</strong>"; |
108 | 113 | } |
| 114 | + if( !(isset( $attribs['noparse'] ) && $attribs['noparse']) ) { |
| 115 | + $result = $parser->replaceVariables( $result, $frame ); |
| 116 | + } |
| 117 | + wfProfileOut( __METHOD__ ); |
109 | 118 | return trim( $result ); |
110 | 119 | } |
111 | 120 | |
— | — | @@ -119,9 +128,8 @@ |
120 | 129 | } |
121 | 130 | |
122 | 131 | public static function getParser() { |
123 | | - global $wgInlineScriptsParserParams; |
124 | 132 | if( !self::$scriptParser ) |
125 | | - self::$scriptParser = new InlineScriptInterpreter( $wgInlineScriptsParserParams ); |
| 133 | + self::$scriptParser = new InlineScriptInterpreter(); |
126 | 134 | return self::$scriptParser; |
127 | 135 | } |
128 | 136 | } |