W3TPL/archive/code

From Woozle Writes Code
< W3TPL‎ | archive
Jump to navigation Jump to search

Preface

The source code for this will soon be moved to GitHub. It's got a lot of rough edges, but it is definitely usable -- and other people could be working on the rough edges. --Woozle 10:13, 25 July 2011 (EDT)

PHP

/*
 HISTORY:
	0.01 (Wzl) Mainly proof-of-concept;
	0.02 (Wzl) Kluge to let <xploop> pull #var value under MW <1.12
 	0.03 (Wzl) <func> and related tags seem to be working
	0.04 (Wzl) Some debugging; now works with v1.12 {{#tag}} function and template parameters (but not very well)
		names are always lowercased because sometimes the parser does it for you
		names are always trimmed, because sometimes the parser includes extra spaces
	0.05 (Wzl) Added variable indirection ($); removed now-redundant "namer" attribute from <let>
		can we do something similar with pre-parsing? (i.e. a character to indicate the need for it -- @(stuff to parse))
		<if> can now make more sense, i.e. using actual values instead of assuming variable names
	0.06 (Wzl) xploop now using variable indirection; removed listvar parameter
	0.07 (Wzl) Execution trace in <dump>
	0.08 (Wzl) <trace> to set trace options; "input" option in <call>
	0.09 (Wzl) <echo> tag; <call> does not output its contents
	0.10 (Wzl) Code runs ok; still writing <w3tpl> tag
	0.11 (Wzl) Added detection of page-protection, and "raw" attribute for <echo>
	0.12 (Wzl) Fixed minor incompatibilities with MW 1.10
	0.13 (Wzl) <for> tag seems to be working, at least for simple stuff
	0.14 (Wzl) <load> tag fails gracefully when title doesn't exist
	0.15 (Wzl) Support for brackets in variable names, to reference arrays
	0.16 (Wzl) No SQL on unprotected pages; "limit" attribute for retrieving partial data
	0.17 (Wzl) Removed some undefined-var warnings which pop up under certain mysterious circumstances
	0.18 (Wzl) Lots of under-the-hood changes; var names are now parsed by the class clsW3VarName, which may later get renamed to clsW3Var and integrated with the full-blown parser to be written. There's still some ambiguity about the exact circumstances under which <let> overwrites existing data and when it operates on the variable's current value.
	0.19 (Wzl) $wgW3TPLSettings['raw-ok'] now allows raw output even if page not protected
	0.20 (Wzl) added "pre" as a deprecated alias for "parse", for backwards compatibility
	0.21 (Wzl) merged changes from other version 0.19 (accidental fork):
		"user.name" and "user.dbkey" system data
	0.22 (Wzl) user.can.[action]
	0.23 (Wzl) added "file=" attribute to <dump> tag; $wgW3_opt_fpLogs
	0.24 (Wzl) wrote code for access to non-MW dbs via "db=" in <for> tag
	0.25 (Wzl) patch for pre-5.3.0 PHP; @title.url
	0.26 (Wzl) 'vars' option for <let> copied from <echo>, but it doesn't seem to work
	0.27 (Wzl) @title.full
	0.30 (Wzl) removed all echo-level tracking; added "var" attribute to <echo>
	0.31 (Wzl)
	    @user.rights - returns marked list of current user's rights
	    "if (defined('__DIR__'))" properly invokes newer PHP constant if it's available
	0.?? 2009-02-13 (Wzl) $wgOptCP_SubstFinish "]" -> "$]" so links and other bracketed stuff don't confuse the var parser
	0.32 2009-04-02 (Wzl)
	    Fixed some warning messages in <for> (use of undeclared var $doTbl in certain circumstances)
	    Fixed bug where upper-cased things in sysvars (@) could not be accessed
	      (was: 2009-01-29 can't access upper-case-named POST variables)
	    "sql=" argument in <for>
	0.33 2009-04-11 (Wzl) - var names are now lowercased before being used as an index, but @sysdata names still are not
	0.34 2009-04-26 (Wzl) - @user.name, @user.email
 	0.35 2009-05-30 (wzl) - merged changes made in v0.33 of separate branch (merged on 2009-06-06)
	    Removed <for name=> parameter, field name is now @row's 2nd argument (not 3rd)
	    <for xps=> option to use xplodable string as list
	    <let oparse> option to parse value only on output (use with "echo" option)
	    @title.subject no longer includes subpages; added @title.name, which does
	0.36 2010-01-19 (Wzl) - minor bugfix in efW3For(): make sure $doDb is always defined
	0.37 2010-01-29 (Wzl) - another minor bugfix in efW3For(): set $out to NULL at start, in case there is no output
	0.38 2010-05-05 (Wzl) - variables in function names using "func=" parameter
	0.39 2010-07-25 (Wzl) - no functional changes; just sorted tag functions alphabetically
	0.40 2010-09-11 (Wzl)
	0.41 2011-05-05 (Wzl) split <for>'s row-display code off into a separate function, for external use (InstaGov)
	    fixed bug in <get arg>
	    added "tag" option to <echo> and <let>
	    deprecated @post and @query; added @http.get/post/req
	0.42 2011-06-01 (Wzl) added plus, minus, min, max, not operators to <let>
	0.43 2011-06-13 (Wzl) moved @sql to @db.sql
	0.44 2011-07-25 (Wzl) functions are now stored in page_props; <call> looks there if it can't find one already loaded
	0.45 2011-08-07 (Wzl) better handling of data.php; cleanup of function-search code
	0.46 2011-08-22 (Wzl) <call> now passes inter-tag text as final argument
	0.47 2011-09-18 (Wzl) belated merge of 0.46 with modified 0.44
	0.48 2011-10-16 (Wzl) adding plugins architecture, <exec> tag
	0.49 2012-01-20 (Wzl) tags returning NULL values cause problems in MW 1.18 (UNIQ-QINU stuff gets displayed)
	0.50 2012-03-18 (Wzl) now shows error message if plugin module does not have requested function
TO DO:
	An alternate <call> syntax might be good, where the function name and arguments are given explicitly:
	  <run func=funcName arg1=value arg2=value input=arg3>this will get passed as the value of arg3</run>
	  ...or possibly just define the function name as a tag for the shorter form: <funcName value1 value2>
	Functions should be able to return values
	Variables inside functions should be local by default; need a way to make them global as well if we do that
	<load> should do nothing by default
	  "parse[=var]" should tell it to parse the page's contents and place the results in var
	  "raw=var" should tell it to put the page's unparsed contents into var
	  "props=var" should tell it to load the page's page_props into array var[]
	  "smwprops=var" should tell it to load the page's Semantic MediaWiki properties into array var[]
 ELEMENTS:
	<hide>: Runs the parser on everything in between the tags, but doesn't display the result.
		Useful for doing a lot of "programmish" stuff, so you can format it nicely and comment it without messing up your display
	<let>, <get>: much like PageVars extension, but using XML tags instead of {{#parser}} functions
	<func>: defines a function which can be called with arguments later
		<arg>: optional method of passing arguments to a function
	<call>: call a previously defined function
	<dump>: show list of all variables (with values) and functions (with code)
	<if>, <else>: control structure
	<xploop list="\demarcated\list" repl=string-to-replace sep=separator></xploop>: Same as {{#xploop}}, but uses varname instead of $s$
	TO-DO <w3tpl></w3tpl>: The language itself
		parser should later be optimized for execution time by using PHP intrinsic string fx instead of PHP-code loop
*/


if (defined( '__DIR__' )) {
  $fpThis = __DIR__;
} else {
  $fpThis = dirname(__FILE__);
//  require_once('StringTemplate.php');	// can this be made load-on-demand? It's only used by <echo>
}
if (!defined('LIBMGR')) {
    require('libmgr.php');
}
clsLibMgr::Add('StringTemplate',	$fpThis.'/StringTemplate.php',__FILE__,__LINE__);
clsLibMgr::Add('strings',		$fpThis.'/strings.php',__FILE__,__LINE__);
clsLibMgr::Add('data',			$fpThis.'/data.php',__FILE__,__LINE__);

$wgAutoloadClasses['clsStringTemplate'] = clsLibMgr::Path('StringTemplate');
$wgAutoloadClasses['xtString'] = clsLibMgr::Path('strings');
$wgAutoloadClasses['clsDatabase'] = clsLibMgr::Path('data');
$wgOptCP_SubstStart = '[$';
$wgOptCP_SubstFinish = '$]';

// This is only necessary because of the SQLValue() and nzArr() utility functions:
clsLibMgr::Load('data',__FILE__,__LINE__);
// Utility functions in data.php ought to be in a separate file.

$w3step = FALSE;
$w3stop = FALSE;

$wgExtensionCredits['other'][] = array(
	'name' => 'W3TPL',
	'description' => 'Woozle\'s Wacky Wiki Text Processing Language',
	'author' => 'Woozle (Nick) Staddon',
	'url' => 'http://htyp.org/W3TPL', 
	'version' => '0.50 2012-03-18'
);

define('ksFuncInit','efW3TPLInit');

//Avoid unstubbing $wgParser on setHook() too early on modern (1.12+) MW versions, as per r35980
if ( defined( 'MW_SUPPORTS_PARSERFIRSTCALLINIT' ) ) {
        $wgHooks['ParserFirstCallInit'][] = ksFuncInit;
} else { // Otherwise do things the old fashioned way
        $wgExtensionFunctions[] = ksFuncInit;
}
$wgHooks['LanguageGetMagic'][] = 'efW3_LanguageGetMagic';
$wgHooks['ParserAfterTidy'][] = 'efW3_ParserAfterTidy';
$wgHooks['OutputPageBeforeHTML'][] = 'efW3_OutputPageBeforeHTML';

function efW3TPLInit() {
        global $wgParser;
	global $wgExtW3TPL;
	global $wgW3RawOk;

// hook in <tag>-style functions:
        $wgParser->setHook( 'arg',	'efW3Arg' );
        $wgParser->setHook( 'call',	'efW3Call' );
        $wgParser->setHook( 'class',	'efW3Class' );
        $wgParser->setHook( 'dump',	'efW3Dump' );
        $wgParser->setHook( 'echo',	'efW3Echo' );
        $wgParser->setHook( 'else',	'efW3Else' );
        $wgParser->setHook( 'exec',	'efW3Exec' );
        $wgParser->setHook( 'for',	'efW3For' );
        $wgParser->setHook( 'func',	'efW3Func' );
        $wgParser->setHook( 'get',	'efW3Get' );
        $wgParser->setHook( 'hide',	'efW3Hide' );
        $wgParser->setHook( 'if',	'efW3If' );
        $wgParser->setHook( 'let',	'efW3Let' );
        $wgParser->setHook( 'load',	'efW3Load' );
        $wgParser->setHook( 'save',	'efW3Save' );
        $wgParser->setHook( 'trace',	'efW3Trace' );
        $wgParser->setHook( 'w3tpl',	'efW3TPLRender' );
        $wgParser->setHook( 'xploop',	'efW3Xploop' );
        return true;
}
function efW3_LanguageGetMagic( &$magicWords, $langCode = "en" ) {
    switch ( $langCode ) {
        default:
            $magicWords['w3xploop']	= array ( 0, 'w3xploop' );
            $magicWords['w3xpcount']	= array ( 0, 'w3xpcount' );
    }
    return true;
}


