Index: trunk/extensions/LuaFoo/includes/SpecialLuaTranslation.php |
— | — | @@ -0,0 +1,40 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +class LuaFoo_SpecialLuaTranslation extends SpecialPage { |
| 5 | + function __construct() { |
| 6 | + parent::__construct( 'LuaTranslation' ); |
| 7 | + } |
| 8 | + |
| 9 | + function getDescription() { |
| 10 | + return wfMsg( 'luafoo-luatranslation' ); |
| 11 | + } |
| 12 | + |
| 13 | + function execute( $par ) { |
| 14 | + global $wgRequest; |
| 15 | + |
| 16 | + $this->setHeaders(); |
| 17 | + |
| 18 | + $form = new HTMLForm( array( |
| 19 | + 'TitleText' => array( |
| 20 | + 'type' => 'text', |
| 21 | + 'label-message' => 'luafoo-convert-title', |
| 22 | + ) ) ); |
| 23 | + $form->setSubmitText( wfMsg( 'luafoo-convert-submit' ) ); |
| 24 | + $form->setSubmitCallback( array( $this, 'showTranslation' ) ); |
| 25 | + $form->setTitle( $this->getTitle() ); |
| 26 | + $form->show(); |
| 27 | + } |
| 28 | + |
| 29 | + function showTranslation( $data ) { |
| 30 | + global $wgOut; |
| 31 | + |
| 32 | + $title = Title::newFromText( $data['TitleText'], NS_TEMPLATE ); |
| 33 | + if ( !$title ) { |
| 34 | + $wgOut->addHTML( wfMsg( 'badtitle' ) ); |
| 35 | + return; |
| 36 | + } |
| 37 | + |
| 38 | + $converted = LuaFoo_Converter::convert( $title, 'lua' ); |
| 39 | + $wgOut->addHTML( Html::element( 'pre', array(), $converted ) ); |
| 40 | + } |
| 41 | +} |
Property changes on: trunk/extensions/LuaFoo/includes/SpecialLuaTranslation.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 42 | + native |
Index: trunk/extensions/LuaFoo/includes/ConverterRuntime.lua |
— | — | @@ -0,0 +1,17 @@ |
| 2 | +function pfunc_if( condition, trueResult, falseResult ) |
| 3 | + local condition = string.gsub( condition, "^%s+(.*)-%s+$", "%1" ) |
| 4 | + local result |
| 5 | + if condition == '' then |
| 6 | + result = falseResult |
| 7 | + else |
| 8 | + result = trueResult |
| 9 | + end |
| 10 | + result = string.gsub( result, "^%s+(.*)-%s+$", "%1" ) |
| 11 | + return result |
| 12 | +end |
| 13 | + |
| 14 | +function mw_trim( s ) |
| 15 | + return string.gsub( s, "^%s+(.*)-%s+$", "%1" ) |
| 16 | +end |
| 17 | + |
| 18 | + |
Index: trunk/extensions/LuaFoo/includes/Converter.php |
— | — | @@ -0,0 +1,897 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * * Expression: a language-independent tree representation of an expression. |
| 5 | + * |
| 6 | + * * Block: A formatted language-dependent list of statements, and an |
| 7 | + * expression which can be evaluated after the statements to produce the |
| 8 | + * desired result of the expression. |
| 9 | + * |
| 10 | + * * Frame: A list of assigned local variable names, which doubles as a |
| 11 | + * makeshift subexpression cache. |
| 12 | + * |
| 13 | + * To do: |
| 14 | + * * Optional line break tag for concatenation |
| 15 | + * * Vertical white space |
| 16 | + */ |
| 17 | + |
| 18 | +class LuaFoo_Converter { |
| 19 | + var $parser, $preprocessor; |
| 20 | + var $doneFunctions = array(); |
| 21 | + var $tab = ' '; |
| 22 | + var $out, $nodeDeps; |
| 23 | + |
| 24 | + var $inlineFunctions = array( |
| 25 | + 'pfunc_if' => 'pfunc_if' |
| 26 | + ); |
| 27 | + |
| 28 | + static function convert( $title, $language ) { |
| 29 | + $converter = new self( $language ); |
| 30 | + $code = $converter->convertTemplate( $title ); |
| 31 | + return $code->toString(); |
| 32 | + } |
| 33 | + |
| 34 | + protected function __construct( $language ) { |
| 35 | + $this->parser = new Parser; |
| 36 | + $this->preprocessor = new Preprocessor_Hash( $this->parser ); |
| 37 | + switch ( $language ) { |
| 38 | + case 'lua': |
| 39 | + $this->language = new LuaFoo_Converter_Lua( $this ); |
| 40 | + break; |
| 41 | + default: |
| 42 | + throw new MWException( __METHOD__.": invalid language \"$language\"" ); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + function convertTemplate( $title ) { |
| 47 | + $funcName = $this->titleToIdentifier( $title ); |
| 48 | + $deps = $this->newDeps(); |
| 49 | + $expression = $this->getTemplateExpression( $title, $deps ); |
| 50 | + $block = $this->language->makeFunction( $funcName, array( 'args' ), $expression ); |
| 51 | + |
| 52 | + foreach ( $deps->getTemplates() as $depTitleText ) { |
| 53 | + if ( !isset( $this->doneFunctions[$depTitleText] ) ) { |
| 54 | + $depTitle = Title::newFromText( $depTitleText ); |
| 55 | + $this->doneFunctions[$depTitleText] = true; |
| 56 | + $block->addBlock( $this->convertTemplate( $depTitle ) ); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + foreach ( $deps->getFunctions() as $func ) { |
| 61 | + $block->addBlock( $this->language->getRuntimeBlock( $func ) ); |
| 62 | + } |
| 63 | + |
| 64 | + return $block; |
| 65 | + } |
| 66 | + |
| 67 | + function newDeps() { |
| 68 | + return new LuaFoo_Converter_Deps; |
| 69 | + } |
| 70 | + |
| 71 | + function newLiteral( $value ) { |
| 72 | + if ( !is_string( $value ) && !is_numeric( $value ) && !is_array( $value ) ) { |
| 73 | + throw new MWException( __METHOD__.': invalid literal type' ); |
| 74 | + } |
| 75 | + return new LuaFoo_Converter_Expression( 'literal', array( $value ) ); |
| 76 | + } |
| 77 | + |
| 78 | + function newHash( $hash ) { |
| 79 | + return new LuaFoo_Converter_Expression( 'hash', array_merge( |
| 80 | + array_map( array( $this, 'newLiteral' ), array_keys( $hash ) ), |
| 81 | + array_values( $hash ) ) ); |
| 82 | + } |
| 83 | + |
| 84 | + function newConcat( $items ) { |
| 85 | + $filteredItems = array(); |
| 86 | + foreach ( $items as $item ) { |
| 87 | + if ( $item->op === 'void' ) { |
| 88 | + continue; |
| 89 | + } |
| 90 | + if ( $item->op === 'literal' && $item->args[0] === '' ) { |
| 91 | + continue; |
| 92 | + } |
| 93 | + if ( $item->op === 'literal' |
| 94 | + && count( $filteredItems ) |
| 95 | + && $filteredItems[ count( $filteredItems ) - 1 ]->op === 'literal' ) |
| 96 | + { |
| 97 | + $filteredItems[ count( $filteredItems ) - 1 ]->args[0] .= $item->args[0]; |
| 98 | + continue; |
| 99 | + } |
| 100 | + if ( $item->op === 'concat' ) { |
| 101 | + foreach ( $item->args as $subConcatArg ) { |
| 102 | + $filteredItems[] = $subConcatArg; |
| 103 | + } |
| 104 | + continue; |
| 105 | + } |
| 106 | + |
| 107 | + $filteredItems[] = $item; |
| 108 | + } |
| 109 | + if ( count( $filteredItems ) == 0 ) { |
| 110 | + return $this->newLiteral( '' ); |
| 111 | + } elseif ( count( $filteredItems ) == 1 ) { |
| 112 | + return reset( $filteredItems ); |
| 113 | + } else { |
| 114 | + return $this->newExpression( 'concat', $filteredItems ); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + protected function trimConcat( $expr, $direction ) { |
| 119 | + while ( count( $expr->args ) ) { |
| 120 | + if ( $direction == 'left' ) { |
| 121 | + $i = 0; |
| 122 | + } else { |
| 123 | + $i = count( $expr->args ) - 1; |
| 124 | + } |
| 125 | + if ( $expr->args[$i]->op !== 'literal' ) { |
| 126 | + break; |
| 127 | + } |
| 128 | + $oldPart = $expr->args[$i]->args[0]; |
| 129 | + if ( $direction == 'left' ) { |
| 130 | + $newPart = ltrim( $oldPart ); |
| 131 | + } else { |
| 132 | + $newPart = rtrim( $oldPart ); |
| 133 | + } |
| 134 | + if ( $oldPart === $newPart ) { |
| 135 | + break; |
| 136 | + } |
| 137 | + if ( $newPart === '' ) { |
| 138 | + unset( $expr->args[$i] ); |
| 139 | + $expr->args = array_values( $expr->args ); |
| 140 | + } else { |
| 141 | + $expr->args[$i] = $this->newLiteral( $newPart ); |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + function newTrim( $expr, $deps ) { |
| 147 | + if ( $expr->op === 'literal' ) { |
| 148 | + // Trim a literal |
| 149 | + return $this->newLiteral( trim( $expr->args[0] ) ); |
| 150 | + } elseif ( $expr->op === 'concat' ) { |
| 151 | + // Trim a concatenation operation |
| 152 | + $trimExpr = clone $expr; |
| 153 | + $this->trimConcat( $trimExpr, 'left' ); |
| 154 | + $this->trimConcat( $trimExpr, 'right' ); |
| 155 | + if ( count( $trimExpr->args ) == 0 ) { |
| 156 | + return $this->newLiteral( '' ); |
| 157 | + } elseif ( count( $trimExpr->args ) == 1 ) { |
| 158 | + return $trimExpr->args[0]; |
| 159 | + } else { |
| 160 | + return $trimExpr; |
| 161 | + } |
| 162 | + } else { |
| 163 | + // Trim a generic expression |
| 164 | + $deps->addFunction( 'mw_trim' ); |
| 165 | + return $this->newExpression( 'call', array( |
| 166 | + $this->newLiteral( 'mw_trim' ), $expr ) ); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + function newParserFunctionCall( $name, $args, $deps ) { |
| 171 | + if ( isset( $this->inlineFunctions[$name] ) ) { |
| 172 | + $inlineFunc = $this->inlineFunctions[$name]; |
| 173 | + return $this->$inlineFunc( $args, $deps ); |
| 174 | + } |
| 175 | + |
| 176 | + array_unshift( $args, $this->newLiteral( $name ) ); |
| 177 | + $deps->addFunction( $name ); |
| 178 | + return $this->newExpression( 'call', $args ); |
| 179 | + } |
| 180 | + |
| 181 | + function newExpression( $op, $args ) { |
| 182 | + return new LuaFoo_Converter_Expression( $op, $args ); |
| 183 | + } |
| 184 | + |
| 185 | + function titleToIdentifier( $title ) { |
| 186 | + $id = ''; |
| 187 | + if ( $title->getNamespace() == NS_TEMPLATE ) { |
| 188 | + $id = 'tpl_'; |
| 189 | + } elseif ( $title->getNamespace() == NS_MAIN ) { |
| 190 | + $id = 'article_'; |
| 191 | + } else { |
| 192 | + $id = $title->getNsText() . '_'; |
| 193 | + } |
| 194 | + $id .= $title->getDBkey(); |
| 195 | + $id = preg_replace( '/[^\w\177-\377]/', '_', $id ); |
| 196 | + return $id; |
| 197 | + } |
| 198 | + |
| 199 | + function parserFunctionToIdentifier( $name ) { |
| 200 | + $name = preg_replace( '/[^\w\177-\377]/', '_', $name ); |
| 201 | + return "pfunc_$name"; |
| 202 | + } |
| 203 | + |
| 204 | + function getTemplateExpression( $title, $deps ) { |
| 205 | + $rev = Revision::newFromTitle( $title ); |
| 206 | + if ( !$rev ) { |
| 207 | + return $this->newLiteral( '[No such page]' ); |
| 208 | + } |
| 209 | + $text = $rev->getText(); |
| 210 | + if ( $text === false ) { |
| 211 | + return $this->newLiteral( '[Unable to load text for this page]' ); |
| 212 | + } |
| 213 | + |
| 214 | + $this->parser->startExternalParse( $title, new ParserOptions, Parser::OT_WIKI ); |
| 215 | + $tree = $this->preprocessor->preprocessToObj( $text, Parser::PTD_FOR_INCLUSION ); |
| 216 | + |
| 217 | + $expression = $this->expand( $tree, $deps ); |
| 218 | + return $expression; |
| 219 | + } |
| 220 | + |
| 221 | + function expand( $contextNode, $deps ) { |
| 222 | + if ( is_string( $contextNode ) ) { |
| 223 | + return $this->newLiteral( $contextNode ); |
| 224 | + } elseif ( $contextNode instanceof PPNode_Hash_Array ) { |
| 225 | + $items = array(); |
| 226 | + for ( $i = 0; $i < $contextNode->getLength(); $i++ ) { |
| 227 | + $items[] = $this->expand( $contextNode->item( $i ), $deps ); |
| 228 | + } |
| 229 | + return $this->newConcat( $items ); |
| 230 | + } elseif ( $contextNode instanceof PPNode_Hash_Attr ) { |
| 231 | + // No output |
| 232 | + return $this->newLiteral( '' ); |
| 233 | + } elseif ( $contextNode instanceof PPNode_Hash_Text ) { |
| 234 | + // String literal |
| 235 | + return $this->newLiteral( $contextNode->value ); |
| 236 | + } elseif ( $contextNode instanceof PPNode_Hash_Tree ) { |
| 237 | + if ( $contextNode->name == 'template' ) { |
| 238 | + // Convert template to function call |
| 239 | + $bits = $contextNode->splitTemplate(); |
| 240 | + return $this->expandTemplate( $bits, $deps ); |
| 241 | + } elseif ( $contextNode->name == 'tplarg' ) { |
| 242 | + $bits = $contextNode->splitTemplate(); |
| 243 | + return $this->expandTemplateArg( $bits, $deps ); |
| 244 | + } elseif ( $contextNode->name == 'comment' ) { |
| 245 | + return $this->newLiteral( '' ); |
| 246 | + } elseif ( $contextNode->name == 'ignore' ) { |
| 247 | + return $this->newLiteral( '' ); |
| 248 | + } else { |
| 249 | + $items = array(); |
| 250 | + for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) { |
| 251 | + $items[] = $this->expand( $node, $deps ); |
| 252 | + } |
| 253 | + return $this->newConcat( $items ); |
| 254 | + } |
| 255 | + } else { |
| 256 | + throw new MWException( __METHOD__.': invalid node type' ); |
| 257 | + } |
| 258 | + } |
| 259 | + |
| 260 | + function expandTemplate( $bits, $deps ) { |
| 261 | + global $wgContLang; |
| 262 | + |
| 263 | + $args = $bits['parts']; |
| 264 | + $part1Expr = $this->expand( $bits['title'], $deps ); |
| 265 | + $functionName = false; |
| 266 | + $part1Literal = false; |
| 267 | + $invalidTitle = false; |
| 268 | + |
| 269 | + if ( $part1Expr->op === 'concat' && $part1Expr->args[0]->op === 'literal' ) { |
| 270 | + $part1LeadString = $part1Expr->args[0]->args[0]; |
| 271 | + $colonPos = strpos( $part1LeadString, ':' ); |
| 272 | + if ( $colonPos !== false ) { |
| 273 | + $functionName = ltrim( substr( $part1LeadString, 0, $colonPos ) ); |
| 274 | + $arg1Expr = clone $part1Expr; |
| 275 | + if ( $colonPos == strlen( $part1LeadString ) - 1 ) { |
| 276 | + array_shift( $arg1Expr->args ); |
| 277 | + } else { |
| 278 | + $arg1Expr->args[0] = $this->newLiteral( |
| 279 | + substr( $part1LeadString, $colonPos + 1 ) ); |
| 280 | + } |
| 281 | + } |
| 282 | + if ( preg_match( Title::getTitleInvalidRegex(), ltrim( $part1LeadString ) ) ) { |
| 283 | + $invalidTitle = true; |
| 284 | + } |
| 285 | + } elseif ( $part1Expr->op === 'literal' ) { |
| 286 | + $part1Literal = $part1Expr->args[0]; |
| 287 | + $colonPos = strpos( $part1Literal, ':' ); |
| 288 | + if ( $colonPos !== false ) { |
| 289 | + $functionName = substr( $part1Literal, 0, $colonPos ); |
| 290 | + $arg1Expr = $this->newLiteral( substr( $part1Literal, $colonPos + 1 ) ); |
| 291 | + } |
| 292 | + } |
| 293 | + |
| 294 | + if ( $functionName !== false ) { |
| 295 | + if ( isset( $this->parser->mFunctionSynonyms[1][$functionName] ) ) { |
| 296 | + $funcWordId = $this->parser->mFunctionSynonyms[1][$functionName]; |
| 297 | + } else { |
| 298 | + $functionName = $wgContLang->lc( $functionName ); |
| 299 | + if ( isset( $this->parser->mFunctionSynonyms[0][$functionName] ) ) { |
| 300 | + $funcWordId = $this->parser->mFunctionSynonyms[0][$functionName]; |
| 301 | + } else { |
| 302 | + $funcWordId = false; |
| 303 | + } |
| 304 | + } |
| 305 | + if ( $funcWordId !== false ) { |
| 306 | + $funcLuaId = $this->parserFunctionToIdentifier( $funcWordId ); |
| 307 | + $funcArgs = array( $arg1Expr ); |
| 308 | + for ( $i = 0; $i < $args->getLength(); $i++ ) { |
| 309 | + $funcArgs[] = $this->expand( $args->item( $i ), $deps ); |
| 310 | + } |
| 311 | + return $this->newParserFunctionCall( $funcLuaId, $funcArgs, $deps ); |
| 312 | + } |
| 313 | + } |
| 314 | + |
| 315 | + if ( $part1Literal !== false ) { |
| 316 | + $title = Title::newFromText( $part1Literal, NS_TEMPLATE ); |
| 317 | + if ( $title ) { |
| 318 | + // Register the template in $deps for later expansion |
| 319 | + $deps->addTemplate( $title->getPrefixedDBkey() ); |
| 320 | + $fname = $this->titleToIdentifier( $title ); |
| 321 | + } |
| 322 | + } elseif ( $invalidTitle ) { |
| 323 | + $title = false; |
| 324 | + } else { |
| 325 | + $title = Title::newFromText( 'dynamic' ); |
| 326 | + } |
| 327 | + |
| 328 | + if ( !$title ) { |
| 329 | + // Invalid title |
| 330 | + $tplFrame = $this->preprocessor->newFrame(); |
| 331 | + $origNode = $tplFrame->virtualBracketedImplode( |
| 332 | + '{{', '|', '}}', $bits['title'], $args ); |
| 333 | + return $this->expand( $origNode, $deps ); |
| 334 | + } |
| 335 | + |
| 336 | + // Create a call to the template function |
| 337 | + $parentTplFrame = $this->preprocessor->newFrame(); |
| 338 | + $tplFrame = $parentTplFrame->newChild( $args, $title ); |
| 339 | + |
| 340 | + $templateArgs = array(); |
| 341 | + foreach ( $tplFrame->numberedArgs as $i => $arg ) { |
| 342 | + $expr = $this->newTrim( $this->expand( $arg, $deps ), $deps ); |
| 343 | + $templateArgs[$i] = $expr; |
| 344 | + } |
| 345 | + foreach ( $tplFrame->namedArgs as $name => $arg ) { |
| 346 | + $expr = $this->expand( $arg, $deps ); |
| 347 | + $templateArgs[$name] = $expr; |
| 348 | + } |
| 349 | + |
| 350 | + if ( $part1Literal === false ) { |
| 351 | + // Do a dynamic call |
| 352 | + $funcArgs = array( |
| 353 | + $this->newLiteral( 'mw_dynamic_template' ), |
| 354 | + $this->newTrim( $part1Expr, $deps ), |
| 355 | + $this->newHash( $templateArgs ), |
| 356 | + ); |
| 357 | + return $this->newExpression( 'call', $funcArgs ); |
| 358 | + } else { |
| 359 | + // Do a regular call |
| 360 | + $funcArgs = array( |
| 361 | + $this->newLiteral( $fname ), |
| 362 | + $this->newHash( $templateArgs ), |
| 363 | + ); |
| 364 | + |
| 365 | + return $this->newExpression( 'call', $funcArgs ); |
| 366 | + } |
| 367 | + } |
| 368 | + |
| 369 | + function expandTemplateArg( $piece, $deps ) { |
| 370 | + $parts = $piece['parts']; |
| 371 | + $nameExpr = $this->newTrim( $this->expand( $piece['title'], $deps ), $deps ); |
| 372 | + |
| 373 | + // Use numeric index if possible |
| 374 | + if ( $nameExpr->op == 'literal' |
| 375 | + && preg_match( '/^[0-9]$/', $nameExpr->args[0] ) ) |
| 376 | + { |
| 377 | + $nameExpr = $this->newLiteral( intval( $nameExpr->args[0] ) ); |
| 378 | + } |
| 379 | + |
| 380 | + if ( $parts->getLength() > 0 ) { |
| 381 | + $defaultExpr = $this->expand( $parts->item( 0 )->getChildren(), $deps ); |
| 382 | + } else { |
| 383 | + $tplFrame = $this->preprocessor->newFrame(); |
| 384 | + $defaultNode = $tplFrame->virtualBracketedImplode( '{{{', '|', '}}}', $piece['title'], $parts ); |
| 385 | + $defaultExpr = $this->expand( $defaultNode, $deps ); |
| 386 | + } |
| 387 | + |
| 388 | + $argsExpr = $this->newExpression( 'args', array() ); |
| 389 | + $argExpr = $this->newExpression( 'get_element', array( $argsExpr, $nameExpr ) ); |
| 390 | + $existsExpr = $this->newExpression( 'key_exists', array( $argsExpr, $nameExpr ) ); |
| 391 | + return $this->newExpression( 'if', array( |
| 392 | + $existsExpr, |
| 393 | + $argExpr, |
| 394 | + $defaultExpr |
| 395 | + ) ); |
| 396 | + } |
| 397 | + |
| 398 | + function pfunc_if( $args, $deps ) { |
| 399 | + if ( isset( $args[0] ) ) { |
| 400 | + $condition = $this->newExpression( 'equals', array( |
| 401 | + $this->newTrim( $args[0], $deps ), |
| 402 | + $this->newLiteral( '' ) ) ); |
| 403 | + } else { |
| 404 | + $condition = $this->newLiteral( false ); |
| 405 | + } |
| 406 | + if ( isset( $args[1] ) ) { |
| 407 | + $trueResult = $this->newTrim( $args[1], $deps ); |
| 408 | + } else { |
| 409 | + $trueResult = $this->newLiteral( '' ); |
| 410 | + } |
| 411 | + if ( isset( $args[2] ) ) { |
| 412 | + $falseResult = $this->newTrim( $args[2], $deps ); |
| 413 | + } else { |
| 414 | + $falseResult = $this->newLiteral( '' ); |
| 415 | + } |
| 416 | + return $this->newExpression( 'if', array( |
| 417 | + $condition, |
| 418 | + $trueResult, |
| 419 | + $falseResult |
| 420 | + ) ); |
| 421 | + } |
| 422 | +} |
| 423 | + |
| 424 | +abstract class LuaFoo_Converter_Language { |
| 425 | + var $converter; |
| 426 | + var $runtimeBlocks; |
| 427 | + |
| 428 | + function __construct( $converter ) { |
| 429 | + $this->converter = $converter; |
| 430 | + } |
| 431 | + |
| 432 | + function newBlock() { |
| 433 | + return new LuaFoo_Converter_Block( $this ); |
| 434 | + } |
| 435 | + |
| 436 | + function newFrame( $seed ) { |
| 437 | + return new LuaFoo_Converter_Frame( $this, $seed ); |
| 438 | + } |
| 439 | + |
| 440 | + function getRuntimeBlock( $name ) { |
| 441 | + if ( $this->runtimeBlocks === null ) { |
| 442 | + $this->setupRuntime(); |
| 443 | + } |
| 444 | + if ( isset( $this->runtimeBlocks[$name] ) ) { |
| 445 | + return $this->runtimeBlocks[$name]; |
| 446 | + } else { |
| 447 | + return $this->getRuntimeUnimplementedBlock( $name ); |
| 448 | + } |
| 449 | + } |
| 450 | + |
| 451 | + abstract public function makeFunction( $name, $argNames, $expression ); |
| 452 | + abstract public function getKeywords(); |
| 453 | + abstract protected function expressionToBlock( $expression, $frame ); |
| 454 | + abstract protected function setupRuntime(); |
| 455 | + abstract protected function getRuntimeUnimplementedBlock( $name ); |
| 456 | +} |
| 457 | + |
| 458 | +class LuaFoo_Converter_Lua extends LuaFoo_Converter_Language { |
| 459 | + var $binaryPrecedence = array( |
| 460 | + 'key_exists' => 10, // produces ~= |
| 461 | + 'equals' => 10, |
| 462 | + 'concat' => 20, |
| 463 | + ); |
| 464 | + |
| 465 | + var $keywords = array( |
| 466 | + 'and', |
| 467 | + 'break', |
| 468 | + 'do', |
| 469 | + 'else', |
| 470 | + 'elseif', |
| 471 | + 'end', |
| 472 | + 'false', |
| 473 | + 'for', |
| 474 | + 'function', |
| 475 | + 'if', |
| 476 | + 'in', |
| 477 | + 'local', |
| 478 | + 'nil', |
| 479 | + 'not', |
| 480 | + 'or', |
| 481 | + 'repeat', |
| 482 | + 'return', |
| 483 | + 'then', |
| 484 | + 'true', |
| 485 | + 'until', |
| 486 | + 'while' |
| 487 | + ); |
| 488 | + |
| 489 | + public function makeFunction( $name, $argNames, $expression ) { |
| 490 | + $frame = $this->newFrame( $name ); |
| 491 | + $block = $this->expressionToBlock( $expression, $frame ); |
| 492 | + $this->addLocalDeclarations( $block, $frame ); |
| 493 | + $argsString = implode( ', ', $argNames ); |
| 494 | + $func = $this->newBlock(); |
| 495 | + $func->addItems( array( |
| 496 | + "function $name($argsString)", |
| 497 | + $block->indent() |
| 498 | + ) ); |
| 499 | + $func->addBlock( $block ); |
| 500 | + $func->addItems( array( |
| 501 | + "return {$block->exprString}", |
| 502 | + $block->unindent(), |
| 503 | + "end" |
| 504 | + ) ); |
| 505 | + return $func; |
| 506 | + } |
| 507 | + |
| 508 | + public function getKeywords() { |
| 509 | + return $this->keywords; |
| 510 | + } |
| 511 | + |
| 512 | + protected function expressionToBlock( $expression, $frame ) { |
| 513 | + if ( !($expression instanceof LuaFoo_Converter_Expression ) ) { |
| 514 | + throw new MWException( 'Non-expression passed to ' . __METHOD__ ); |
| 515 | + } |
| 516 | + $block = $this->newBlock(); |
| 517 | + |
| 518 | + if ( $expression->op == 'literal' ) { |
| 519 | + $block->setExpressionString( $this->encodeLiteral( $expression->args[0] ) ); |
| 520 | + return $block; |
| 521 | + } |
| 522 | + |
| 523 | + $var = $frame->getVariable( $expression ); |
| 524 | + if ( $var !== false ) { |
| 525 | + $block->setExpressionString( $var ); |
| 526 | + return $block; |
| 527 | + } |
| 528 | + |
| 529 | + $args = array(); |
| 530 | + foreach ( $expression->args as $i => $arg ) { |
| 531 | + $argBlock = $this->expressionToBlock( $arg, $frame ); |
| 532 | + $block->addBlock( $argBlock ); |
| 533 | + $s = $argBlock->exprString; |
| 534 | + if ( $this->needsBrackets( $expression, $arg ) ) { |
| 535 | + $s = "($s)"; |
| 536 | + } |
| 537 | + $args[$i] = $s; |
| 538 | + } |
| 539 | + |
| 540 | + switch( $expression->op ) { |
| 541 | + case 'concat': |
| 542 | + $block->setExpressionString( implode( ' .. ', $args ) ); |
| 543 | + break; |
| 544 | + case 'equals': |
| 545 | + $block->setExpressionString( "{$args[0]} == {$args[1]}" ); |
| 546 | + break; |
| 547 | + case 'if': |
| 548 | + $resultVar = $frame->newVariable(); |
| 549 | + $frame->setVariableHash( $resultVar, $expression ); |
| 550 | + $block->setExpressionString( $resultVar ); |
| 551 | + $block->addItems( array( |
| 552 | + "if {$args[0]} then", |
| 553 | + $block->indent(), |
| 554 | + "$resultVar = {$args[1]}", |
| 555 | + $block->unindent(), |
| 556 | + "else", |
| 557 | + $block->indent(), |
| 558 | + "$resultVar = {$args[2]}", |
| 559 | + $block->unindent(), |
| 560 | + "end" ) ); |
| 561 | + break; |
| 562 | + case 'call': |
| 563 | + $name = "_G[{$args[0]}]"; |
| 564 | + if ( $expression->args[0]->op === 'literal' ) { |
| 565 | + $literalValue = $expression->args[0]->args[0]; |
| 566 | + if ( preg_match( '/^[\w\177-\377]*$/', $literalValue ) ) { |
| 567 | + $name = $literalValue; |
| 568 | + } |
| 569 | + } |
| 570 | + if ( count( $args ) === 2 && $expression->args[1]->op === 'hash' ) { |
| 571 | + // This is a nice little lua feature, a short syntax for |
| 572 | + // calling a function with a single table as a parameter |
| 573 | + $block->setExpressionString( "$name{$args[1]}" ); |
| 574 | + } else { |
| 575 | + unset( $args[0] ); |
| 576 | + $funcArgs = implode( ', ', $args ); |
| 577 | + $block->setExpressionString( "$name($funcArgs)" ); |
| 578 | + } |
| 579 | + break; |
| 580 | + case 'key_exists': |
| 581 | + $array = $args[0]; |
| 582 | + $key = $args[1]; |
| 583 | + $block->setExpressionString( "{$array}[{$key}] ~= nil" ); |
| 584 | + break; |
| 585 | + case 'get_element': |
| 586 | + $array = $args[0]; |
| 587 | + $key = $args[1]; |
| 588 | + $block->setExpressionString( "{$array}[{$key}]" ); |
| 589 | + break; |
| 590 | + case 'args': |
| 591 | + $block->setExpressionString( 'args' ); |
| 592 | + break; |
| 593 | + case 'void': |
| 594 | + $block->setExpressionString( '' ); |
| 595 | + break; |
| 596 | + case 'hash': |
| 597 | + $n = count( $args ) / 2; |
| 598 | + $s = '{'; |
| 599 | + for ( $i = 0; $i < $n; $i++ ) { |
| 600 | + if ( $expression->args[$i]->op === 'literal' |
| 601 | + && $this->isName( $expression->args[$i]->args[0] ) ) |
| 602 | + { |
| 603 | + $encKey = $expression->args[$i]->args[0]; |
| 604 | + } else { |
| 605 | + $encKey = '[' . $args[$i] . ']'; |
| 606 | + } |
| 607 | + $value = $args[$i + $n]; |
| 608 | + if ( $s !== '{' ) { |
| 609 | + $s .= ', '; |
| 610 | + } |
| 611 | + $s .= "$encKey = $value"; |
| 612 | + } |
| 613 | + $s .= '}'; |
| 614 | + $block->setExpressionString( $s ); |
| 615 | + break; |
| 616 | + } |
| 617 | + return $block; |
| 618 | + } |
| 619 | + |
| 620 | + public function isName( $name ) { |
| 621 | + return !in_array( $name, $this->keywords ) |
| 622 | + && preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name ); |
| 623 | + } |
| 624 | + |
| 625 | + protected function encodeLiteral( $input ) { |
| 626 | + if ( is_string( $input ) ) { |
| 627 | + $result = strtr( $input, array( |
| 628 | + "\\" => "\\\\", |
| 629 | + "\n" => '\n', |
| 630 | + "\0" => '\0', |
| 631 | + "'" => "\\'" |
| 632 | + ) ); |
| 633 | + return "'$result'"; |
| 634 | + } elseif ( is_bool( $input ) ) { |
| 635 | + return $input ? 'true' : 'false'; |
| 636 | + } elseif ( is_numeric( $input ) ) { |
| 637 | + return floatval( $input ); |
| 638 | + } elseif ( is_array( $input ) ) { |
| 639 | + $result = ''; |
| 640 | + foreach ( $input as $key => $value ) { |
| 641 | + if ( is_string( $key ) && $this->isName( $key ) ) { |
| 642 | + $encKey = $key; |
| 643 | + } else { |
| 644 | + $encKey = '[' . $this->encodeLiteral( $key ) . ']'; |
| 645 | + } |
| 646 | + $encValue = $this->encodeLiteral( $value ); |
| 647 | + if ( $result !== '' ) { |
| 648 | + $result .= ', '; |
| 649 | + } |
| 650 | + $result .= "$encKey = $encValue"; |
| 651 | + } |
| 652 | + return "\{$result\}"; |
| 653 | + } else { |
| 654 | + throw new MWException( __METHOD__.': invalid type' ); |
| 655 | + } |
| 656 | + } |
| 657 | + |
| 658 | + protected function needsBrackets( $context, $arg ) { |
| 659 | + if ( isset( $this->binaryPrecedence[ $context->op ] ) |
| 660 | + && isset( $this->binaryPrecedence[ $arg->op ] ) ) |
| 661 | + { |
| 662 | + $contextLevel = $this->binaryPrecedence[ $context->op ]; |
| 663 | + $argLevel = $this->binaryPrecedence[ $arg->op ]; |
| 664 | + if ( $contextLevel == $argLevel ) { |
| 665 | + if ( $context->op === 'concat' ) { |
| 666 | + // Right associative |
| 667 | + return false; |
| 668 | + } else { |
| 669 | + // Left associative |
| 670 | + return true; |
| 671 | + } |
| 672 | + } else { |
| 673 | + return $contextLevel > $argLevel; |
| 674 | + } |
| 675 | + } |
| 676 | + return false; |
| 677 | + } |
| 678 | + |
| 679 | + protected function setupRuntime() { |
| 680 | + $file = fopen( dirname( __FILE__ ) . '/ConverterRuntime.lua', 'r' ); |
| 681 | + $currentFunction = false; |
| 682 | + $currentBlock = $this->newBlock(); |
| 683 | + $currentIndent = 0; |
| 684 | + while ( true ) { |
| 685 | + $line = fgets( $file ); |
| 686 | + $newFunction = false; |
| 687 | + if ( $line !== false ) { |
| 688 | + $line = rtrim( $line, "\r\n" ); |
| 689 | + $indent = strspn( $line, "\t" ); |
| 690 | + if ( $indent > $currentIndent ) { |
| 691 | + $currentBlock->addItem( $currentBlock->indent() ); |
| 692 | + } elseif ( $indent < $currentIndent ) { |
| 693 | + $currentBlock->addItem( $currentBlock->unindent() ); |
| 694 | + } |
| 695 | + $currentIndent = $indent; |
| 696 | + if ( $indent ) { |
| 697 | + $line = substr( $line, $indent ); |
| 698 | + } |
| 699 | + |
| 700 | + if ( preg_match( '/^function (\w+)/', $line, $m ) ) { |
| 701 | + $newFunction = $m[1]; |
| 702 | + } |
| 703 | + } |
| 704 | + |
| 705 | + if ( $line === false || $newFunction !== false ) { |
| 706 | + // Store the old function |
| 707 | + if ( $currentFunction !== false ) { |
| 708 | + $this->runtimeBlocks[$currentFunction] = $currentBlock; |
| 709 | + } |
| 710 | + |
| 711 | + // Start the new function |
| 712 | + $currentFunction = $newFunction; |
| 713 | + $currentBlock = $this->newBlock(); |
| 714 | + } |
| 715 | + |
| 716 | + if ( $line === false ) { |
| 717 | + break; |
| 718 | + } |
| 719 | + |
| 720 | + $currentBlock->addItem( $line ); |
| 721 | + } |
| 722 | + } |
| 723 | + |
| 724 | + protected function getRuntimeUnimplementedBlock( $name ) { |
| 725 | + $block = $this->newBlock(); |
| 726 | + $block->addItems( array( |
| 727 | + "function $name()", |
| 728 | + $block->indent(), |
| 729 | + "error(\"This function is not implemented\")", |
| 730 | + $block->unindent(), |
| 731 | + "end" |
| 732 | + ) ); |
| 733 | + return $block; |
| 734 | + } |
| 735 | + |
| 736 | + protected function addLocalDeclarations( $block, $frame ) { |
| 737 | + foreach ( $frame->variables as $var => $dummy ) { |
| 738 | + $block->prependItem( "local $var" ); |
| 739 | + } |
| 740 | + } |
| 741 | +} |
| 742 | + |
| 743 | +class LuaFoo_Converter_Block { |
| 744 | + var $items = array(), $exprString = '', $language; |
| 745 | + |
| 746 | + function __construct( $language ) { |
| 747 | + $this->language = $language; |
| 748 | + } |
| 749 | + |
| 750 | + function addItems() { |
| 751 | + $args = func_get_args(); |
| 752 | + foreach ( $args as $items ) { |
| 753 | + $this->items = array_merge( $this->items, $items ); |
| 754 | + } |
| 755 | + } |
| 756 | + |
| 757 | + function prependItem( $item ) { |
| 758 | + array_unshift( $this->items, $item ); |
| 759 | + } |
| 760 | + |
| 761 | + function addItem( $item ) { |
| 762 | + $this->items[] = $item; |
| 763 | + } |
| 764 | + |
| 765 | + function addBlock( $block ) { |
| 766 | + $this->addItems( $block->items ); |
| 767 | + } |
| 768 | + |
| 769 | + function setExpressionString( $expr ) { |
| 770 | + $this->exprString = $expr; |
| 771 | + } |
| 772 | + |
| 773 | + function toString() { |
| 774 | + $out = ''; |
| 775 | + $indentLevel = 0; |
| 776 | + foreach ( $this->items as $item ) { |
| 777 | + if ( $item instanceof LuaFoo_Converter_Indent ) { |
| 778 | + $indentLevel++; |
| 779 | + } elseif ( $item instanceof LuaFoo_Converter_Unindent ) { |
| 780 | + $indentLevel--; |
| 781 | + } else { |
| 782 | + $out .= str_repeat( $this->language->converter->tab, $indentLevel ) . $item . "\n"; |
| 783 | + } |
| 784 | + } |
| 785 | + return $out; |
| 786 | + } |
| 787 | + |
| 788 | + function indent() { |
| 789 | + return new LuaFoo_Converter_Indent; |
| 790 | + } |
| 791 | + |
| 792 | + function unindent() { |
| 793 | + return new LuaFoo_Converter_Unindent; |
| 794 | + } |
| 795 | +} |
| 796 | + |
| 797 | +class LuaFoo_Converter_Indent {} |
| 798 | + |
| 799 | +class LuaFoo_Converter_Unindent {} |
| 800 | + |
| 801 | +class LuaFoo_Converter_Expression { |
| 802 | + var $op, $args, $hash; |
| 803 | + |
| 804 | + function __construct( $op, $args ) { |
| 805 | + $this->op = $op; |
| 806 | + $this->args = $args; |
| 807 | + } |
| 808 | + |
| 809 | + function getHash() { |
| 810 | + if ( $this->hash === null ) { |
| 811 | + $s = $this->op; |
| 812 | + foreach ( $this->args as $arg ) { |
| 813 | + if ( $arg instanceof LuaFoo_Converter_Expression ) { |
| 814 | + $s .= ':' . $arg->getHash(); |
| 815 | + } else { |
| 816 | + $s .= ':' . sha1( serialize( $arg ) ); |
| 817 | + } |
| 818 | + } |
| 819 | + $this->hash = sha1( $s ); |
| 820 | + } |
| 821 | + return $this->hash; |
| 822 | + } |
| 823 | +} |
| 824 | + |
| 825 | +class LuaFoo_Converter_Frame { |
| 826 | + static $consonants = 'bdfghjklmnprstvwz'; |
| 827 | + static $vowels = 'aeiou'; |
| 828 | + |
| 829 | + var $variables = array(); |
| 830 | + var $language; |
| 831 | + var $variablesByHash = array(); |
| 832 | + |
| 833 | + function __construct( $language, $seed ) { |
| 834 | + $this->language = $language; |
| 835 | + srand( crc32( $seed ) ); |
| 836 | + } |
| 837 | + |
| 838 | + public function newVariable() { |
| 839 | + do { |
| 840 | + $length = rand( 5, 8 ); |
| 841 | + $s = ''; |
| 842 | + for ( $i = 0; $i < $length; $i++ ) { |
| 843 | + if ( $i % 2 ) { |
| 844 | + $s .= $this->getRandomVowel(); |
| 845 | + } else { |
| 846 | + $s .= $this->getRandomConsonant(); |
| 847 | + } |
| 848 | + } |
| 849 | + } while ( isset( $this->variables[$s] ) ); |
| 850 | + |
| 851 | + $this->variables[$s] = true; |
| 852 | + |
| 853 | + return $s; |
| 854 | + } |
| 855 | + |
| 856 | + public function getVariable( $expr ) { |
| 857 | + $hash = $expr->getHash(); |
| 858 | + if ( isset( $this->variablesByHash[$hash] ) ) { |
| 859 | + return $this->variablesByHash[$hash]; |
| 860 | + } else { |
| 861 | + return false; |
| 862 | + } |
| 863 | + } |
| 864 | + |
| 865 | + public function setVariableHash( $var, $expr ) { |
| 866 | + $hash = $expr->getHash(); |
| 867 | + $this->variablesByHash[$hash] = $var; |
| 868 | + } |
| 869 | + |
| 870 | + protected function getRandomVowel() { |
| 871 | + return self::$vowels[ rand( 0, strlen( self::$vowels ) - 1 ) ]; |
| 872 | + } |
| 873 | + |
| 874 | + protected function getRandomConsonant() { |
| 875 | + return self::$consonants[ rand( 0, strlen( self::$consonants ) - 1 ) ]; |
| 876 | + } |
| 877 | +} |
| 878 | + |
| 879 | +class LuaFoo_Converter_Deps { |
| 880 | + var $titleTexts = array(); |
| 881 | + var $functions = array(); |
| 882 | + |
| 883 | + function addTemplate( $t ) { |
| 884 | + $this->titleTexts[$t] = true; |
| 885 | + } |
| 886 | + |
| 887 | + function addFunction( $name ) { |
| 888 | + $this->functions[$name] = true; |
| 889 | + } |
| 890 | + |
| 891 | + function getTemplates() { |
| 892 | + return array_keys( $this->titleTexts ); |
| 893 | + } |
| 894 | + |
| 895 | + function getFunctions() { |
| 896 | + return array_keys( $this->functions ); |
| 897 | + } |
| 898 | +} |
Property changes on: trunk/extensions/LuaFoo/includes/Converter.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 899 | + native |
Index: trunk/extensions/LuaFoo/LuaFoo.i18n.php |
— | — | @@ -0,0 +1,8 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +$messages = array(); |
| 5 | +$messages['en'] = array( |
| 6 | + 'luafoo-luatranslation' => 'Translate template to Lua', |
| 7 | + 'luafoo-convert-title' => 'Template to convert to Lua:', |
| 8 | + 'luafoo-convert-submit' => 'Convert', |
| 9 | +); |
Property changes on: trunk/extensions/LuaFoo/LuaFoo.i18n.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 10 | + native |
Index: trunk/extensions/LuaFoo/LuaFoo.php |
— | — | @@ -0,0 +1,13 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 5 | + exit( 1 ); |
| 6 | +} |
| 7 | + |
| 8 | +$lfip = dirname( __FILE__ ); |
| 9 | +$wgAutoloadClasses['LuaFoo_SpecialLuaTranslation'] = "$lfip/includes/SpecialLuaTranslation.php"; |
| 10 | +$wgAutoloadClasses['LuaFoo_Converter'] = "$lfip/includes/Converter.php"; |
| 11 | + |
| 12 | +$wgSpecialPages['LuaTranslation'] = 'LuaFoo_SpecialLuaTranslation'; |
| 13 | +$wgExtensionMessagesFiles['LuaFoo'] = "$lfip/LuaFoo.i18n.php"; |
| 14 | + |
Property changes on: trunk/extensions/LuaFoo/LuaFoo.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 15 | + native |