r95836 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r95835‎ | r95836 | r95837 >
Date:06:50, 31 August 2011
Author:tstarling
Status:deferred
Tags:
Comment:
An extension to hold my Lua/MediaWiki integration work. Currently it just has a little converter which converts a wikitext template to Lua, with abstractions suitable for adding more output languages. Some day it may be useful for comparative benchmarking or perhaps even migration. For the time being, it is just a proof of concept.
Modified paths:
  • /trunk/extensions/LuaFoo (added) (history)
  • /trunk/extensions/LuaFoo/LuaFoo.i18n.php (added) (history)
  • /trunk/extensions/LuaFoo/LuaFoo.php (added) (history)
  • /trunk/extensions/LuaFoo/includes (added) (history)
  • /trunk/extensions/LuaFoo/includes/Converter.php (added) (history)
  • /trunk/extensions/LuaFoo/includes/ConverterRuntime.lua (added) (history)
  • /trunk/extensions/LuaFoo/includes/SpecialLuaTranslation.php (added) (history)

Diff [purge]

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
142 + 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
1899 + 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
110 + 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
115 + native

Status & tagging log