function TrueFalse($iVar) {
	return $iVar?'TRUE':'FALSE';
}
function W3VarExists($iName) {
	global $wgW3Vars;

	$strName = strtolower($iName);
	return isset($wgW3Vars[$strName]);
}
function W3KillVar($iName) {
	global $wgW3Vars;

	$strName = strtolower($iName);
	unset($wgW3Vars[$strName]);
}
function W3SetVar($iName, $iValue, $iAppend = FALSE) {
	global $wgW3Vars, $wgW3_doTrace_vars;

	$strName = strtolower($iName);
	if ($iAppend && isset($wgW3Vars[$strName])) {
		$wgW3Vars[$strName] .= $iValue;
		if ($wgW3_doTrace_vars) {
			W3AddTrace(' $['.ShowHTML($strName).'] += ['.$iValue.'] => ['.$wgW3Vars[$strName].']');
		}
	} else {
		$wgW3Vars[$strName] = $iValue;
		if ($wgW3_doTrace_vars) {
			W3AddTrace(' $['.ShowHTML($strName).'] = ['.$iValue.']');
		}
	}
}
function W3GetSysData($iName) {
    global $wgTitle,$wgUser,$wgRequest;
    global $wgW3_doTrace_vars;
    global $wgW3_data;
    global $sql;

    $strName = $iName;
    $strParts =  explode('.', $strName);
    if (isset($strParts[1])) {
	$strParam = strtolower($strParts[1]);
    } else {
	$strParam = NULL;
    }
    switch ($strParts[0]) {
      case 'title':
	switch ($strParam) {
	  case 'id':
	    $out = $wgTitle->getArticleID();
	    break;
	  case 'full':	// namespace:subject
	    $out = $wgTitle->getPrefixedText();
	    break;
	  case 'subject':	// just the part after the namespace and before any slashes
	    $out = $wgTitle->getBaseText();
	    break;
	  case 'name':		// just the part after the namespace
	    $out = $wgTitle->getText();
	    break;
	  case 'url':
	    $out = $wgTitle->getFullURL();
	    break;
	  case 'dbkey':		// as stored in db tables
	    $out = $wgTitle->getDBkey();
	    break;
	}
	break;
      case 'row':
	$strFld = $strParts[1];	// field name
	$strSet = '@@row@@';

	$out = NULL;
// This is a horrible kluge necessitated by the 2 different ways of accessing data in <for>
	$val = nzArray($wgW3_data,$strSet);
	if (!is_null($val)) {
	    $vRow = $val;
	    if (is_array($vRow)) {
		if (array_key_exists($strFld,$vRow)) {
		    $out = $vRow[$strFld];
		} else {
		    $strErr = 'Dataset ['.$strSet.'] has no field named ['.$strFld.'].';
		    //throw new exception($strErr);
		    $out = '<b>Error</b>: '.$strErr;
		}
	    } else {
		if (isset($vRow->$strFld)) {
		    $out = $vRow->$strFld;
		}
	    }
	}
	W3AddTrace('DATA['.$strSet.']['.$strFld.'] => ['.$out.']');
	break;
      case 'mem':
	$out = memory_get_usage(TRUE);
	break;
      case 'user':
	switch ($strParam) {
	  case 'login':
	    $out = $wgUser->getName();
	    break;
	  case 'dbkey':
	    $out = $wgUser->getTitleKey();
	    break;
	  case 'id':
	    $out = $wgUser->getID();
	    break;
	  case 'can':
	    $out = $wgUser->isAllowed($strParts[2]);
	    break;
	  case 'rights':
	    $arrRights = $wgUser->getRights();
	    $out = '';
	    foreach ($arrRights as $key=>$val) {
		$out .= '\\'.$val;
	    }
	    break;
	  case 'email':
	    $out = $wgUser->getEmail();
	    break;
	  case 'name':
	    $out = $wgUser->getRealName();
	    break;
	  default:
	    $out = '?'.$strParam;
	}
	break;
      case 'http':
	$strName = nz($strParts[2]);
	switch ($strParam) {
	  case 'get':
	    if (empty($strName)) {
		    # TO DO: return raw query
	    } else {
		    //$out = $wgRequest->getText($strParam);
		    if (isset($_GET[$strName])) {
			$out = $_GET[$strName];
		    } else {
			$out = NULL;	// maybe this should be a list? or raw query, unparsed?
		    }
	    }
	    break;	
	  case 'post':
	    if ($strParam) {
		    if (isset($_POST[$strParam])) {
			    $out = $_POST[$strParam];
		    } else {
			    $out = NULL;	// maybe this should be a list?
		    }
	    }
	    W3AddTrace(' GET POST ['.ShowHTML($strParam).']: ['.$out.']');
	    break;
	  case 'req':
	    if ($strParam) {
		$out = $wgRequest->getVal($strName);
	    } else {
		$out = NULL;	// maybe this should be a list?
	    }
	    break;
	}
	break;
      case 'query':	// DEPRECATED -- same as http.get; eliminate eventually
	if ($strParam == '') {
		// never used
	} else {
	    //$out = $wgRequest->getText($strParam);
	    if (isset($_GET[$strParam])) {
		$out = $_GET[$strParam];
	    } else {
		$out = '';
	    }
	}
	break;	
      case 'post':	// DEPRECATED -- same as http.post; eliminate eventually
	if ($strParam) {
	    if (isset($_POST[$strParam])) {
		$out = $_POST[$strParam];
	    } else {
		$out = NULL;
	    }
	}
	W3AddTrace(' GET POST ['.ShowHTML($strParam).']: ['.$out.']');
	break;
      case 'env':
	  if ($strParam) {
		  if (isset($_ENV[$strParam])) {
			  $out = $_ENV[$strParam];
		  } else {
			  $out = NULL;
		  }
	  }
	  break;
      case 'db':
	switch ($strParam) {
	  case 'sql':
	    $out = $sql;
	    break;
	  default:
	    $out = 'unknown subtype for db: ['.$strParam.']';
	}
	break;
      /* This is mainly for debugging (later it may be useful for library maintenance), so I'm
	not going to try to make it forward-compatible with the changes I expect to make later,
	i.e. having functions as an object type.
	SYNTAX: @func.name.def|page
      */
      case 'func':
      global $wgW3_funcs;

	$fname = $strParam;
	$fobj = $wgW3_funcs[$fname];

	switch ($strParts[2]) {
	  case 'def':
	    $out = $fobj->dump();
	    break;
	  case 'page':
	    // not implemented yet
	    break;
	}
	break;
    }
    if ($wgW3_doTrace_vars) {
	    W3AddTrace(' GETSYSDATA-'.$strParts[0].' ['.ShowHTML($iName).']: ['.$out.']');
    }
    return $out;
}
/*----
  2011-09-19 this doesn't work; discontinuing it
function W3SetSysData($iName,$iValue) {
	global $wgOut;
	global $wgW3Trace_indent;

	W3AddTrace(' SETSYSDATA ['.$iName.':'.$iValue.']');
	$strName = $iName;
	$strParts =  explode('.', $strName);
	switch (strtolower($strParts[0])) {
	    case 'catg':
		$wgW3Trace_indent++;
		W3AddTrace('CATG');
		$wgW3Trace_indent--;
		$wgOut->mCategoryLinks = array();
		break;
	}
}
*/
function W3GetExpr($iName) {
// check expression for $, meaning it's actually a reference to a variable
// If found, return value of variable - otherwise return original string.
	$objVar = new clsW3VarName($iName);
	$objVar->Trace();
	$objVar->Fetch();
	$strOut = $objVar->Value;
	return $strOut;

}
function W3GetVal($iName,$iIndex=NULL) {
// gets value of given variable
// checks function arguments, if function is defined
	$objVar = new clsW3VarName();
	$objVar->ParseName($iName);
	if (!is_null($iIndex)) {
		$objVar->SetIndex($iIndex);
	}
	$objVar->Trace();
	$objVar->Fetch();
	$strVal = $objVar->Value;
	return $strVal;
}
function W3GetEcho() {
	global $wgW3_echoBuffer;

	$out = $wgW3_echoBuffer;
	$wgW3_echoBuffer = '';
	return $out;
}
function W3AddEcho($iVal) {
	global $wgW3_echoBuffer;

	$wgW3_echoBuffer .= $iVal;
}
function W3AddTrace($iLine,$iInd=0) {
	global $wgW3Trace, $wgW3Trace_indents, $wgW3Trace_indent;
	global $wgW3_doTrace;
	global $wgW3_TraceCount;

	if ($wgW3_doTrace) {
		if ($wgW3_TraceCount < 2000) {
			$wgW3Trace[] = $iLine;
			$wgW3Trace_indents[] = $wgW3Trace_indent;
			$wgW3Trace_indent += $iInd;
		}
		$wgW3_TraceCount++;
	}
/**/
}
function W3Status_RawOk() {
	global $wgTitle;
	global $wgW3_func;
	global $wgW3TPLSettings;
	global $wgW3_Override_RawOk;

	if (isset($wgW3_Override_RawOk)) {
	    $isProt = $wgW3_Override_RawOk;
	} else {
	    if ($wgW3TPLSettings['raw-ok']) {
		    $isProt = TRUE;
	    } else {
		    $isProt = $wgTitle->isProtected ('edit');
	    }
	    if (!$isProt) {
		    if (is_object($wgW3_func)) {
			    $isProt = $wgW3_func->isOkRaw;
		    }
	    }
	    W3AddTrace('IS RAW ok in ['.$wgTitle->getFullText().']: '.TrueFalse($isProt));
	}
	return $isProt;
}
function W3Status_SQLOk() {
    global $wgTitle;
    global $wgW3_func;
    global $wgW3TPLSettings;

    if ($wgW3TPLSettings['sql-ok']) {
	$isOk = TRUE;
    } else {
	$isProt = $wgTitle->isProtected ('edit');
	if ($isProt) {
	    $isOk = TRUE;
	} else {
	    if (is_object($wgW3_func)) {
		$isOk = $wgW3_func->isOkSQL;
	    } else {
		$isOk = FALSE;
	    }
	}
    }
    // FUTURE: print a message if SQL is forbidden
    //W3AddTrace('IS SQL allowed in ['.$wgTitle->getFullText().']: '.TrueFalse($isProt));
    return $isOk;
}
/*----
  HISTORY:
    2011-05-31 added "tag" attribute
*/
function W3Let_scalar( $iVar, $iArgs, $input, $parser ) {
	global $wgRequest;
	global $wgW3_func;
	global $wgOptCP_SubstStart, $wgOptCP_SubstFinish;

	$strRepl = W3GetExpr($iArgs->GetVal('repl'));
	$strWith = W3GetExpr($iArgs->GetVal('with'));
	$doRepl = !is_null($strRepl) || !is_null($strWith);
	$doAppend = $iArgs->Exists('append');

// TRACING:
	$strTrace = ' - &lt;LET&gt; scalar:';
	if (!is_null($strRepl)) {
		$strTrace .= ' repl=&ldquo;'.$strRepl.'&rdquo;';
	}
	if (!is_null($strWith)) {
		$strTrace .= ' repl=&ldquo;'.$strWith.'&rdquo;';
	}
	if ($doAppend) {
		$strTrace .= ' APPEND';
	}
	W3AddTrace($strTrace);

	if ($iArgs->Exists('val')) {
		$iVar->Value = $iArgs->GetExpr('val');
		$strTrace = ' - LET VAL: expr=['.ShowHTML($iVar->Value).']';
		W3AddTrace($strTrace);
	} elseif ($iArgs->Exists('arg')) {
		$strCopy = $iArgs->vArgs['arg'];	// don't do any indirection from user input (possible security hole)
		$parser->disableCache();
		$iVar->Value = $wgRequest->getVal($strCopy); // , $strDefault) -- maybe add feature later
	} elseif ($iArgs->Exists('farg')) {
		if (is_null($wgW3_func)) {
			W3AddTrace(' - ERROR: no function active to provide arg ['.$strName.']');
		} else {
			$strName = strtolower($iArgs->GetExpr('farg'));
			if ($wgW3_func->HasArg($strName)) {
				$iVar->Value = $wgW3_func->ArgVal($strName);
				W3AddTrace(' - ARG['.$strName.'] => &ldquo;'.$iVar->Value.'&rdquo;');
			} else {
				W3AddTrace(' - ERROR: function ['.$wgW3_func->Name.'] has no argument named ['.$strName.'].');
			}
		}
	} elseif ($iArgs->Exists('chr')) {
		$iVar->Value = chr($iArgs->GetExpr('chr'));
	}

// AT THIS POINT, $this->Value is loaded with the value we want to operate on.

// later, we may want inc/dec to imply self-operation if there is no other input...
//	but this needs to be thought through carefully. For now, require "self" to increment self.

// do processing on current value:
	if ($iArgs->Exists('inc')) {
		$iVar->Value++;
	}
	if ($iArgs->Exists('dec')) {
		$iVar->Value--;
	}
	if ($iArgs->Exists('not')) {
	    $iVar->Value = !$iVar->Value;
	}

	if ($iArgs->Exists('parse') || $iArgs->Exists('pre')) {		// restoring "pre" for backwards compatibility
		$iVar->Value = $parser->recursiveTagParse($iVar->Value);
	}
	if ($iArgs->Exists('vars')) {
		W3AddTrace(' LET VARS before ['.ShowHTML($iVar->Value).']');
		$objTplt = new clsStringTemplate_w3tpl($wgOptCP_SubstStart,$wgOptCP_SubstFinish);
		$objTplt->Value = $iVar->Value;
		$iVar->Value = $objTplt->Replace();
		W3AddTrace(' LET VARS after ['.ShowHTML($iVar->Value).']');
	}
	if ($iArgs->Exists('ucase')) {
		$iVar->Value = strtoupper($iVar->Value);
	}
	if ($iArgs->Exists('lcase')) {
		$iVar->Value = strtolower($iVar->Value);
	}
	if ($iArgs->Exists('ucfirst')) {
		$iVar->Value = ucfirst($iVar->Value);
	}
	if ($iArgs->Exists('lcfirst')) {
		$iVar->Value = lcfirst($iVar->Value);
	}
	if ($iArgs->Exists('trim')) {
		$iVar->Value = trim($iVar->Value);
	}
	if ($iArgs->Exists('len')) {
		$strLen = $iArgs->GetExpr('len');
		if (is_numeric($strLen)) {
			$iVar->Value = substr($iVar->Value,0,$strLen);
		}
	}

// 2011-06-01 these functions will probably need some debugging, especially in how they interact with other functions
	if ($iArgs->Exists('plus')) {
		$valNew = $iVar->Value;		// save the newly-calculated value off to one side
		$iVar->Fetch();			// restore the prior value
		$iVar->Value += $valNew;	// add the new value to the prior value
	}
	if ($iArgs->Exists('minus')) {
		$valNew = $iVar->Value;		// save the newly-calculated value off to one side
		$iVar->Fetch();			// restore the prior value
		$iVar->Value -= $valNew;	// subtract the new value from the prior value
	}
	if ($iArgs->Exists('min')) {
		$valNew = $iVar->Value;		// save the newly-calculated value off to one side
		$iVar->Fetch();			// restore the prior value
		if ($valNew < $iVar->Value) {
		    $iVar->Value = $valNew;
		}
	}
	if ($iArgs->Exists('max')) {
		$valNew = $iVar->Value;		// save the newly-calculated value off to one side
		$iVar->Fetch();			// restore the prior value
		if ($valNew > $iVar->Value) {
		    $iVar->Value = $valNew;
		}
	}

	if ($iArgs->Exists('fmt')) {
	    $fmt = $iArgs->GetExpr('fmt');
	    $iVar->Value = sprintf($fmt,$iVar->Value);
	}
	if ($iArgs->Exists('tag')) {
	    // surround result with <> to make it into an HTML tag
	    $iVar->Value = '<'.trim($iVar->Value).'>';
	}
	if ($doRepl) {
		if (is_null($strRepl)) {
			$strRepl = $input;
		} elseif (is_null($strWith)) {
			$strWith = $input;
		}
		$doIncluding = TRUE;
		if ($iArgs->Exists('before')) {
		// replace everything before the mark
		// TODO
			$doIncluding = FALSE;
		}
		if ($iArgs->Exists('after')) {
		// replace everything after the mark
		// TODO
			$doIncluding = FALSE;
		}
		if ($iArgs->Exists('including')) {
			$doIncluding = TRUE;
		}
		if ($doIncluding) {
			$strRes = str_replace($strRepl,$strWith,$iVar->Value);
		}
		W3AddTrace('LET REPLACE ['.$strRepl.'] WITH ['.$strWith.'] => ['.$strRes.'] in &ldquo;'.$iVar->Value.'&rdquo;');
		$iVar->Value = $strRes;
	}
	W3AddTrace('LET ['.$iVar->Name.'] &larr; &ldquo;'.$iVar->Value.'&rdquo;');

// AT THIS POINT, we have the semi-final value to be stored
// -- if it's being appended, then get the old value and prepend it to the new:

	if ($doAppend) {
		$valNew = $iVar->Value;		// save the newly-calculated value off to one side
		
		W3AddTrace(' - APPEND &ldquo;'.$valNew.'&rdquo;');
		$iVar->Fetch();			// restore the prior value
		$iVar->Value .= $valNew;	// append the new value to the prior value
	}
}
function W3Let_array ( $iVar, $iArgs, $input, $parser ) {
	$doSort = $iArgs->Exists('sort');

	if ($doSort) {
		$iVar->DoSort($iArgs->Exists('rev'),$iArgs->Exists('val'));
	}
}
// **********
// === BEGIN tag functions
/*-----
  TAG: <arg>
  HOW DOES THIS WORK? Maybe this is another way of passing tag-bracketed data? I forgot I created this tag...
*/
function efW3Arg( $input, $args, $parser ) {
	global $wgW3_func;

	if (isset($args['name'])) {
		$name = $args['name'];
	} else {
		$name = '';
	}
	if (isset($args['pre'])) {
		$value = $parser->recursiveTagParse($input);
	} else {
		$value = $input;
	}
	$strTrace = ' +ARG: '.$wgW3_func->LoadArg($value,$name);
	W3AddTrace($strTrace);
}
/*-----
  TAG: <call>
  NOTES: We might later split this into two tags, one for already-loaded functions and one that
    looks them up in the database, but I'm thinking that using already-loaded functions will actually
    be the exception rather than the rule, so no need to optimize for it.
    ...and actually, if the function has been loaded, then access is almost as quick as before, so
    why optimize further?
*/
function efW3Call( $input, $args, $parser ) {
	global $wgW3_funcs,$wgW3_func;
	global $wgOut;

	W3AddTrace('{CALL:');
	$pcnt = 0;
	$strTrace = NULL;
//	$wgW3_echoOutput = NULL;
	foreach ($args as $name => $value) {
	    $strName = strtolower(trim($name));
	    if ($pcnt) {
		// every arg except the first one
		$strVal = W3GetExpr($value);
		$strTrace .= $objFunc->LoadArg($strVal,$strName);
		$strTrace .= ' ';
		$strTrace .= ')';
	    } else {
		// we're looking at the very first arg -- must be the function's name
		if ($strName == 'func') {
		    $funcName = strtolower(W3GetExpr($value));
		} else {
		    $funcName = $strName;
		}
		W3AddTrace(' - (CALL) '.$funcName);

		// there's probably a smoother way to do this...
		if (!is_array($wgW3_funcs)) {
		    $wgW3_funcs = array();
		}
		if (array_key_exists($funcName,$wgW3_funcs)) {
		  // function already loaded
		    $objFunc = $wgW3_funcs[$funcName];
		    $wgW3_func = $objFunc;
		} else {
		    $objFunc = NULL;	// in case we can't load it

		  // try to find it in page_props
		    $objProps = new clsContentProps($parser);
		    $objFunc = $objProps->LoadFunc($funcName);
/*
		    $fkey = '>fx()>'.$funcName;	// pp_propname to find
		    $sql = 'SELECT pp_page, pp_value FROM page_props WHERE pp_propname='.SQLValue($fkey);
		    $dbr =& wfGetDB( DB_SLAVE );
		    try {
			$res = $dbr->query($sql);
		    }
		    catch (Exception $e) {
			$out = "W3TPL got a db error searching for function [$funcName] - ''".$dbr->lastError()."'' - from this SQL:\n* ".$sql;
			return $parser->recursiveTagParse($out);
		    }
		    // get row ID of function
		    //if (is_resource($res)) {
			if ($dbr->numRows( $res ) <= 0) {
				$objFunc = NULL;	// function not found
			}
			while ($row = $dbr->fetchRow($res)) {
			    $id = $row['pp_page'];
			    $funcCode = $row['pp_value'];
			}
			// now look up arguments
			$akey = $fkey.'>';
			$sql = 'SELECT pp_propname, pp_value FROM page_props WHERE '
			  .'(pp_page='.$id.') AND '
			  .'(pp_propname LIKE '.SQLValue($akey.'%').')';
			try {
				$res = $dbr->query($sql);
			}
			catch (Exception $e) {
				$out = "W3TPL got a db error searching for arguments for function [$funcName] - ''".$dbr->lastError()."'' - from this SQL:\n* ".$sql;
				return $parser->recursiveTagParse($out);
			}

			$funcArgs = NULL;
			while ($row = $dbr->fetchRow($res)) {
			    $arg = $row['pp_propname'];
			    $val = $row['pp_value'];
			    $funcArgs[$arg] = $val;
			}

			// create function object and add to loaded list:
			$objFunc = new clsW3Function($parser,$funcName,$funcArgs,$funcCode);
*/
			$wgW3_func = $objFunc;
			$wgW3_funcs[$funcName] = $objFunc;

		    /*} else {
			$out = "W3TPL found no resource trying to load function [$funcName] from this SQL:\n* ".$sql;
			return $parser->recursiveTagParse($out);
		    }*/
		    
		}
		if (!is_object($objFunc)) {
		    $wgOut->AddHTML("<span class=previewnote><strong>W3TPL ERROR</strong>: Function [$funcName] is undefined.</span><br>* SQL: $sql");
		    return NULL;
		}
		$objFunc->ResetArgs();
	    }
	    $pcnt++;
	}
	if ($input) {
//		$strTrace .= ' INPUT: {'.ShowHTML($input).'}';
//		$res = $parser->recursiveTagParse($input);
		// pass the data between <call ...> and </call> to the function as the final parameter:
		$objFunc->LoadArg($input);
//		$strTrace .= ' PARSED: {'.ShowHTML($res).'}';
	}
	if ($pcnt) {
	    $out = $objFunc->execute();
	} else {
	    $wgOut->AddHTML('<span class=previewnote><strong>W3TPL ERROR</strong>: Function "'.$strName.'" not loaded; probably an internal error.</span>');
	}
	W3AddTrace($strTrace.' CALL}');
	$out = W3GetEcho();
	$wgW3_func = NULL;		// no active function (is this still used?)
	return $out;
}
/*-----
  TAG: <class>
*/
function efW3Class( $input, $args, $parser ) {
    global $wgW3_funcs;

    $valIn = $input;	// this will contain the class definition
    $arFuncsGlobal = $wgW3_funcs;	// save global functions
    $wgW3_funcs = NULL;			// reset function list
    $out = $parser->recursiveTagParse($valIn);	// adds functions defined within class to global function list
    $arFuncsClass = $wgW3_funcs;	// save class function list
    $wgW3_funcs = $arFuncsGlobal;	// restore global functions

    foreach ($arFuncsClass as $name => $code) {
    }
}
/*-----
  TAG: <dump>
*/
function efW3Dump( $input, $args, $parser ) {
	global $wgW3Vars, $wgW3_funcs;
	global $wgW3_doTrace, $wgW3_doTrace_vars;	// tracing options
	global $wgW3Trace, $wgW3Trace_indents;	// tracing data
	global $wgW3_TraceCount;
	global $wgW3_opt_fpLogs;

	$out = '<ul>';
	$doPost = isset($args['post']);	// show posted data
	$doTrace = isset($args['trace']);	// show trace log
	$doVars = isset($args['vars']);	// show all variables
	$doFuncs = isset($args['funcs']);	// show function definitions
	$doMem = isset($args['mem']);	// show memory usage

	$wgW3_doTrace = $doTrace;
	$wgW3_doTrace_vars = $doTrace && $doVars;

	if ($doPost) {
		$out .= '<li><b>Posted:</b>:<ul>';
		foreach ($_POST AS $key => $value) {
			$out .= ("\n<li>[<b>$key</b>]: [$value]");
		}
		$out .= '</ul>';
	}

	if ($doMem) {
		$out .= '<li> <b>Memory usage before</b>: '.memory_get_usage(TRUE).' bytes';
	}
	
	if ($input != '') {
		$out .= $parser->recursiveTagParse($input);
	}
	if ($doMem) {
		$out .= '<li> <b>Memory usage after</b>: '.memory_get_usage(TRUE).' bytes';
	}
	if ($doVars) {
		if (is_array($wgW3Vars)) {
			$out .= '<li><b>Variables</b>:<ul>';
			foreach ($wgW3Vars as $name => $value) {
				if (is_array($value)) {
					$out .= '<li> ['.$name.']: array';
					$out .= '<ul>';
					foreach ($value as $akey => $aval) {
						$out .= '<li> '.$name.'['.$akey.'] = ['.ShowHTML($aval).']';
					}
					$out .= '</ul>';
				} else {
					$out .= '<li> ['.$name.'] = ['.$value.']';
				}
			}
			$out .= '</ul>';
		} else {
			$out .= '<li><i>No variables set</i>';
		}
	}
	if ($doFuncs) {
		if (is_array($wgW3_funcs)) {
			$out .= '<li><b>Functions</b>:<ul>';
			foreach ($wgW3_funcs as $name => $obj) {
				$out .= '<li>'.$obj->dump();
			}
			$out .= '</ul>';
		} else {
			$out .= '<li><i>No functions defined</i>';
		}
	}
	if ($doTrace) {
		if (is_array($wgW3Trace)) {
			$out .= '<li><b>Trace</b> ('.$wgW3_TraceCount.' events):<ul>';
			$indCur = 0;
			foreach ($wgW3Trace as $idx => $line) {

				$indLine = $wgW3Trace_indents[$idx];
				if ($indLine > $indCur) {
					$out .= '-<b>'.$indLine.'</b>-<ul>';
				} elseif ($indLine < $indCur) {
					$out .= '</ul>';
				}
				$indCur = $indLine;
/**/
				$out .= '<li><b>'.$idx.'</b> '.$line;
			}
			$out .= '</ul>';
		} else {
			$out .= '<li><i>'.$wgW3_TraceCount.' trace events</i>';
		}
	}
	$out .= '</ul>';
	if (isset($args['file'])) {
		$strFile = $args['file'];	// dump to a file instead of screen
		$fh = fopen($wgW3_opt_fpLogs.$strFile, 'a');
//		$dt = new DateTime();
//		$outPfx = $dt->format('Y-m-d H:i:s') . "\n";
		$outPfx = date('Y-m-d H:i:s');
		if (isset($args['msg'])) {
			$outPfx .= ' - '.$args['msg'];
		}
		$outPfx .= '<br>';
		$qb = fwrite($fh, $outPfx.$out);
		fclose($fh);
		if (isset($args['hide'])) {
			return NULL;
		} else {
			return "$qb bytes logged to '''$strFile'''.";
		}
	} else {
		return $out;
	}
}
/*-----
  TAG: <echo>
*/
function efW3Echo( $input, $args, $parser ) {
	global $wgOptCP_SubstStart,$wgOptCP_SubstFinish;

	W3AddTrace('ECHO:');

	if (isset($args['chr'])) {
		$valIn = chr($args['chr']);
	} else if (isset($args['var'])) {
		$valIn = W3GetVal($args['var']);
	} else {
		$valIn = $input;
	}

	if (isset($args['strip'])) {
		$out = ShowHTML($valIn);
	}
	$doRaw = FALSE;
	if (isset($args['raw'])) {
		if (W3Status_RawOk()) {
			$doRaw = TRUE;
		}
	}
	$doNow = isset($args['now']);
	W3AddTrace(' input:['.ShowHTML($valIn).']');
	$out = $valIn;

	if (isset($args['vars'])) {
		W3AddTrace(' VARS before ['.ShowHTML($out).']');
		$objTplt = new clsStringTemplate_w3tpl($wgOptCP_SubstStart,$wgOptCP_SubstFinish);
		$objTplt->Value = $out;
		$out = $objTplt->Replace();
		W3AddTrace(' VARS after ['.ShowHTML($out).']');
	}

	if ($doRaw) {
		// no further processing
	} else {
		$out = $parser->recursiveTagParse($out);
		W3AddTrace(' PARSING returned ['.ShowHTML($out).']');
	}
	if (isset($args['isolate'])) {
		$out = IsolateOutput($out);
	}
	if (isset($args['tag'])) {	// surround result with <> to make it into an HTML tag
	    $out = '<'.$out.'>';
	}

	W3AddTrace(' output: ['.ShowHTML($out).'] ECHO}');
	if ($doNow) {
		return $out;
	} else {
		W3AddEcho($out);
	}
}
/*-----
  TAG: <else>
*/
function efW3Else( $input, $args, $parser ) {
	global $wgW3_ifFlag, $wgW3_ifDepth;

	$doHide = isset($args['hide']);	// only output <echo> sections

	$ifFlag = $wgW3_ifFlag[$wgW3_ifDepth];
	W3AddTrace(' ELSE('.$wgW3_ifDepth.'): ['.$ifFlag.']');
	if ($ifFlag) {
		W3AddTrace('ELSE skipped');
		$out = NULL;
	} else {
		W3AddTrace('ELSE executed');
		$wgW3_ifDepth++;
		$out = $parser->recursiveTagParse($input);
		$wgW3_ifDepth--;
		W3AddTrace('ELSE: OUT = ['.$out.']('.ShowHTML($out).')');
	}

	if ($doHide) {
		$out = W3GetEcho();
		return $out;
	} else {
		return $out;
	}
}
/*-----
  TAG: <exec>
  NOTES:
    This may eventually supercede <call>; for now, it is for calling plugin functions.
  2011-10-16 created
*/
function efW3Exec( $input, $args, $parser ) {
    global $wgW3Vars,$wgW3Mods;

    $strOutVar = NULL;
    $oArgs = array();

    foreach ($args as $name => $val) {
	$xval = W3GetExpr($val);	// parse the value in case it is not a constant
	switch ($name) {
	  case 'f':
	  case 'func':
	    $strFName = $xval;
	    break;
	  case 'mod':
	  case 'module':
	    $strMName = $xval;
	    break;
	  case 'content':	// the named argument's value is what's between the tags
	    $oArgs[$xval] = $input;
	    break;
	  case 'output':	// write output to the named variable; otherwise try to display it
	    $strOutVar = $xval;
	    break;
	  default:		// all others are named arguments
	    $oArgs[$name] = $xval;
	}
    }
    $res = $wgW3Mods->Dispatch($strMName,$strFName,$oArgs,$parser);
    if (is_null($strOutVar)) {
	return $res;	// output the function's return value
    } else {
	$wgW3Vars[$strOutVar] = $res;
    }
}
/*----
  USED BY:
    efW3For()
    API (to be documented)
*/
function ProcessRows($iDB,$iRes,$iName,$iParser,$input,$doHide, $iCallback=NULL) {
    global $wgW3_data;

    $dbr = $iDB;
    $res = $iRes;
    $strName = $iName;
    $parser = $iParser;
    $out = NULL;

    while( $row = $dbr->fetchObject ( $res ) ) {
	W3AddTrace('FOR: row->['.$strName.']');
	$wgW3_data[$strName] = $row;
	if (!is_null($iCallback)) {
	    $out .= $iCallback($row);
	}
	$strParsed = $parser->recursiveTagParse($input);
	if ($doHide) {
		$out .= W3GetEcho();
		W3AddTrace(' - FOR echo: ['.ShowHTML($out).']');
	} else {
		$out .= $strParsed;
		W3AddTrace(' - FOR parse: ['.ShowHTML($out).']');
	}
    }
    return $out;
}
/*-----
  TAG: <for>
  TO DO: There needs to be a descendent data class which can handle MW databases so we can
	switch between internal and external data just by selecting the appropriate class.
	As it is, there's a lot of untidy and almost-duplicate code for each case.
*/
function efW3For( $input, $args, $parser ) {
	global $wgW3_data;
	global $wgW3Vars;
	global $wgW3DBs;
	global $w3stop;
	global $sql;

	if ($w3stop) { return; }

	$objArgs = new W3HookArgs($args);
	W3AddTrace('FOR:');
	$out = NULL;
	$txtErr = NULL;

	$doHide = isset($args['hide']);	// only output <echo> sections
	$doArr = isset($args['array']);
	$doXps = isset($args['xps']);
	if ($doArr) {
		$strArr = $args['array'];
	}
	if ($doXps) {
		$strXps = $args['xps'];
	}
	if ($doArr || $doXps) {
		if (isset($args['index'])) {
			$strIdxName = $args['index'];
		}
		if (isset($args['sep'])) {
			$strSep = $args['sep'];
		} else {
			$strSep = NULL;
		}
	}
	if (W3Status_SQLOk()) {
	// for now, only look for SQL stuff if page is protected
	// TO DO: display error message on unprotected pages if SQL is used
	    $doSql = isset($args['sql']);
	    if ($doSql) {
		    //$sqlQry = W3GetVal($args['sql']);
		    $sqlQry = $objArgs->GetExpr('sql',TRUE);
	    }
	    $doTbl = isset($args['table']);
	    if ($doTbl) {
		    $strTbl = $args['table'];
	    }
	    $doDb = $doSql || $doTbl;
	    $strWhere = $objArgs->GetExpr('where', TRUE);
	} else {
	    $doDb = FALSE;
	    $txtErr = '<b>W3TPL</b>: database operation not allowed on unprotected page.';
	}
	// these parameters can be used for other types of data (implemented yet? probably not)
	$strSort = $objArgs->GetExpr('sort', TRUE);
	$strLimit = $objArgs->GetExpr('limit', TRUE);
//	$strName = $objArgs->GetVal('name');	// name of variable for storing data
	$strName = '@@row@@';			// v0.35
	$strEmpty = $objArgs->GetVal('empty');	// string to return if there is no data
	$strWhat = '';
	if ($doDb) {
	// doing something with a database
		$out = "\n".'&lt;for&gt; WARNING: no output created';	// default message (TO DO: make this configurable)
		if ($objArgs->Exists('db')) {
			$useMWDB = FALSE;
			$strDBName = $objArgs->GetVal('db');
			if (array_key_exists($strDBName,$wgW3DBs)) {
			    $strDBSpec = $wgW3DBs[$strDBName];
			    $dbr = new clsDatabase($strDBSpec);
			    $dbr->Open();
			    if ($doTbl) {
				$sqlWhat = $strTbl;	// TO DO: make sure table exists
			    }
			} else {
			    throw new exception('$wgW3DBs does not have a database named "'.$strDBSpec.'"');
			}
		} else {
			$useMWDB = TRUE;
			$dbr =& wfGetDB( DB_SLAVE );
			if ($doTbl) {
			    if ($dbr->tableExists($strTbl)) {
				    $sqlWhat = $strTbl;
			    } else {
				    $txtErr = "<b>W3TPL</b>: the table [$strTbl] does not exist. Use 'sql=' for more complex expressions.";
				    return $out;
			    }
			}
		}
		if ($strWhere != '') {
	//		$sqlWhere = $dbr->addQuotes($strWhere);
			$sqlWhere = $strWhere;		// TODO: need some way to harden against injection attack
		} else {
			$sqlWhere = FALSE;
		}
		if ($doSql) {
		    $sqlFull = $sqlQry;
		} else {
		    $sqlFull = 'SELECT * FROM '.$sqlWhat;
		    if ($sqlWhere) {
			    $sqlFull .= ' WHERE '.$sqlWhere;
		    }
		    if ($strSort) {
			    $sqlFull .= ' ORDER BY '.$strSort;
		    }
		    if ($strLimit) {
			    $sqlFull .= ' LIMIT '.$strLimit;
		    }
		}
		$sql = $sqlFull;
		W3AddTrace(' - SQL=[<b>'.$sqlFull.'</b>]');

		if ($useMWDB) {
			try {
		//		$res = $dbr->query($sqlWhat,$sqlWhere);
				$res = $dbr->query($sqlFull);
			}
			catch (Exception $e) {
/*
				$sqlSim = 'SELECT * FROM '.$sqlWhat;
				if ($sqlWhere) {
					$sqlSim .= ' WHERE '.$sqlWhere;
				}
*/
				$txtErr = "\n".'<br>W3TPL had a database error:<br>'."\n"
				  .'<i>'.$dbr->lastError().'</i><br>'."\n"
				  .'from this SQL:<br>'."\n"
				  .'<i>'.$sqlFull.'</i><br>';
				return $parser->recursiveTagParse($out);
			}
			W3AddTrace(' - rows: '.$dbr->numRows( $res ));
			if ($dbr->numRows( $res ) <= 0) {
				$dbr->freeResult( $res );
				return $parser->recursiveTagParse($strEmpty);
			}

/*
			while( $row = $dbr->fetchObject ( $res ) ) {
				W3AddTrace('FOR: row->['.$strName.']');
				$wgW3_data[$strName] = $row;
				$strParsed = $parser->recursiveTagParse($input);
				if ($doHide) {
					$out .= W3GetEcho();
					W3AddTrace(' - FOR echo: ['.ShowHTML($out).']');
				} else {
					$out .= $strParsed;
					W3AddTrace(' - FOR parse: ['.ShowHTML($out).']');
				}
			}
*/
			$out .= ProcessRows($dbr,$res,$strName,$parser,$input,$doHide);
			$dbr->freeResult( $res );
		} else {
			$res = $dbr->_api_query($sqlFull);
			if (is_resource($res)) {
			    if (mysql_num_rows( $res ) <= 0) {
				    return $parser->recursiveTagParse($strEmpty);
			    }
			    while ($row = mysql_fetch_assoc($res)) {
				    $wgW3_data[$strName] = $row;
				    $strParsed = $parser->recursiveTagParse($input);
				    if ($doHide) {
					    $out .= W3GetEcho();
					    W3AddTrace(' - FOR echo: ['.ShowHTML($out).']');
				    } else {
					    $out .= $strParsed;
					    W3AddTrace(' - FOR parse: ['.ShowHTML($out).']');
				    }
			    }
			} else {
			    throw new exception('Problem executing SQL: ['.$sqlFull.']');
			}
		}
		//$dbr->freeResult( $res );	// this actually causes *more* memory to be used
		//unset($wgW3_data);		// and so does this!
	}
	if ($doArr) {
		if (isset($args['index'])) {
			$strIdxName = $args['index'];
		}
		//$wgW3Vars[$strArr][0] = 'zero';
		$arr = $wgW3Vars[$strArr];
		if (is_array($arr)) {
			$idx = 0;
			foreach ($arr as $name => $value) {
				$idx++;
				if ($strLimit) {
					if ($idx > $strLimit) {
						break;
					}
				}
				$strTrace = 'FOR: row->['.$strArr.']'; 
				if ($strIdxName) {
					$wgW3Vars[$strIdxName] = (string)$name;
					$strTrace .= ' INDEX=['.$name.']=>['.$strIdxName.']';
				}
				$strParsed = $parser->recursiveTagParse($input);
				W3AddTrace($strTrace);
				if (!isset($out)) { $out = NULL; }
				if ($doHide) {
					$out .= W3GetEcho();
					W3AddTrace('FOR echo: ['.ShowHTML($out).']');
				} else {
					$out .= $strParsed;
					W3AddTrace('FOR parse: ['.ShowHTML($out).']');
				}
			}
		} else {
			W3AddTrace('FOR array=['.$strArr.']: not an array!');
		}
	}
	if ($doXps) {
		W3AddTrace('FOR xps=['.$strXps.']');
		if (isset($args['index'])) {
			$strIdxName = $args['index'];
			W3AddTrace(' - index ['.$strIdxName.']');
			$tok = substr ( $strXps, 0, 1);	// token for splitting
			W3AddTrace(' - tok ['.$tok.']');
			$out = NULL;
			if ($tok) {
				$tks = substr ( $strXps, 1 );	// tokenized string
				$list = explode ( $tok, $tks );	// split the string
				foreach ($list as $value) {
					if (!is_null($out)) {
						$out .= $strSep;
					}
					$wgW3Vars[$strIdxName] = $value;
					W3AddTrace(' - XPS iteration: ['.$strIdxName.'] <- ['.$value.']');
					$strParsed = $parser->recursiveTagParse($input);
					if ($doHide) {
						$out .= W3GetEcho();
						W3AddTrace(' - echo: ['.ShowHTML($out).']');
					} else {
						$out .= $strParsed;
						W3AddTrace(' - parse: ['.ShowHTML($out).']');
					}
				}
			}
		}
	}
	if (!is_null($txtErr)) {
	    W3AddEcho('<div class="previewnote">'.$txtErr.'</div>');	// what is the *proper* class for error msgs?
	    // or maybe only have error messages show up at preview time
	}
	return $out;
}
/*====
  DETAILS:
    * This currently uses the page_props table, but we're treating the properties as global
      and pretending that pages will play nice by not overwriting each other's properties.
    * There's probably a better way to do this, but it probably involves creating a new table for globals,
      which would make it more difficult to install.
*/
class clsContentProps {
    protected $objOut;
    protected $objParse;

