Difference between revisions of "User:Eclecticdave"
Eclecticdave (talk | contribs) |
Eclecticdave (talk | contribs) |
||
(2 intermediate revisions by 2 users not shown) | |||
Line 7: | Line 7: | ||
'''What you need to know''' | '''What you need to know''' | ||
+ | * The patch will not help with the 'include' parameter - it is solely designed to address embedded templates. | ||
* Template escaping using ²{ }² is no longer possible, but is unnecessary on the new parser. You will need to remove the escapes. Any templates used in ''format/listseparators'', ''secseparators'', ''multisecseparators'' and any of the ''resultsheader/footer'' parameters will automatically be deferred and evaluated for each article returned. | * Template escaping using ²{ }² is no longer possible, but is unnecessary on the new parser. You will need to remove the escapes. Any templates used in ''format/listseparators'', ''secseparators'', ''multisecseparators'' and any of the ''resultsheader/footer'' parameters will automatically be deferred and evaluated for each article returned. | ||
* Substitution of tokens like %PAGE%, %COUNT% etc. within embedded templates is also no longer possible. I've implemented an alternative in the form of a new 'dplvar' parser function - use the form {{#dplvar:PAGE}} instead. | * Substitution of tokens like %PAGE%, %COUNT% etc. within embedded templates is also no longer possible. I've implemented an alternative in the form of a new 'dplvar' parser function - use the form {{#dplvar:PAGE}} instead. | ||
Line 450: | Line 451: | ||
+?> | +?> | ||
</pre> | </pre> | ||
+ | |||
+ | == Comment from [[User:Gero|Gero]] 11:29, 24 April 2008 (CEST) == | ||
+ | Thank you for your hard work, Dave! Obviously it will not be an easy endeavour to keep DPL´s current functionality up and running. I really hope that you will find a way through the jungle - because I am currently very busy with other things. Th eonly thing I can promise is that I will maintain DPL in the same way I did for the last 18 months or so -- once a stable version on 1.12 exists. But it sounds like horror to me that a given DPL version in future can EITHER be run with 1.11 or lower OR with 1.12 and later. | ||
+ | I cannot maintain two versions. IS there are chance to get this solved? | ||
+ | :11:29, 24 April 2008 (CEST) | ||
+ | |||
+ | I'll certainly keep tinkering with it. However I also have some other things vying for my time, which is the main reason why I posted the patch now, rather than wait until it was complete - to give others a chance to play with what I've done so far. | ||
+ | |||
+ | As for keeping it working on 1.11 - that could be tricky ... there's certainly going to be a fair amount of code specific to the parser in use. However, it should be possible to factor out the common bits such as the SQL. That's obviously a non-trivial undertaking, but I'll be happy to take a look when I get the time. | ||
+ | : [[User:Eclecticdave|Eclecticdave]] 23:09, 24 April 2008 (CEST) |
Latest revision as of 22:09, 24 April 2008
Experimental Patch for DPL on MW 1.12
The patch below is intended to be a "proof of concept" implementation of DPL under MW 1.12's new parser. It is far from feature complete and almost certainly has some bugs so user beware ;-)
The patch applies to DPL version 1.6.9.
What you need to know
- The patch will not help with the 'include' parameter - it is solely designed to address embedded templates.
- Template escaping using ²{ }² is no longer possible, but is unnecessary on the new parser. You will need to remove the escapes. Any templates used in format/listseparators, secseparators, multisecseparators and any of the resultsheader/footer parameters will automatically be deferred and evaluated for each article returned.
- Substitution of tokens like %PAGE%, %COUNT% etc. within embedded templates is also no longer possible. I've implemented an alternative in the form of a new 'dplvar' parser function - use the form instead.
- Due to a quirk in the new parser the first parameter to DPL is evaluated too early for template expansion to be intercepted (see bug 12842). Don't use format/listseparators etc in the first parameter!
- The patched DPL will not run on MW1.11 or earlier.
- Don't apply this patch to a running wiki - please! Re-read the bits about 'experimental' and 'proof of concept' if you find yourself thinking about doing this ;-)
diff --git a/DynamicPageList2.php b/DynamicPageList2.php index 9371862..81e34c0 100644 --- a/DynamicPageList2.php +++ b/DynamicPageList2.php @@ -312,6 +312,8 @@ class ExtDynamicPageList2 private static $allowedNamespaces = NULL; // to be initialized at first use of DPL2, array of all namespaces except Media and Special, because we cannot use the DB for these to generate dynamic page lists. // Cannot be customized. Use ExtDynamicPageList2::$options['namespace'] or ExtDynamicPageList2::$options['notnamespace'] for customization. + + /** * Map parameters to possible values. * A 'default' key indicates the default value for the parameter. @@ -768,7 +770,7 @@ class ExtDynamicPageList2 //------------------------------ parser #function #dplmatrix: $wgParser->setFunctionHook( 'dplmatrix', array( __CLASS__, 'dplMatrixParserFunction' ) ); //------------------------------ variant as a parser #function - $wgParser->setFunctionHook( 'dpl', array( __CLASS__, 'dplParserFunction' ) ); + $wgParser->setFunctionHook( 'dpl', array( __CLASS__, 'dplParserFunction' ), SFH_OBJECT_ARGS ); // Internationalization file require_once( 'DynamicPageList2.i18n.php' ); @@ -810,6 +812,7 @@ class ExtDynamicPageList2 $magicWords['dpl'] = array( 0, 'dpl' ); $magicWords['dplchapter'] = array( 0, 'dplchapter' ); $magicWords['dplmatrix'] = array( 0, 'dplmatrix' ); + $magicWords['dplvar'] = array( 0, 'dplvar' ); # unless we return true, other parser functions extensions won't get loaded. return true; } @@ -851,26 +854,25 @@ class ExtDynamicPageList2 return $parsedDPL; } - public static function dplParserFunction(&$parser) + public static function dplParserFunction(&$parser, $frame, $arg_list) { // callback for the parser function {{#dpl: $params = array(); $input=""; - $numargs = func_num_args(); + $numargs = count($arg_list); if ($numargs < 2) { $input = "#dpl: no arguments specified"; return str_replace('§','<','§pre>§nowiki>'.$input.'§/nowiki>§/pre>'); } - // fetch all user-provided arguments (skipping $parser) - $arg_list = func_get_args(); + // fetch all user-provided arguments + $input .= $arg_list[0] . "\n"; for ($i = 1; $i < $numargs; $i++) { - $p1 = $arg_list[$i]; - $input .= str_replace("\n","",$p1) ."\n"; + $input .= trim($frame->expand($arg_list[$i])) . "\n"; } // for debugging you may want to uncomment the following statement - // return str_replace('§','<','§pre>§nowiki>'.$input.'§/nowiki>§/pre>'); + //return str_replace('§','<','§pre>'.$input.'§/pre>'); // $dump1 = self::dumpParsedRefs($parser,"before DPL func"); @@ -878,7 +880,7 @@ class ExtDynamicPageList2 // $dump2 = self::dumpParsedRefs($parser,"after DPL func"); // return $dump1.$text.$dump2; - return self::dynamicPageList($input, $params, $parser, $reset, 'func'); + return self::dynamicPageList($arg_list, $frame, $params, $parser, $reset, 'func'); } public static function dplChapterParserFunction(&$parser, $text='', $heading=' ', $maxLength = -1, $page = '?page?', $link = 'default', $trim=false ) { @@ -960,6 +962,8 @@ class ExtDynamicPageList2 } } + + private static function dumpParsedRefs($parser,$label) { if (!preg_match("/Query Q/",$parser->mTitle->getText())) return ''; $text="\n<pre>$label:\n"; @@ -1037,7 +1041,7 @@ class ExtDynamicPageList2 } // The real callback function for converting the input text to HTML output - private static function dynamicPageList( $input, $params, &$parser, &$bReset, $calledInMode ) { + private static function dynamicPageList( &$arg_list, &$frame, $params, &$parser, &$bReset, $calledInMode ) { error_reporting(E_ALL); @@ -1177,6 +1181,10 @@ class ExtDynamicPageList2 $aMultiSecSeparators = explode(',', self::$options['multisecseparators']['default']); $iDominantSection = self::$options['dominantsection']['default']; + $oSecSeparators = NULL; + $oMultiSecSeparators = NULL; + $oListSeparators = NULL; + $_sOffset = self::$options['offset']['default']; $iOffset = ($_sOffset == '') ? 0: intval($_sOffset); @@ -1258,6 +1266,7 @@ class ExtDynamicPageList2 // ###### PARSE PARAMETERS ###### // we replace double angle brackets by < > ; thus we avoid premature tag expansion in the input + /* $input = str_replace('»','>',$input); $input = str_replace('«','<',$input); @@ -1270,6 +1279,7 @@ class ExtDynamicPageList2 $input = str_replace('}²','}}',$input); $aParams = explode("\n", $input); + */ $bIncludeUncat = false; // to check if pseudo-category of Uncategorized pages is included // version 0.9: @@ -1278,14 +1288,26 @@ class ExtDynamicPageList2 // parse the result recursively. This allows to build complex structures in the output // which are only understood by the parser if seen as a whole - foreach($aParams as $iParam => $sParam) { - $aParam = explode('=', $sParam, 2); + foreach($arg_list as $arg) { + // The first argument is always pre-parsed, as part of finding out it starts with '#dpl:'. + if (is_string($arg)) { + $aParam = explode('=', $arg, 2); if( count( $aParam ) < 2 ) { - if (trim($aParam[0])!='') $output .= $logger->escapeMsg(DPL2_i18n::WARN_UNKNOWNPARAM, $aParam[0]. " [missing '=']", implode(', ', array_keys(self::$options))); + if (trim($aParam[0])!='') + $output .= $logger->escapeMsg(DPL2_i18n::WARN_UNKNOWNPARAM, $aParam[0]. " [missing '=']", + implode(', ', array_keys(self::$options))); continue; } $sType = trim($aParam[0]); $sArg = trim($aParam[1]); + $oArg = NULL; + } + else { + $aParam = $arg->splitArg(); + $sType = trim($aParam{'name'}->node->nodeValue); + $oArg = $aParam{'value'}; + $sArg = trim($frame->expand($oArg)); + } if( $sType=='') { $output .= $logger->escapeMsg(DPL2_i18n::WARN_UNKNOWNPARAM, '[empty string]', implode(', ', array_keys(self::$options))); @@ -1935,6 +1957,7 @@ class ExtDynamicPageList2 $sArg = str_replace( '\n', "\n", $sArg ); $sArg = str_replace( "¶", "\n", $sArg ); // the paragraph delimiter is utf8-escaped $aListSeparators = explode (',', $sArg, 4); + $oListSeparators = $oArg; // mode=userformat will be automatically assumed $sPageListMode='userformat'; $sInlTxt = ''; @@ -1945,6 +1968,7 @@ class ExtDynamicPageList2 $sArg = str_replace( '\n', "\n", $sArg ); $sArg = str_replace( "¶", "\n", $sArg ); // the paragraph delimiter is utf8-escaped $aSecSeparators = explode (',',$sArg); + $oSecSeparators = $oArg; break; case 'multisecseparators': @@ -1952,6 +1976,7 @@ class ExtDynamicPageList2 $sArg = str_replace( '\n', "\n", $sArg ); $sArg = str_replace( "¶", "\n", $sArg ); // the paragraph delimiter is utf8-escaped $aMultiSecSeparators = explode (',',$sArg); + $oMultiSecSeparators = $oArg; break; case 'table': @@ -2015,21 +2040,27 @@ class ExtDynamicPageList2 break; case 'resultsheader': $sResultsHeader = $sArg; + $oResultsHeader = $oArg; break; case 'resultsfooter': $sResultsFooter = $sArg; + $oResultsFooter = $oArg; break; case 'noresultsheader': $sNoResultsHeader = $sArg; + $oNoResultsHeader = $oArg; break; case 'noresultsfooter': $sNoResultsFooter = $sArg; + $oNoResultsFooter = $oArg; break; case 'oneresultheader': $sOneResultHeader = $sArg; + $oOneResultHeader = $oArg; break; case 'oneresultfooter': $sOneResultFooter = $sArg; + $oOneResultFooter = $oArg; break; /** @@ -2244,6 +2275,8 @@ class ExtDynamicPageList2 // if 'table' parameter is set: derive values for listseparators, secseparators and multisecseparators $defaultTemplateSuffix='.default'; if ($sTable!='') { + $oSecSeparators = NULL; + $oMultiSecSeparators = NULL; $defaultTemplateSuffix=''; $sPageListMode='userformat'; $sInlTxt = ''; @@ -2288,7 +2321,9 @@ class ExtDynamicPageList2 $sSqlPage_size = ''; $sSqlPage_touched = ''; $sSqlCalcFoundRows = ''; - if ( isset($iCount) && $sGoal != 'categories' && strpos($sResultsHeader.$sResultsFooter,'%TOTALPAGES%')!==false) $sSqlCalcFoundRows = 'SQL_CALC_FOUND_ROWS'; + // Can't figure out a way of checking if TOTALPAGES is used on new parser/syntax + //if ( isset($iCount) && $sGoal != 'categories' && strpos($sResultsHeader.$sResultsFooter,'%TOTALPAGES%')!==false) $sSqlCalcFoundRows = 'SQL_CALC_FOUND_ROWS'; + if ( isset($iCount) && $sGoal != 'categories' ) $sSqlCalcFoundRows = 'SQL_CALC_FOUND_ROWS'; if ($sDistinctResultSet == 'false') $sSqlDistinct = ''; else $sSqlDistinct = 'DISTINCT'; $sSqlGroupBy = ''; @@ -2829,6 +2864,9 @@ class ExtDynamicPageList2 } if ($dbr->numRows( $res ) <= 0) { + if (isset($oNoResultsHeader)) $sNoResultsHeader = trim($frame->expand($oNoResultsHeader)); + if (isset($oNoResultsFooter)) $sNoResultsFooter = trim($frame->expand($oNoResultsFooter)); + if ($sNoResultsHeader != '') $output .= str_replace( '\n', "\n", str_replace( "¶", "\n", $sNoResultsHeader)); if ($sNoResultsFooter != '') $output .= str_replace( '\n', "\n", str_replace( "¶", "\n", $sNoResultsFooter)); if ($sNoResultsHeader == '' && $sNoResultsFooter == '') $output .= $logger->escapeMsg(DPL2_i18n::WARN_NORESULTS); @@ -3021,10 +3059,12 @@ class ExtDynamicPageList2 // ###### SHOW OUTPUT ###### $listMode = new DPL2ListMode($sPageListMode, $aSecSeparators, $aMultiSecSeparators, $sInlTxt, $sListHtmlAttr, - $sItemHtmlAttr, $aListSeparators, $iOffset, $iDominantSection); + $sItemHtmlAttr, $aListSeparators, $iOffset, $iDominantSection, + $oSecSeparators, $oMultiSecSeparators, $oListSeparators, $frame); $hListMode = new DPL2ListMode($sHListMode, $aSecSeparators, $aMultiSecSeparators, '', $sHListHtmlAttr, - $sHItemHtmlAttr, $aListSeparators, $iOffset, $iDominantSection); + $sHItemHtmlAttr, $aListSeparators, $iOffset, $iDominantSection, + $oSecSeparators, $oMultiSecSeparators, $oListSeparators, $frame); $dpl = new DPL2($aHeadings, $bHeadingCount, $iColumns, $iRows, $iRowSize, $sRowColFormat, $aArticles, $aOrderMethods[0], $hListMode, $listMode, $bEscapeLinks, $bIncPage, $iIncludeMaxLen, @@ -3032,8 +3072,18 @@ class ExtDynamicPageList2 $iTitleMaxLen, $defaultTemplateSuffix, $aTableRow, $bIncludeTrim); if ($rowcount == -1) $rowcount = $dpl->getRowCount(); + $dpl->setTotalRows($rowcount); $dpl2result = $dpl->getText(); $header=''; + + // Re-evaluate templates + if (isset($oOneResultHeader)) $sOneResultHeader = trim($frame->expand($oOneResultHeader)); + if (isset($oOneResultFooter)) $sOneResultFooter = trim($frame->expand($oOneResultFooter)); + if (isset($oNoResultsHeader)) $sNoResultsHeader = trim($frame->expand($oNoResultsHeader)); + if (isset($oNoResultsFooter)) $sNoResultsFooter = trim($frame->expand($oNoResultsFooter)); + if (isset($oResultsHeader)) $sResultsHeader = trim($frame->expand($oResultsHeader)); + if (isset($oResultsFooter)) $sResultsFooter = trim($frame->expand($oResultsFooter)); + if ($sOneResultHeader != '' && $rowcount==1) { $header = str_replace('%PAGES%',1,$sOneResultHeader); } else if ($rowcount==0) { @@ -3253,9 +3303,14 @@ class DPL2ListMode { var $sSectionTags = array(); var $aMultiSecSeparators = array(); var $iDominantSection = -1; + var $oSecSeparators = NULL; + var $oMultiSecSeparators = NULL; + var $oListSeparators = NULL; + var $frame = NULL; function DPL2ListMode($listmode, $secseparators, $multisecseparators, $inlinetext, $listattr = '', $itemattr = '', - $listseparators, $iOffset, $dominantSection) { + $listseparators, $iOffset, $dominantSection, + $oSecSeparators, $oMultiSecSeparators, $oListSeparators, $frame) { // default for inlinetext (if not in mode=userformat) if (($listmode != 'userformat') && ($inlinetext == '')) $inlinetext = ' - '; @@ -3265,6 +3320,10 @@ class DPL2ListMode { $this->sSectionTags = $secseparators; $this->aMultiSecSeparators = $multisecseparators; + $this->oSecSeparators = $oSecSeparators; + $this->oMultiSecSeparators = $oMultiSecSeparators; + $this->oListSeparators = $oListSeparators; + $this->frame = $frame; $this->iDominantSection = $dominantSection - 1; // 0 based index switch ($listmode) { @@ -3325,12 +3384,14 @@ class DPL2ListMode { class DPL2 { + private static $totalRows = 0; + private static $article = NULL; + private static $mEscapeLinks; // whether to escape img/cat or not var $mArticles; var $mHeadingType; // type of heading: category, user, etc. (depends on 'ordermethod' param) var $mHListMode; // html list mode for headings var $mListMode; // html list mode for pages - var $mEscapeLinks; // whether to escape img/cat or not var $mIncPage; // true only if page transclusion is enabled var $mIncMaxLen; // limit for text to include var $mIncSecLabels = array(); // array of labels of sections to transclude @@ -3347,15 +3408,61 @@ class DPL2 { var $nameSpaces; var $mTableRow; // formatting rules for table fields + public static function dplVarParserFunction(&$parser, $name) { + $arg_list = func_get_args(); + + if (strcasecmp($name, 'PAGE') == 0) { + $pagename = self::$article->mTitle->getPrefixedText(); + if (self::$mEscapeLinks && (self::$article->mNamespace==14 || self::$article->mNamespace==6) ) { + // links to categories or images need an additional ":" + $pagename = ':'.$pagename; + } + return $pagename; + } + if (strcasecmp($name, 'TITLE') == 0) { + $title = self::$article->mTitle->getText(); + // TODO + //if (strpos($title,'%TITLE')>=0) { + // if ($this->mReplaceInTitle[0]!='') $title = preg_replace($this->mReplaceInTitle[0],$this->mReplaceInTitle[1],$title); + // if( isset($titleMaxLength) && (strlen($title) > $titleMaxLength)) $title = substr($title, 0, $titleMaxLength) . '...'; + //} + return $title; + } + if (strcasecmp($name, 'PAGES') == 0) { + return self::$totalRows; + } + if (strcasecmp($name, 'TOTALPAGES') == 0) { + return self::$totalRows; + } + if (strcasecmp($name, 'VERSION') == 0) { + return self::VERSION; + } + // Can't just put commas in here because listseparators/format also + // uses them and its all parsed in one big lump in the new parser + // So, we use a special escape \c and replace it later. + if (strcasecmp($name, 'CATLIST') == 0) { + return implode('\c', self::$article->mCategoryLinks); + } + if (strcasecmp($name, 'CATNAMES') == 0) { + return implode('\c', self::$article->mCategoryTexts); + } + + // If we get here, just return the value 180 for now. + return "180"; + } + function DPL2($headings, $bHeadingCount, $iColumns, $iRows, $iRowSize, $sRowColFormat, $articles, $headingtype, $hlistmode, $listmode, $bescapelinks, $includepage, $includemaxlen, $includeseclabels, $includeseclabelsmatch, $includeseclabelsnotmatch, $includematchparsed, &$parser, $logger, $replaceInTitle, $iTitleMaxLen, $defaultTemplateSuffix, $aTableRow, $bIncludeTrim ) { - global $wgContLang; + global $wgContLang, $wgParser; + + $wgParser->setFunctionHook( 'dplvar', array( __CLASS__, 'dplVarParserFunction' ) ); + $this->nameSpaces = $wgContLang->getNamespaces(); $this->mArticles = $articles; $this->mListMode = $listmode; - $this->mEscapeLinks = $bescapelinks; + self::$mEscapeLinks = $bescapelinks; $this->mIncPage = $includepage; if($includepage) { $this->mIncSecLabels = $includeseclabels; @@ -3560,8 +3667,45 @@ class DPL2 { // the following statement caused a problem with multiple columns: $this->filteredCount = 0; for ($i = $iStart; $i < $iStart+$iCount; $i++) { $article = $this->mArticles[$i]; + + self::$article = &$article; + if (isset($mode->oSecSeparators)) + $mode->aSecSeparators = explode(',', $mode->frame->expand($mode->oSecSeparators)); + if (isset($mode->oMultiSecSeparators)) + $mode->aMultiSecSeparators = explode(',', $mode->frame->expand($mode->oMultiSecSeparators)); + if (isset($mode->oListSeparators)) + $mode->aListSeparators = explode(',', $mode->frame->expand($mode->oListSeparators)); + + // Convert newlines, plus \c escapes used in CATLIST and CATNAMES + switch(count($mode->aListSeparators)) { + case 4: + $mode->aListSeparators[3] = str_replace( '\n', "\n", str_replace( "¶", "\n", $mode->aListSeparators[3] )); + $mode->aListSeparators[3] = str_replace( '\c', ", ", $mode->aListSeparators[3] ); + case 3: + $mode->aListSeparators[2] = str_replace( '\n', "\n", str_replace( "¶", "\n", $mode->aListSeparators[2] )); + $mode->aListSeparators[2] = str_replace( '\c', ", ", $mode->aListSeparators[2] ); + case 2: + $mode->aListSeparators[1] = str_replace( '\n', "\n", str_replace( "¶", "\n", $mode->aListSeparators[1] )); + $mode->aListSeparators[1] = str_replace( '\c', ", ", $mode->aListSeparators[1] ); + case 1: + $mode->aListSeparators[0] = str_replace( '\n', "\n", str_replace( "¶", "\n", $mode->aListSeparators[0] )); + $mode->aListSeparators[0] = str_replace( '\c', ", ", $mode->aListSeparators[0] ); + } + + /* Taken from DPLListMode ... */ + switch(count($mode->aListSeparators)) { + case 4: + $mode->sListEnd = $mode->aListSeparators[3]; + case 3: + $mode->sItemEnd = $mode->aListSeparators[2]; + case 2: + $mode->sItemStart = $mode->aListSeparators[1]; + case 1: + $mode->sListStart = $mode->aListSeparators[0]; + } + $pagename = $article->mTitle->getPrefixedText(); - if ($this->mEscapeLinks && ($article->mNamespace==14 || $article->mNamespace==6) ) { + if (self::$mEscapeLinks && ($article->mNamespace==14 || $article->mNamespace==6) ) { // links to categories or images need an additional ":" $pagename = ':'.$pagename; } @@ -3823,6 +3967,10 @@ class DPL2 { return $this->filteredCount; } + function setTotalRows($rowcount) { + self::$totalRows = $rowcount; + } + //cut wiki text around lim function cutAt($lim,$text) { if ($lim<0) return $text; @@ -3958,4 +4106,4 @@ class DPL2Logger { } -?> \ No newline at end of file +?>
Comment from Gero 11:29, 24 April 2008 (CEST)
Thank you for your hard work, Dave! Obviously it will not be an easy endeavour to keep DPL´s current functionality up and running. I really hope that you will find a way through the jungle - because I am currently very busy with other things. Th eonly thing I can promise is that I will maintain DPL in the same way I did for the last 18 months or so -- once a stable version on 1.12 exists. But it sounds like horror to me that a given DPL version in future can EITHER be run with 1.11 or lower OR with 1.12 and later. I cannot maintain two versions. IS there are chance to get this solved?
- 11:29, 24 April 2008 (CEST)
I'll certainly keep tinkering with it. However I also have some other things vying for my time, which is the main reason why I posted the patch now, rather than wait until it was complete - to give others a chance to play with what I've done so far.
As for keeping it working on 1.11 - that could be tricky ... there's certainly going to be a fair amount of code specific to the parser in use. However, it should be possible to factor out the common bits such as the SQL. That's obviously a non-trivial undertaking, but I'll be happy to take a look when I get the time.
- Eclecticdave 23:09, 24 April 2008 (CEST)