    public function __construct($iParser) {
	$this->objParse = $iParser;
	$this->objOut = $iParser->mOutput;
    }
    /*----
      ACTION: Saves global properties
    */
    public function SaveArray(array $iArr, $iBase=NULL) {
	$keys = NULL;
	foreach ($iArr as $name => $val) {
	    $keys .= '>'.$name;
	    $key = $iBase.'>'.$name;
	    if (is_array($val)) {
		$this->SaveArray($val,$key);
	    } else {
		$this->SaveVal($key,$val);
	    }
	}
	$this->SaveVal($iBase.'>',$keys);	// save list of all sub-keys
    }
    public function SaveVal($iKey,$iVal) {
	$this->objOut->setProperty($iKey,$iVal);
    }
    protected function GetLoadSQL($iKey) {
	$sql = 'SELECT pp_page, pp_value FROM page_props WHERE pp_propname='.SQLValue($iKey);
	return $sql;
    }
    public function LoadVal($iKey) {
	$dbr =& wfGetDB( DB_SLAVE );
global $sql;
	$sql = $this->GetLoadSQL($iKey);
	try {
	    $res = $dbr->query($sql);
	}
	catch (Exception $e) {
	    $txt = "W3TPL got a db error searching for property [$iKey] - ''".$dbr->lastError()."'' - from this SQL:\n* ".$sql;
	    W3AddEcho('<div class="previewnote">'.$txt.'</div>');	// what is the *proper* class for error msgs?
//	    return $parser->recursiveTagParse($out);
	}
	if ($dbr->numRows( $res ) <= 0) {
		$rtn = NULL;	// key not found
	} else {
	    while ($row = $dbr->fetchRow($res)) {
		$id = $row['pp_page'];
		$rtn = $row['pp_value'];
	    }
	}
	return $rtn;
    }
    public function LoadVals($iKey) {
	$keys = $this->LoadVal($iKey.'>');
global $doDebug;
global $sql;

	if (is_null($keys)) {
	    return NULL;
	} else {
	    $xts = new xtString($keys);
	    $arNames = $xts->Xplode();
	    foreach ($arNames as $name) {
		$key = $iKey.'>'.$name;
		$val = $this->LoadVal($key);
		$arDown = $this->LoadVals($key);
		if (is_array($arDown)) {
		    $arThis[$name] = $arDown;
		} else {
		    $arThis[$name] = $val;
		}
	    }
	    return $arThis;
	}
    }
/*
    public function LoadArray($iBase=NULL) {
	$xts = new xtString();

	$arVals = $this->LoadVals($iBase);
	foreach ($arVals as $key => $val) {
	    $xts->Value = $key;
	    $arSplit = $xts->Xplode();
	    if (count($arSplit) == 1) {
	    } else {
	    }
	}

	// get row ID of function
	//if (is_resource($res)) {
	    if ($dbr->numRows( $res ) <= 0) {
		    $objFunc = NULL;	// function not found
	    }
	    while ($row = $dbr->fetchRow($res)) {
		$id = $row['pp_page'];
		$funcCode = $row['pp_value'];
	    }
	    // now look up arguments
	    $akey = $fkey.'>';
	    $sql = 'SELECT pp_propname, pp_value FROM page_props WHERE '
	      .'(pp_page='.$id.') AND '
	      .'(pp_propname LIKE '.SQLValue($akey.'%').')';
	    try {
		    $res = $dbr->query($sql);
	    }
	    catch (Exception $e) {
		    $out = "W3TPL got a db error searching for arguments for function [$funcName] - ''".$dbr->lastError()."'' - from this SQL:\n* ".$sql;
		    return $parser->recursiveTagParse($out);
	    }
    }
*/
    /*===
      OBJECT-SPECIFIC
    ===*/

    /*----
      ACTION: create a function object from stored data
    */
    public function LoadFunc($iName) {
	$key = ">fx()>$iName";
	$ar = $this->LoadVals($key);
	$ar['name'] = $iName;	// why is this not being set in LoadVals()?
	$objFunc = new clsW3Function($this->objParse);
	$objFunc->PutDef($ar);
	$wgW3_func = $objFunc;
	$wgW3_funcs[$iName] = $objFunc;

	return $objFunc;
    }

}
/*====
  DETAILS: This currently uses the page_props table, but it should be substrate-independent -- e.g.
    it could be modified to use SMW without breaking anything.
*/
class clsPageProps extends clsContentProps {
    protected $objPage;

    public function __construct($iParser,$iPage) {
	parent::__construct($iParser);
	$this->objPage = $iPage;
    }
	
    protected function GetLoadSQL($iKey) {
	if (is_object($this->objPage)) {
	    $sql = 'SELECT pp_page, pp_value FROM page_props'
	      .' WHERE'
		.' (pp_propname='.SQLValue($iKey).') AND'
		.' (pp_page='.$this->objPage->getArticleID().')';
	    return $sql;
	} else {
	    throw new exception('No page object available for loading value of page property ['.$iKey.'].');
	}
    }

}
/*-----
  TAG: <func>
  NOTE: Function output is not sent to web output because typically we want to be able to format it nicely,
    which usually means lots of extra blank lines and indents in the code. We don't want these in the output.
  HISTORY:
    2012-01-15 returning NULL now causes UNIQ-QINU tag to be emitted; changing to '' seems to fix this.
*/
function efW3Func( $input, $args, $parser ) {
	global $wgW3_funcs;


	$pcnt = 0;
	$funcArgs = array();	// declare var in case there are no args
	foreach ($args as $name => $value) {
		if ($pcnt) {
/*
The parser apparently sets the argument's value to its name if no value is specified.
This is a sort of bug for this purpose, but maybe it makes sense in other contexts.
The real way to get around it is to use <w3tpl> block syntax* instead of the <func> tag.
  *to be implemented
*/
			if ($value != $name) {
				$funcArgs[$name] = $value;
			} else {
				$funcArgs[$name] = null;
			}
		} else {
			$funcName = strtolower($value);	// 2011-07-25 allow name="function name" as 1st param
		}
		$pcnt++;
	}
	if (isset($funcName)) {
	    W3AddTrace('FUNC &ldquo;'.$funcName.'&rdquo;');
	    $objFunc = new clsW3Function($parser,$funcName,$funcArgs,$input);
	    $wgW3_funcs[$funcName] = $objFunc;

// store the function in page_props (added 2011-07-24)
// -- we'll use prefix-marked strings to store the function data, using ">" as the prefix
//	because it should never be in a function or argument name
//	    $fkey = '>fx()>'.$funcName;

/*
	    // store the function's permissions
	    $strPerms = NULL;
	    if (W3Status_RawOk()) {
		$strPerms .= ' raw';
	    }
	    if (W3Status_SQLOk()) {
		$strPerms .= ' sql';
	    }
	    $arFProps['perms'] = $strPerms;
	    // store the function's code
	    $arFProps['code'] = $input;
	    // store the argument data
	    foreach ($funcArgs as $name => $val) {
//		$akey = $fkey.'>'.$name;
//		$parser->mOutput->setProperty($akey,$val);
		$arFProps['arg'][$name] = $val;
	    }
	    $arProps['fx()'][$funcName] = $arFProps;
*/

	    $objProps = new clsContentProps($parser);
	    $objProps->SaveArray($objFunc->GetDef());
	} else {
	    W3AddTrace('FUNC: function name not set! input=['.$input.']');
	}
	return '';
}
/*-----
  TAG: <get>
*/
function efW3Get( $input, $args, $parser ) {
        global $wgRequest,$wgOut;

	$objArgs = new W3HookArgs($args);
	W3AddTrace('GET:');
	$doRaw = FALSE;

	if (isset($args['default'])) {
		$strDefault = $args['default'];
	} else {
		$strDefault = NULL;
	}

	// BUG: <get val=@something /> isn't handled right.
	if ($objArgs->Exists('name')) {
	    $strName = strtolower($objArgs->GetExpr('name'));
	} else {
	    $strName = NULL;
	}
	W3AddTrace(' - name=['.$strName.']');

$doDebug = FALSE;
	
// get the starting value
	if (isset($args['val'])) {
	    $strVal = $args['val'];
	    $strVal = W3GetExpr($strVal);	// check for redirections
	} elseif (isset($args['arg'])) {
	    $parser->disableCache();
	    $strVal = $wgRequest->getVal($strName, $strDefault);
	} else {
	    $strIdx = $objArgs->GetExpr('index',TRUE);
	    $strVal = W3GetVal($strName,$strIdx);
	}

	if (isset($args['pfx'])) {
		$strTxt = W3GetExpr($args['pfx']);
		$strVal = $strTxt.$strVal;
	}
	if (isset($args['sfx'])) {
		$strTxt = W3GetExpr($args['sfx']);
		$strVal .= $strTxt;
	}

	if (isset($args['codes'])) {
		$strVal = ShowHTML($strVal);
	} else {
		$doRaw = FALSE;
		if (isset($args['raw'])) {
			if (W3Status_RawOk()) {
				$doRaw = TRUE;
			}
		}
		if (!$doRaw) {
			$strVal = $parser->recursiveTagParse($strVal);
		}
	}

	if (isset($args['isolate'])) {
		$strVal = IsolateOutput($strVal);
	}
	if (isset($args['len'])) {
		$strVal = substr($strVal,0,$args['len']);
	}
	if (isset($args['ucase'])) {
		$strVal = strtoupper($strVal);
	}
	if (isset($args['lcase'])) {
		$strVal = strtolower($strVal);
	}
	return $strVal;
}
/*-----
  TAG: <hide>
  ACTION: Doesn't display anything between the tags.
    Any tag that wants to be able to display certain things *anyway* must handle output directly.
  HISTORY:
    2012-01-15 returning NULL now causes UNIQ-QINU tag to be emitted; changing to '' seems to fix this.
*/
function efW3Hide( $input, $args, $parser ) {
	$parser->recursiveTagParse( $input );
	return '';
}
/*-----
  TAG: <if>
*/
function efW3If( $input, $args, $parser ) {
	global $wgW3_ifFlag,$wgW3_ifDepth;

	$doHide = isset($args['hide']);	// only output <echo> sections

	$ifFlag = false;
	if (!$wgW3_ifDepth) {
		$wgW3_ifDepth = 0;
	}
	if (isset($args['flag'])) {
		$strName = $args['flag'];
		$strVal = W3GetVal($strName);
		if (is_null($strVal) || ($strVal == '')) {
			$ifFlag = FALSE;
			$dbgType = 'blank';
		} else if (is_numeric($strVal)) {
			$ifFlag = ($strVal != 0);
			$dbgType = 'numeric';
		} else {
			$ifFlag = TRUE;
			$dbgType = '';
		}

		$strTrace = '';
		if ($wgW3_ifDepth != 0) {
		    $strTrace .= '.'.$wgW3_ifDepth;
		}
		$strTrace .= ' flag='.$strName;
		$strTrace .= ' val='.'['.$strVal.']';

		W3AddTrace('IF'.$strTrace.' != 0: ['.TrueFalse($ifFlag).']:'.$dbgType);
	} elseif (isset($args['comp'])) {
// We need to be able to pass either constants or variables via these parameters, so use GetExpr not GetVal
		$strName = $args['comp'];
		$strVal1 = W3GetExpr($strName);

		$strTrace = '';
		if ($wgW3_ifDepth != 0) {
		    $strTrace .= '.'.$wgW3_ifDepth;
		}

		$strTrace .= ' ('.$strName.'="'.$strVal1.'")';
		$strName = $args['with'];
		$strVal2 = W3GetExpr($strName);
		$strTrace .= ' ('.$strName.'="'.$strVal2.'")';
		if (isset($args['pre'])) {
			$wgW3_ifDepth++;
			$strVal1 = $parser->recursiveTagParse($strVal1);
			$strVal2 = $parser->recursiveTagParse($strVal2);
			$wgW3_ifDepth--;
//			$strVal1 = $parser->replaceVariables($strVal1);
//			$strVal2 = $parser->replaceVariables($strVal2);

		}
		$ifFlag = ($strVal1 == $strVal2);
		W3AddTrace('IF'.$strTrace.':'.TrueFalse($ifFlag));
	}
	if (isset($args['not'])) {
		$ifFlag = !$ifFlag;	// invert the flag
	}
	$wgW3_ifFlag[$wgW3_ifDepth] = $ifFlag;
	if ($ifFlag) {
		$wgW3_ifDepth++;
		$out = $parser->recursiveTagParse($input);
		$wgW3_ifDepth--;
	} else {
		$out = '';
	}

	if ($doHide) {
		$out = W3GetEcho();
		return $out;
	} else {
		return $out;
	}
}
/*-----
  TAG: <let>
*/
function efW3Let( $input, $args, $parser ) {
        global $wgRequest;
	global $wgTitle; // for "save" option
	global $wgW3Vars,$wgW3_func;

	$strCopy = NULL;
	$objArgs = new W3HookArgs($args);
	$strNameRaw = $objArgs->GetVal('name');

$doDbg = $objArgs->Exists('load');

// trim whitespace and normalize name:
	$strName = trim($strNameRaw);
	$objVar = new clsW3VarName();
	$objVar->ParseName($strName);	// resolve any indirection (e.g. $var)

	if (isset($args['index'])) {
		$strIdx = W3GetExpr($args['index']);
		$objVar->SetIndex($strIdx);
	}
	$strName = $objVar->Name;
	W3AddTrace('&lt;LET&gt; name=['.$strNameRaw.'] parsed to ['.$strName.']');

	if (isset($args['null'])) {
	// if "null" option, then nothing else matters
	    $objVar->Clear();

	} elseif ($objArgs->Exists('load')) {
	    // This could be either array or scalar, so we have to handle it here.
	    // This means other options won't work on a load; oh well, fix later.
	    $objVar->Clear();

	    if ($objArgs->Exists('page')) {
		$strTitleRaw = $objArgs->GetVal('page');
		$vobjTitle = new clsW3VarName();
		$vobjTitle->ParseName($strTitleRaw);
		$strTitle = $vobjTitle->Name;
//		$strTitle = $objArgs->GetExpr('page');
		W3AddTrace(' - from page: ['.$strTitle.']');
		$objTitle = Title::newFromText($strTitle);
		if (!is_object($objTitle)) {
		    echo 'vArgs:<pre>'.print_r($objArgs->vArgs,TRUE).'</pre>';
		    echo 'wgW3Vars:<pre>'.print_r($wgW3Vars,TRUE).'</pre>';
		    throw new exception('Did not get a title object for the page named ['.$strTitle.'], parsed from ['.$strTitleRaw.']');
		}
		$objProps = new clsPageProps($parser,$objTitle);
		if ($objArgs->Exists('array')) {
		    $objVar->LoadArray($objProps);
		} else {
		    $strVal = $objProps->LoadVal($strName);
		    W3AddTrace(' -- value: ['.$strVal.']');
		    $objVar->Load($strVal);
		}
	    } else {
		// for now, we're not going to support loading global arrays
		//  just because it seems like a recipe for trouble.
		W3AddTrace(' - from global');
		$objProps = new clsContentProps($parser);
		$strVal = $objProps->LoadVal($strName);
		$objVar->Load($strVal);
	    }
	} else {
	    // preload variable with whatever it needs, then use standard option processing

	    // "copy" option works for any data type:
	    if ($objVar->CheckCopy($objArgs)) {
	    // do nothing; work already done
	    } else {
		if (is_null($input) or isset($args['self'])) {
			$objVar->Fetch();
			W3AddTrace(' - from self: ['.$objVar->Value.']');
		} else {
			$objVar->Value = $input;
			W3AddTrace(' - from input: ['.$objVar->Value.']');
		}
	    }
	    if ($objVar->IsArray()) {
		    W3Let_array ( $objVar, $objArgs, $input, $parser );
	    } else {
		    W3Let_scalar ( $objVar, $objArgs, $input, $parser );

// 2011-09-20 this code seems to overwrite an array value -- so moving it here
		    if (isset($args['parse'])) {	// 2011-07-22 not tested yet
			$objVar->Value = $parser->recursiveTagParse($objVar->Value);
		    }
		    if (isset($args['tag'])) {	// surround result with <> to make it into an HTML tag
			$objVar->Value = '<'.$objVar->Value.'>';
		    }
		    $objVar->Store();
	    }
	}

// (option) store the results:
	if (isset($args['save'])) {
	    $objProps = new clsPageProps($parser,$wgTitle);
	    $objVar->Save($objProps);
	}

// (option) print the results:
	if (isset($args['echo'])) {
		if (isset($args['oparse'])) {
		    $rtn = $parser->recursiveTagParse($objVar->Value);
		} else {
		    $rtn = $objVar->Value;
		}
	} else {
		$rtn = NULL;
	}
	return is_null($rtn)?'':$rtn;
}
/*-----
  TAG: <load>
  NOTE: Some pages apparently don't create the parser object; if this code needs to run on one of those pages,
	then this may need to create $wgParser if it doesn't exist. For now, we assume optimistically.
   TO DO:
	Make parsing optional for protected pages
*/
function efW3Load( $input, $args, $parser ) {
	global $wgTitle, $wgOut;
	global $w3stop;

	if ($w3stop) { return; }

	$strTitle = $args['page'];
	W3AddTrace('LOAD: page={'.$strTitle.'}');
	$strTitle = W3GetExpr($strTitle);
	$doEcho = isset($args['echo']);
	
	W3AddTrace('LOAD -> {'.$strTitle.'}');


	$objTitle = Title::newFromText($strTitle);
	if (is_object($objTitle)) {
		if (stripos($strTitle, 'special:')===0) {
		/* title is Specialpage; you'd think there would be a general page-loading method which handles these,
		  but I haven't been able to find it. */
		    W3AddTrace('LOAD SpecialPage ID='.$objTitle->getArticleID());
		    //$txtContent = SpecialPage::capturePath($objTitle);


		    $wgTitleOld = $wgTitle;
		    $wgOutOld = $wgOut;
		    $wgOut = new OutputPage;

		    $ret = SpecialPage::executePath( $objTitle, FALSE );
		    if ( $ret === true ) {
			    $ret = $wgOut->getHTML();
		    }
		    $wgTitle = $wgTitleOld;
		    $wgOut = $wgOutOld;

		    //SpecialPage::executePath( $objTitle, false );
		    $txtContent = $ret;
		    if ($txtContent === FALSE) {
			W3AddTrace('LOAD: Content not loadable');
		    } else {
			W3AddTrace('LOAD: Content has '.strlen($txtContent).' bytes');
		    }
		} else {
		    W3AddTrace('LOAD Title ID='.$objTitle->getArticleID());
		    $objArticle = new Article($objTitle);
		    $txtContent = $objArticle->getContent();
		    W3AddTrace('LOAD: page (Title ID='.$objTitle->getArticleID().') has '.strlen($txtContent).' bytes, starting with: '.substr($txtContent,0,40));
		}
		$txtContent .= $input;	// any additional input to be parsed in page's context

// eventually we will have a way to retrieve the contents via a variable
// until then, the "raw" option is just for debugging
		if (isset($args['raw'])) {
			$out = $txtContent;
		} else {
			if (isset($args['local'])) {
			// parse title in its own context, not in the parent page's context
				W3AddTrace(' - LOAD as LOCAL');

				// temporarily replace $wgTitle with the page we're parsing
				$objTitleOuter = $wgTitle;
				$wgTitle = $objTitle;
				$out = $parser->recursiveTagParse($txtContent);
				$wgTitle = $objTitleOuter;
				// restore $wgTitle's original value

			} else {
				$out = $parser->recursiveTagParse($txtContent);
			}
			if (isset($args['nocat'])) {
			// clear out any categories added by parsing of loaded text
				$parser->mOutput->mCategories = array();
				//$parser->mOutput->setCategoryLinks(NULL);
			}
		}
	} else {
		$out = 'Title ['.ShowHTML($strTitle).'] does not exist.';	// change this to a proper system message at some point
	}
	if (isset($args['let'])) {
		$strName = $args['let'];
		W3SetVar($strName, $out);
	}
	W3AddTrace('LOAD END');
	if ($doEcho) {	
		return $out;
	} else {
		return NULL;
	}
}
/*-----
  TAG: <save>
*/
function efW3Save( $input, $args, $parser ) {
	global $wgW3_edit_queue,$w3stop;

	if ($w3stop) { return; }

	$strTitle = $args['page'];
	W3AddTrace('SAVE: page={'.$strTitle.'}');
	$strTitle = W3GetExpr($strTitle);
	W3AddTrace('SAVE -> {'.$strTitle.'}');

	$txtSummary = NULL;
	$intFlags = EDIT_DEFER_UPDATES;	// does this prevent the parser from getting confused?
	if (isset($args['text'])) {
		$txtContent = W3GetExpr($args['text']);
	} else {
		$txtContent = '';
	}
	if (isset($args['insert'])) {
		$txtContent = W3GetExpr($args['insert']).$txtContent;
	}
	if (isset($args['append'])) {
		$txtContent .= W3GetExpr($args['append']);
	}
	if (isset($args['comment'])) {
		$txtSummary = $args['comment'];
	} else {
		$intFlags = $intFlags | EDIT_AUTOSUMMARY;
	}
	if (isset($args['minor'])) {
		$intFlags = $intFlags | EDIT_MINOR;
	}

	$objTitle = Title::newFromText($strTitle);
	$ok = FALSE;
	$strStatus = 'fail - no article ['.$strTitle.']';
	if (is_object($objTitle)) {
		$objArticle = new Article($objTitle);
		if (is_object($objArticle)) {
			$wgW3_edit_queue[] = new w3ArticleEdit($objArticle,$txtContent,$txtSummary,$intFlags);
			$ok = TRUE;
			$strStatus = 'ok';
		} else {
			$ok = FALSE;
			$strStatus = 'fail - could not load ['.$strTitle.']';
		}
/*		$objArticle = new Article($objTitle);
		$ok = $objArticle->doEdit( $txtContent, $txtSummary, $intFlags );
		$strStatus = $ok?'ok':'fail - no save';
*/
	}
	if (isset($args['ok'])) {
		$strName = $args['ok'];
		W3SetVar($strName, $ok);
	}
	if (isset($args['status'])) {
		$strName = $args['status'];
		W3SetVar($strName, $strStatus);
	}
}
/*-----
  TAG: <w3tpl>
  TO DO: inline code parsing/processing
	HISTORY:
		2012-01-20 returning NULL causes problems in MW 1.18
*/
function efW3TPLRender( $input, $args, $parser ) {
	global $wgRestrictDisplayTitle;
	global $w3step,$w3stop;

	$objArgs = new W3HookArgs($args);

	$out = '';

	if ($objArgs->Exists('nocache')) {
	        $parser->disableCache();
	}
	if ($objArgs->Exists('quit')) {
		$w3stop = TRUE;
	}
	if ($objArgs->Exists('title')) {
	    $wgRestrictDisplayTitle = FALSE;
	    $strTitle = $objArgs->GetExpr('title');
// only one of the following lines works, depending on whether we are previewing or viewing normally
	    $parser->getOutput()->setDisplayTitle($strTitle);	// changes the page header title (display mode)
	    $parser->getOutput()->setTitleText($strTitle);	// changes the HTML <title> text (preview mode)
	}
	if ($objArgs->Exists('step')) {
		$w3step = true;	// show each line of code before it is executed
	}
	if ($input != '') {
		$out .= $input;

		if (isset($args['pre'])) {
			$out = $parser->recursiveTagParse( $out );
		}
		if (!isset($args['notpl'])) {
			$out = ActualRender($out,$args);
		}
		if (isset($args['post'])) {
			$out = $parser->recursiveTagParse( $out );
		}
	}
	return $out;
}
/*-----
  TAG: <xploop>
  NOTE: I *think* this tag is deprecated, in favor of <for> with the appropriate option. It may be discontinued at some point.
*/
function efW3Xploop( $input, $args, $parser ) {
	global $wgW3Vars;

	$objArgs = new W3HookArgs($args);

	$strListRaw = $objArgs->GetVal('list');
	$strList = W3GetExpr($strListRaw);
	$strTok = $objArgs->GetVal('repl');
	$doEcho = isset($args['echo']);

	if (isset($args['var'])) {
		$strVar = strtolower($args['var']);
	} else {
		$strVar = NULL;
	}
	$sepStr = $objArgs->GetVal('sep');

	if ($strTok) {
// doing a straight token replacement
		$strTrace = 'XPLOOP replace ('.ShowHTML($strTok).') &larr; &ldquo;'.ShowHTML($strList).'&rdquo;';
	} else {
// setting variable value
		$strTrace = 'XPLOOP set ('.ShowHTML($strVar).') &larr; &ldquo;'.ShowHTML($strList).'&rdquo;';
	}
	if ($strList != $strListRaw) {
		$strTrace .= '&larr;&ldquo;'.$strListRaw.'&rdquo;';
	}
	W3AddTrace($strTrace);
	if (isset($args['parselist'])) {
		$strList = $parser->recursiveTagParse( $strList );
	}

	$tok = substr ( $strList, 0, 1);	// token for splitting
	if ($tok) {
		$tks = substr ( $strList, 1 );	// tokenized string
		$list = explode ( $tok, $tks );	// split the string
		$out = '';
	} else {
		return NULL;
	}
	if ($strTok) {
// doing a straight token replacement
		foreach ($list as $value) {
			if ($out) {
				$out .= $sepStr;
			}
			$out .= str_replace( $strTok, $value, $input );
		}
		// 2012-01-21 if this is necessary, DOCUMENT it:
		//$out = CharacterEscapes::charUnesc( $out, array(), $parser );
		$out = $parser->recursiveTagParse( $out );
	} else {
		foreach ($list as $value) {
			if ($out) {
				$out .= $sepStr;
			}
			$wgW3Vars[$strVar] = $value;
			W3AddTrace(' - XP: ['.$strVar.'] <- ['.$value.']');
			$out .= $parser->recursiveTagParse( $input );
		}
	}
	if ($doEcho) {
		return $out ;
	} else {
		$out = W3GetEcho();
		return $out;
	}
}
//**********

class w3ArticleEdit {
	private $vArticle;
	private $vContent;
	private $vSummary;
	private $vFlags;

	public function __construct($iArticle, $iContent, $iSummary, $iFlags) {
		$this->vArticle = $iArticle;
		$this->vContent = $iContent;
		$this->vSummary = $iSummary;
		$this->vFlags = $iFlags;
	}

	public function Exec() {
		$ok = $this->vArticle->doEdit( $this->vContent, $this->vSummary, $this->vFlags );
		$strStatus = $ok?'ok':'fail - no save';
		return $ok;
	}
}

function ActualRender($input) {
// break the code up into separate commands
		if ($doLine) {	// TRUE = line is complete
			//W3AddTrace('TPL: LINE=[<u>'.$strDbgLine.'</u>]');
			$out .= $cmdObj->execute($lines);
		}
// final semicolon not required:
	if ($line) {
		$out .= $cmdObj->execute($line,$clause);
	}
	return $out;
}

// TO DO: Change name to ShowMarkup, because it handles HTML, wikitext, and partly-parsed wikitext
function ShowHTML($iText, $iRepNewLine=TRUE) {

/*/
// DEBUGGING - explode string by inserting space between each character:
	$cpLen = strlen($iText);
	$out2 = '';
	for ($i = 0; $i <= $cpLen; $i++) {
		$out2 .= substr($iText,$i,1).' ';
	}
	$out = $out2;
/**/
//	$out = 'LENGTH='.strlen($iText);

	$out = $iText;
	$out = str_replace ( '<','&lt;',$out );
	$out = str_replace ( '>','&gt;',$out);
	$out = str_replace ( '[','&#91;',$out );
	$out = str_replace ( ']','&#93;',$out );
	$out = str_replace ( chr(7),'<b>^G</b>',$out);
	$out = str_replace ( chr(127),'<b>del</b>',$out);
	if ($iRepNewLine) {
		$out = str_replace ( chr(10),'<b>^J</b>',$out);
		$out = str_replace ( chr(13),'<b>^M</b>',$out);
	}
	return $out;
}

class clsStringTemplate_w3tpl extends clsStringTemplate {
// This version can be used if the values are in an associative array
	protected function GetValue($iName) {
		return W3GetVal($iName);
	}
}

/*
class w3expr {
	private $text;
	abstract function Parse($iTask);
} */

/*====
  USAGE:
    * This can be initialized in either of two ways:
      1. Construct with all parameters
      2. Construct with just iParser, and call PutDef for everything else
*/
class clsW3Function {

	var $vParser;
	var $isOkRaw, $isOkSQL;
	var $Name, $vParams, $vCode;
	var $vArgs, $vArgIdx;

	public function __construct($iParser, $iName=NULL, $iParams=NULL, $iCode=NULL) {
		$this->vParser = $iParser;
		$this->Name = $iName;
		$this->vParams = $iParams;
		$this->vCode = $iCode;
		$this->isOkRaw = W3Status_RawOk();
		$this->isOkSQL = W3Status_SQLOk();
	}
	/*----
	  INPUT: $iarDef = array containing complete function definition, as retrieved by clsContentProps
	  MIRROR: GetDef();
	*/
	public function PutDef(array $iarDef) {
	    if (!isset($iarDef['name'])) {
		throw new exception('Name not set in function definition.');
	    }
	    $this->NameName = $iarDef['name'];
	    $this->vParams = nzArray($iarDef,'arg');
	    if (!isset($iarDef['code'])) {
		throw new exception('Code not set in function definition.');
	    }
	    $this->vCode = $iarDef['code'];
	    $strPerms = $iarDef['perms'];

	    $this->isOkRaw = strpos($strPerms,' raw');
	    $this->isOkSQL = strpos($strPerms,' sql');
	}
	/*----
	  RETURNS: array containing complete function definition, suitable for storing in clsContentProps
	  MIRROR: PutDef();
	*/
	public function GetDef() {
	    $arArgs = $this->vParams;

	    // store the function's permissions
	    $strPerms = NULL;

	    if ($this->isOkRaw) {
		$strPerms .= ' raw';
	    }
	    if ($this->isOkSQL) {
		$strPerms .= ' sql';
	    }
	    $arFProps['perms'] = $strPerms;
	    // store the function's code
	    $arFProps['code'] = $this->vCode;
	    // store the argument data
	    foreach ($arArgs as $name => $val) {
//		$akey = $fkey.'>'.$name;
//		$parser->mOutput->setProperty($akey,$val);
		$arFProps['arg'][$name] = $val;
	    }
	    $arOut['fx()'][$this->Name] = $arFProps;

	    return $arOut;
	}
	public function dump() {
		$out = '<b>'.$this->Name.'</b>(';
		if (is_array($this->vParams)) {
			$pcnt = 0;
			foreach ($this->vParams as $name => $value) {
				if ($pcnt) {
					$out .= ', ';
				}
				$out .= '<u>'.$name.'</u>';
				if (isset($value)) {
					$out .= '='.$value;
				}
				$pcnt++;
			}
		}
		$out .= ') {';
		$strCode = ShowHTML($this->vCode,FALSE);
		$out .= '<pre>'.$strCode.'</pre>}';
		return $out;
	}
	public function ResetArgs() {
		$this->vArgIdx = 0;
		unset($this->vArgs);
	}
	public function LoadArg($iValue,$iName='') {
		if ($iName) {
			$strName = strtolower($iName);
			// (2011-08-21) this line is a kluge that should work most of the time
			$this->vArgIdx++;
			// it assumes that only the last parameter will ever be omitted
			// fix this later if it seems useful to support out-of-order omissions
		} else {
			$strName = $this->ParamName($this->vArgIdx);
			$this->vArgIdx++;
		}
		$this->vArgs[$strName] = $iValue;
		return '[<u>'.$strName.'</u>]=&ldquo;'.$iValue.'&rdquo;';
	}
	public function HasArg($iName) {
		return isset($this->vArgs[$iName]);
	}
	public function ArgVal($iName) {
		return $this->vArgs[$iName];
	}
	public function ParamName($iIndex) {
		$keys = array_keys($this->vParams);
		$key = $keys[$iIndex];
		return $key;
	}
	public function execute() {
	// set variables from passed arguments
		
		$useArgs = FALSE;
		if (isset($this->vArgs)) {
			if (is_array($this->vArgs)) {
				$useArgs = TRUE;
			}
		}
		if ($useArgs) {
			foreach ($this->vArgs as $name => $value) {
				if (W3VarExists($name)) {
					$oldVars[$name] = W3GetVal($name);
				}
				W3SetVar($name, $value);
			}
		}
	// parse (execute) the function code
		$out = $this->vParser->recursiveTagParse( $this->vCode );
	// restore original variables (old value if any, or remove from list)
		if ($useArgs) {
			foreach ($this->vArgs as $name => $value) {
				if (isset($oldVars[$name])) {
					W3SetVar($name, $oldVars[$name]);
				} else {
					W3KillVar($name);
				}
			}
		}
		return $out;
	}
}

class clsW3VarName {
/*
* An *expression* can be:
** a literal value - "this is a string"
** a reference to a variable - "$theVar"
** a reference to a special function - @row.fieldname
* A variable reference can be:
** scalar value - "$aScalar"
** array index - "$anArray[index_expression]" where "index_expression" is an expression

We automatically find the value of all inner elements as needed, but leave the outermost unresolved
	in order to allow for different operations depending on context.
*/
	public $Expr;	// code-expression to parse
	public $Name;	// final name of variable or function being referenced
	public $Index;	// (optional) index into variable array
	public $Value;	// loaded value of variable or function, for operations
	public $isFunc;

	public function __construct($iExpr = NULL) {
		$this->isFunc = FALSE;
		W3AddTrace('clsW3VarName: init=['.$iExpr.']');
		if (!is_null($iExpr)) {
			$this->ParseExpr($iExpr);
		}
	}
	/*----
	  ACTION: Clears the variable's value and removes any array elements
	*/
	public function Clear() {
	    if ($this->IsArray()) {
		$ar = $this->Value;
		foreach ($ar as $key => $val) {
		    $objNew = new clsW3VarName();
		    $objNew->Name = $this->Name;
		    $objNew->Index = $key;
		    $objNew->Clear();
		}
	    }
	    $this->Value = NULL;
	    $this->Store();
	}
	/*----
	  RETURNS: Object for the named array element
	*/
	public function GetElement($iIndex) {
	}
	/*----
	  RETURNS: full variable name -- including array index, if present
	  HISTORY:
	    2011-09-19 crude implementation so we can store array indexes in page properties
	*/
	public function FullName() {
	    $out = $this->Name;
	    if ($this->IsElem()) {
		$out .= '['.$this->Index.']';
	    }
	    return $out;
	}

	public function ParseExpr($iExpr) {
		global $wgW3_doTrace_vars;
		global $wgW3Trace_indent;

		$wgW3Trace_indent++;
		$strExpr = $iExpr;
		if ($wgW3_doTrace_vars) {
			W3AddTrace(' {PARSE-EXPR ['.ShowHTML($iExpr).']');
		}
		$chFirst = substr($strExpr,0,1);
		switch ($chFirst) {
		  case '$':
			$strRef = strtolower(substr($strExpr,1));
			$this->ParseName($strRef);
			break;
		  case '@':
			$strRef = strtolower(substr($strExpr,1));
			$this->ParseName($strRef);
			$this->isFunc = TRUE;
			break;
		  default:
			// it's a literal, so the string is the value
			$this->Value = $strExpr;
		}
		if ($wgW3_doTrace_vars) {
			W3AddTrace(' => ['.ShowHTML($strExpr).'] EXPR}');
		}
		$wgW3Trace_indent--;
		return $strExpr;
	}

	public function ParseName($iName) {
		global $wgW3_doTrace_vars;
		global $wgW3Trace_indent;
		$strName = $iName;
		$wgW3Trace_indent++;
		if ($wgW3_doTrace_vars) {
			W3AddTrace('{PARSE-NAME ['.ShowHTML($iName).']');
		}
	
		if (substr($strName,0,1) == '@') {
			$this->isFunc = TRUE;
			$this->Name = substr($strName,1);
			W3AddTrace(' - is func ['.$this->Name.']');
		} else {
			if (substr($strName, -1) == ']') {
			// name includes index offset
				$idxOpen = strpos($strName,'[');
				if ($idxOpen) {
					$idxShut = strpos($strName,']',$idxOpen);
					$vIndex = substr($strName,$idxOpen+1,$idxShut-$idxOpen-1);
					$strTrace = 'INDEX['.$vIndex.'] (@'.$idxOpen.'-'.$idxShut.' in ['.$strName.']) -> [';
					$strTrace .= $vIndex.']';
					$objIdx = new clsW3VarName($vIndex);
					$strName = substr($strName,0,$idxOpen);
					$objIdx->Fetch();	// calculate value of index expression
					$this->Index = $objIdx->Value;
					$strTrace .= ' NEW NAME=['.$strName.']';
					W3AddTrace($strTrace);
				}
			}
			$objVar = new clsW3VarName($strName);
			$objVar->Fetch();
			$this->Name = $objVar->Value;
		}
		if ($wgW3_doTrace_vars) {
			W3AddTrace('name:['.ShowHTML($this->Name).'] = val:['.ShowHTML($this->Value).'] NAME}');
		}
		$wgW3Trace_indent--;
/**/
	}

	public function SetIndex($iValue) {
		$this->Index = $iValue;
		W3AddTrace('clsW3VarName.SetIndex of ('.$this->Name.') to ['.$iValue.']');
	}
	public function IsElem() {	// is element of an array?
		return !is_null($this->Index);
	}
	public function IsVar() {
		return !is_null($this->Name) && !$this->isFunc;
	}
	public function IsArray() {
		return is_array($this->Value);
	}
	/*---
	  ACTION: Saves the variable in page properties
	    If the variable is an array, saves each element in a separate property
	  HISTORY:
	    2011-09-19 written to encapsulate existing functionality in <let>
	*/
	public function Save(clsPageProps $iProps) {
	    if ($this->IsArray()) {
		$iProps->SaveArray($this->Value,$this->FullName());
	    } else {
		$iProps->SaveVal($this->Name,$this->Value);
	    }
/* This works, but isn't consistent with how functions are stored and doesn't allow for array detection
	    if ($this->IsArray()) {
		$ar = $this->Value;
		foreach ($ar as $key => $val) {
		    $name = $this->FullName().'['.$key.']';
		    $iobjOut->setProperty($name,$val );
		}
	    } else {
		$strKey = $this->FullName();
		// is there some better way to access mOutput?
		$iobjOut->setProperty($strKey,$this->Value);
	    }
*/
	}
	/*----
	  ACTION: Loads the variable from page properties
	*/	
	public function Load(clsPageProps $iProps) {
	    $this->Value = $iProps->LoadVal($this->Name);
	    $this->Store();
	}
	/*----
	  ACTION: Loads the variable from page properties as an array
	*/
	public function LoadArray(clsPageProps $iProps) {
	    W3AddTrace(' -- LoadArray ENTER');
	    $arVal = $iProps->LoadVals($this->Name);
	    if (is_array($arVal)) {
		W3AddTrace(' -- values for ['.$this->Name.'] array:');
		foreach ($arVal as $key => $val) {
		    W3AddTrace(' --- ['.$key.'] = ['.$val.']');
		    $objNew = new clsW3VarName();
		    $objNew->Name = $this->Name;
		    $objNew->Index = $key;
		    $objNew->Value = $val;
		    $objNew->Store();
		}
	    } else {
		W3AddTrace(' -- not an array');
	    }
	    W3AddTrace(' -- LoadArray EXIT');
	}
	/*----
	  ACTION: Loads the variable from page properties
	    If the page property indicates an array, loads the array into the variable
	*/
	/*----
	  NOTE: need to document what these (Fetch, Store) are for. They are apparently very important.
	*/
	public function Fetch() {
		global $wgW3Vars;

		$strName = strtolower($this->Name);
		if ($this->isFunc) {
			$this->Value = W3GetSysData($this->Name);
		} elseif ($this->IsVar()) {
			if ($this->IsElem()) {
				$strIndex = $this->Index;
				if (isset($wgW3Vars[$strName][$strIndex])) {
					$this->Value = $wgW3Vars[$strName][$strIndex];
					W3AddTrace('clsW3VarName.Fetch ('.$strName.')['.$strIndex.']=>&ldquo;'.$this->Value.'&rdquo;');
				} else {
					$this->Value = NULL;
					W3AddTrace('clsW3VarName.Fetch ('.$strName.')['.$strIndex.']=>NULL');
				}
			} else {
				if (isset($wgW3Vars[$strName])) {
					$this->Value = $wgW3Vars[$strName];
					W3AddTrace('clsW3VarName.Fetch ('.$strName.')=>&ldquo;'.$this->Value.'&rdquo;');
				} else {
					$this->Value = NULL;
					W3AddTrace('clsW3VarName.Fetch ('.$strName.')=>NULL');
				}
			}
		} else {
			// literal value - already set.
		}
		W3AddTrace('clsW3VarName.Fetch: value=['.$this->Value.']');
		$this->Trace();
	}

	public function Store() {
		global $wgW3Vars;

		$strName = $this->Name;
		$strVal = $this->Value;
		if ($this->isFunc) {
			W3SetSysData($strName,$strVal);
		} elseif ($strName) {
			$strName = strtolower($strName);
			if ($this->isElem()) {
				$strIndex = $this->Index;
				$wgW3Vars[$strName][$strIndex] = $strVal;
				W3AddTrace('clsW3VarName.Store: '.$strName.'['.$strIndex.'] &larr; &ldquo;'.$strVal.'&rdquo;');
			} else {
				$wgW3Vars[$strName] = $strVal;
				W3AddTrace('clsW3VarName.Store: ['.$strName.'] &larr; &ldquo;'.$strVal.'&rdquo;');
			}
			$this->Trace();
		} else {
			W3AddTrace('clsW3VarName.Store: ERROR: no name for value &ldquo;'.$strVal.'&rdquo;');
			$this->Trace();
		}
	}

	public function DoSort($iRev=FALSE, $iVal=FALSE) {
		if (is_array($this->Value)) {
			if ($iVal) {
			// sort by value
				if ($iRev) {
					$ok = arsort($this->Value);
				} else {
					$ok = asort($this->Value);
				}
			} else {
				if ($iRev) {
					$ok = arsort($this->Value);
				} else {
					$ok = ksort($this->Value);
				}
			}
			if ($ok) {
				W3AddTrace('LET sort ['.$this->Name.'] OK');
			} else {
				W3AddTrace('LET sort ['.$this->Name.'] <b>ERROR</b>: failed.');
			}
		} else {
			W3AddTrace('LET sort <b>ERROR</b>: ['.$this->Name.'] is not an array.');
		}
	}

	public function CheckCopy($iArgs) {
		if ($iArgs->Exists('copy')) {
			$strSrce = $iArgs->GetVal('copy');
			$this->Value = W3GetVal($strSrce);
			W3AddTrace('CheckCopy from ['.$strSrce.']');	// show raw input
			$this->Trace();				// show result
			return TRUE;
		} else {
			return FALSE;
		}
	}

	public function Trace() {
		if (is_array($this->Value)) {
			$strVal = '<i>array!</i>';
		} else {
			$strVal = $this->Value;
		}

		W3AddTrace('clsW3VarName.Trace: '.
			'Name=['.$this->Name.'] '.
			'Val=['.$strVal.'] '.
			'Index=['.$this->Index.'] '.
			TrueFalseHTML('is var',$this->IsVar()).' '.
			TrueFalseHTML('is element',$this->IsElem()).' '.
			TrueFalseHTML('is func',$this->isFunc)
			);
	}
}

function TrueFalseHTML($iName, $iVal) {
	if ($iVal) {
		return '<b>'.$iName.'</b>';
	} else {
		return '<font color=#7f7f7f><s>'.$iName.'</s></font>';
	}
}

class W3HookArgs {
	public $vArgs;

	public function __construct($iArgs) {
		$this->vArgs = $iArgs;
	}
	public function Exists($iName) {
		return isset($this->vArgs[$iName]);
	}
	public function GetVal($iName) {
		if (isset($this->vArgs[$iName])) {
			return $this->vArgs[$iName];
		} else {
			return NULL;
		}
	}
	public function GetExpr($iName, $iTrace = FALSE) {
		if (isset($this->vArgs[$iName])) {
			$strArg = $this->vArgs[$iName];
			$strOut = W3GetExpr($strArg);
			if ($iTrace) {
				W3AddTrace(' - '.strtoupper($iName).'=[<b>'.$strArg.'</b>] -> [<b>'.$strOut.'</b>]');
			}
			return $strOut;
		} else {
			return NULL;
		}
	}
}

/*
class W3TPL_fx {
	function runXploop ( &$parser, $inStr = '', $inTplt = '$s$', $inSep = '' ) 
	{
		$tok = substr ( $inStr, 0, 1);	// token for splitting
		if ($tok) {
			$tks = substr ( $inStr, 1 );	// tokenized string
			$list = explode ( $tok, $tks );	// split the string
			$sep = CharacterEscapes::charUnesc( $inSep, array(), $parser );
			$tplt = CharacterEscapes::charUnesc( $inTplt, array(), $parser );
			$out = '';
			foreach ($list as $value) {
		//		$lcnt++;
				if ($out) {
					$out .= $sep;
				}
				$out .= str_replace( '$s$', $value, $tplt );
			}
			return $parser->recursiveTagParse($out);
	//		return array($out, 'noparse' => false, 'isHTML' => false);
		} else {
			return NULL;
		}
	}
	function runXpCount ( &$parser, $inStr = '' ) 
	{
		$tok = substr ( $inStr, 0, 1);
		return substr_count($inStr, $tok);
	}
	function runLet ( &$parser, $iName = '', $iVal = '' ) {
		global $wgW3Vars;

		$strName = strtolower($iName);
		$strName = W3GetVal($strName);	// check for indirection ($)
		$wgW3Vars[$strName] .= $iVal;
	}
	function runGet ( &$parser, $iName = '' ) {
		$strName = strtolower($iName);
		$strVal = W3GetVal('$'.$strName);
		return $parser->recursiveTagParse($strVal);
	}
}
*/

/*
 Code for preventing raw "isolated" output from being further processed by the parser output stage
 See http://www.mediawiki.org/wiki/Manual:Tag_extensions#How_can_I_avoid_modification_of_my_extension.27s_HTML_output.3F
*/
$wgW3_Markers = array();
define('ksW3_parser_marker_start','@@W3TPL##--');
define('ksW3_parser_marker_stop','--##LPT3W@@');
function IsolateOutput($iText) {
	global $wgW3_Markers;

	$markCount = count($wgW3_Markers);
	$mark = ksW3_parser_marker_start.$markCount.ksW3_parser_marker_stop;
	$wgW3_Markers[$markCount] = $iText;
	return $mark;
}
 
function efW3_ParserAfterTidy(&$parser, &$text) {
	// find markers in $text
	// replace markers with actual output
	global $wgW3_Markers;

// replace markers with isolated text:
	$k = array();
	for ($i = 0; $i < count($wgW3_Markers); $i++)
		$k[] = ksW3_parser_marker_start . $i . ksW3_parser_marker_stop;
	$text = str_replace($k, $wgW3_Markers, $text);

	return true;
}
function efW3_OutputPageBeforeHTML(&$out, &$text) {
	global $wgW3_edit_queue;
// execute deferred edits:

	if (is_array($wgW3_edit_queue)) {
		foreach ($wgW3_edit_queue as $key=>$obj) {
			$obj->Exec();
		}
	}
	return TRUE;
}
class w3tpl_modules {
    private $arMods;
    private $fpBase;
    private $fpExt;
    private $strCurFile;	// file currently being loaded

    /*----
      INPUT: path to modules folder, including terminal slash
    */
    public function __construct($iPath,$iExt='.php') {
	$this->arMods = array();
	$this->fpBase = $iPath;
	$this->fpExt = $iExt;
    }
    /*----
      INPUT: base name of file - no extension, no path
    */
    public function Register($iFile) {
	$fs = $this->fpBase.$iFile.$this->fpExt;
	$this->strCurFile = $iFile;
	require_once($fs);
	$this->strCurFile = NULL;
    }
    public function Register_class(w3tpl_module $iModule) {
	$strKey = $this->strCurFile;
	$this->arMods[$strKey] = $iModule;
    }
    public function Dispatch($iModule, $iFunc, array $iArgs, Parser $iParser) {
	$strMod = $iModule;
	$objMod = $this->arMods[$strMod];
	$rtn = $objMod->Exec($iFunc,$iArgs,$iParser);
	if (is_null($rtn)) {
	    return "<b>Error</b>: Function [$iFunc] not found in module [$iModule].";
	} else {
	    return $rtn;
	}
    }
}
$wgW3Mods = new w3tpl_modules('plugins/');
abstract class w3tpl_module {
    protected $objParser;

    public function __construct() {
	global $wgW3Mods;

	$wgW3Mods->Register_class($this);
    }
    public function Exec($iFName,array $iArgs,Parser $iParser) {
	$strName = 'w3f_'.$iFName;
	$this->objParser = $iParser;

	if (method_exists($this, $strName)) {
	    $out = $this->$strName($iArgs);
	} else {
	    $out = NULL;
	}
	return $out;
    }
    protected function Parse_WikiText($iText) {
	$out = $this->objParser->recursiveTagParse($iText);
	return $out;
    }
}