Add module documentation support

Add the ability for modules to be documented using a /doc subpage, which
is automatically transcluded onto the module page.

To get the transcluding to work right, I wound up having to change from
the deprecated-in-1.21 ArticleViewCustom hook to ContentHandler, as
there didn't seem to be any other way to get the ParserOutput into the
links tables. Which means Scribunto now needs MediaWiki 1.21 rather
than 1.20.

Change-Id: Id487097c2a505c11f92a3404f5d3ee98beb2570c
This commit is contained in:
Brad Jorsch 2013-02-20 17:00:42 -05:00 committed by Gerrit Code Review
parent 00d4b711ed
commit 30a75fb0f1
6 changed files with 227 additions and 48 deletions

View file

@ -22,6 +22,11 @@ $messages['en'] = array(
'scribunto-error-long' => 'Script errors:
$1',
'scribunto-doc-subpage-name' => 'doc',
'scribunto-doc-subpage-does-not-exist' => "''Documentation for this module may be created at [[$1]]''",
'scribunto-doc-subpage-show' => '{{$1}}
<hr>',
'scribunto-doc-subpage-header' => "'''This is the documentation subpage for [[$1]]'''",
'scribunto-console-intro' => '* The module exports are available as the variable "p", including unsaved modifications.
* Precede a line with "=" to evaluate it as an expression, or use print().
@ -82,6 +87,10 @@ $messages['qqq'] = array(
'scribunto-console-intro' => 'An explanatory message shown to module programmers in the debug console, where they can run Lua commands and see how they work.
"Module exports" are the names that are exported. See the chapter [http://www.lua.org/pil/15.2.html Privacy] in the book "Programming in Lua".',
'scribunto-doc-subpage-name' => 'Subpage name for module documentation.',
'scribunto-doc-subpage-does-not-exist' => 'Message displayed if the documentation subpage does not exist. $1 is the prefixed title of the subpage.',
'scribunto-doc-subpage-show' => 'Message displayed if the documentation subpage does exist. $1 is the prefixed title of the subpage. Should probably transclude that page.',
'scribunto-doc-subpage-header' => 'Message displayed at the top of the documentation subpage. $1 is the prefixed title of the module.',
'scribunto-console-current-src' => 'Name of the fictional Lua module created in the debugging console. May appear e.g. in Lua error messages (like $1 in {{msg-mw|Scribunto-module-line}})',
'scribunto-console-cleared' => 'Message displayed in the console when the module source has been changed.',
'scribunto-console-cleared-session-lost' => 'Message displayed in the console when the session has expired.',

View file

@ -42,6 +42,8 @@ $wgAutoloadClasses['ScribuntoHooks'] = $dir.'common/Hooks.php';
$wgAutoloadClasses['ScribuntoException'] = $dir.'common/Common.php';
$wgAutoloadClasses['Scribunto'] = $dir.'common/Common.php';
$wgAutoloadClasses['ApiScribuntoConsole'] = $dir.'common/ApiScribuntoConsole.php';
$wgAutoloadClasses['ScribuntoContentHandler'] = $dir.'common/ScribuntoContentHandler.php';
$wgAutoloadClasses['ScribuntoContent'] = $dir.'common/ScribuntoContent.php';
$wgHooks['ParserFirstCallInit'][] = 'ScribuntoHooks::setupParserHook';
$wgHooks['ParserLimitReport'][] = 'ScribuntoHooks::reportLimits';
@ -49,17 +51,18 @@ $wgHooks['ParserClearState'][] = 'ScribuntoHooks::clearState';
$wgHooks['ParserCloned'][] = 'ScribuntoHooks::parserCloned';
$wgHooks['CanonicalNamespaces'][] = 'ScribuntoHooks::addCanonicalNamespaces';
$wgHooks['ArticleViewCustom'][] = 'ScribuntoHooks::handleScriptView';
$wgHooks['TitleIsWikitextPage'][] = 'ScribuntoHooks::isWikitextPage';
$wgHooks['CodeEditorGetPageLanguage'][] = 'ScribuntoHooks::getCodeLanguage';
$wgHooks['EditPageBeforeEditChecks'][] = 'ScribuntoHooks::beforeEditChecks';
$wgHooks['EditPageBeforeEditButtons'][] = 'ScribuntoHooks::beforeEditButtons';
$wgHooks['EditFilterMerged'][] = 'ScribuntoHooks::validateScript';
$wgHooks['ArticleViewHeader'][] = 'ScribuntoHooks::showDocSubpageHeader';
$wgHooks['ContentHandlerDefaultModelFor'][] = 'ScribuntoHooks::contentHandlerDefaultModelFor';
$wgHooks['UnitTestsList'][] = 'ScribuntoHooks::unitTestsList';
$wgParserTestFiles[] = $dir . 'tests/engines/LuaCommon/luaParserTests.txt';
$wgParserOutputHooks['ScribuntoError'] = 'ScribuntoHooks::parserOutputHook';
$wgContentHandlers['Scribunto'] = 'ScribuntoContentHandler';
$sbtpl = array(
'localBasePath' => dirname( __FILE__ ) . '/modules',

View file

@ -66,6 +66,35 @@ class Scribunto {
$parser->scribunto_engine = null;
}
}
/**
* Test whether the page should be considered a documentation subpage
* @param $title Title
* @return boolean
*/
public static function isDocSubpage( $title ) {
$docSubpage = wfMessage( 'scribunto-doc-subpage-name' );
if ( $docSubpage->isDisabled() ) {
return false;
}
$docSubpage = '/' . $docSubpage->plain();
return ( substr( $title->getText(), -strlen( $docSubpage ) ) === $docSubpage );
}
/**
* Return the Title for the documentation subpage
* @param $title Title
* @return Title|null
*/
public static function getDocSubpage( $title ) {
$docSubpage = wfMessage( 'scribunto-doc-subpage-name' );
if ( $docSubpage->isDisabled() ) {
return null;
}
return $title->getSubpage( $docSubpage->plain() );
}
}
/**

View file

@ -81,7 +81,7 @@ class ScribuntoHooks {
$moduleName = trim( $frame->expand( $args[0] ) );
$engine = Scribunto::getParserEngine( $parser );
$title = Title::makeTitleSafe( NS_MODULE, $moduleName );
if ( !$title ) {
if ( !$title || Scribunto::isDocSubpage( $title ) ) {
throw new ScribuntoException( 'scribunto-common-nosuchmodule' );
}
$module = $engine->fetchModuleFromParser( $title );
@ -126,45 +126,6 @@ class ScribuntoHooks {
}
}
/**
* Overrides the standard view for modules. Enables syntax highlighting when
* possible.
*
* @param $text string
* @param $title Title
* @param $output OutputPage
* @return bool
*/
public static function handleScriptView( $text, $title, $output ) {
global $wgScribuntoUseGeSHi;
if( $title->getNamespace() == NS_MODULE ) {
$engine = Scribunto::newDefaultEngine();
$language = $engine->getGeSHiLanguage();
if( $wgScribuntoUseGeSHi && $language ) {
$geshi = SyntaxHighlight_GeSHi::prepare( $text, $language );
$geshi->set_language( $language );
if( $geshi instanceof GeSHi && !$geshi->error() ) {
$code = $geshi->parse_code();
if( $code ) {
$output->addHeadItem( "source-{$language}", SyntaxHighlight_GeSHi::buildHeadItem( $geshi ) );
$output->addHTML( "<div dir=\"ltr\">{$code}</div>" );
return false;
}
}
}
// No GeSHi, or GeSHi can't parse it, use plain <pre>
$output->addHTML( "<pre class=\"mw-code mw-script\" dir=\"ltr\">\n" );
$output->addHTML( htmlspecialchars( $text ) );
$output->addHTML( "\n</pre>\n" );
return false;
} else {
return true;
}
}
/**
* @param $title Title
* @param $lang string
@ -172,7 +133,9 @@ class ScribuntoHooks {
*/
public static function getCodeLanguage( $title, &$lang ) {
global $wgScribuntoUseCodeEditor;
if( $wgScribuntoUseCodeEditor && $title->getNamespace() == NS_MODULE ) {
if( $wgScribuntoUseCodeEditor && $title->getNamespace() == NS_MODULE &&
!Scribunto::isDocSubpage( $title )
) {
$engine = Scribunto::newDefaultEngine();
if( $engine->getCodeEditorLanguage() ) {
$lang = $engine->getCodeEditorLanguage();
@ -184,14 +147,14 @@ class ScribuntoHooks {
}
/**
* Indicates that modules are not wikitext.
* Set the Scribunto content handler for modules
* @param $title Title
* @param $result
* @param &$model string
* @return bool
*/
public static function isWikitextPage( $title, &$result ) {
if( $title->getNamespace() == NS_MODULE ) {
$result = false;
public static function contentHandlerDefaultModelFor( $title, &$model ) {
if( $title->getNamespace() == NS_MODULE && !Scribunto::isDocSubpage( $title ) ) {
$model = 'Scribunto';
return false;
}
return true;
@ -232,6 +195,10 @@ class ScribuntoHooks {
return true;
}
if ( Scribunto::isDocSubpage( $editor->getTitle() ) ) {
return true;
}
$req = RequestContext::getMain()->getRequest();
$name = 'scribunto_ignore_errors';
@ -262,6 +229,10 @@ class ScribuntoHooks {
return true;
}
if ( Scribunto::isDocSubpage( $editor->getTitle() ) ) {
return true;
}
unset( $buttons['preview'] );
return true;
}
@ -281,6 +252,10 @@ class ScribuntoHooks {
return true;
}
if ( Scribunto::isDocSubpage( $title ) ) {
return true;
}
$req = RequestContext::getMain()->getRequest();
if ( $req->getBool( 'scribunto_ignore_errors' ) ) {
return true;
@ -343,4 +318,24 @@ WIKI;
Xml::encodeJsCall( 'mw.scribunto.setErrors', array( $parserOutput->scribunto_errors ) )
. '});' );
}
/**
* @param &$article Article
* @param &$outputDone boolean
* @param &$pcache boolean
* @return boolean
*/
public static function showDocSubpageHeader( &$article, &$outputDone, &$pcache ) {
global $wgOut;
$title = $article->getTitle();
if( $title->getNamespace() === NS_MODULE && Scribunto::isDocSubpage( $title ) ) {
$docSubpage = wfMessage( 'scribunto-doc-subpage-name' )->plain();
$title = substr( $title, 0, -strlen( $docSubpage ) - 1 );
$wgOut->addHTML(
wfMessage( 'scribunto-doc-subpage-header', $title )->parseAsBlock()
);
}
return true;
}
}

105
common/ScribuntoContent.php Normal file
View file

@ -0,0 +1,105 @@
<?php
/**
* Scribunto Content Model
*
* @file
* @ingroup Extensions
* @ingroup Scribunto
*
* @author Brad Jorsch <bjorsch@wikimedia.org>
*/
/**
* Represents the content of a Scribunto script page
*/
class ScribuntoContent extends TextContent {
function __construct( $text ) {
parent::__construct( $text, 'Scribunto' );
}
/**
* Parse the Content object and generate a ParserOutput from the result.
*
* @param $title Title The page title to use as a context for rendering
* @param $revId null|int The revision being rendered (optional)
* @param $options null|ParserOptions Any parser options
* @param $generateHtml boolean Whether to generate HTML (default: true).
* @return ParserOutput
*/
public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
global $wgParser, $wgScribuntoUseGeSHi;
$text = $this->getNativeData();
$output = null;
if ( !$options ) {
//NOTE: use canonical options per default to produce cacheable output
$options = $this->getContentHandler()->makeParserOptions( 'canonical' );
}
// Get documentation, if any
$output = new ParserOutput();
$doc = Scribunto::getDocSubpage( $title );
if ( $doc ) {
$msg = wfMessage(
$doc->exists() ? 'scribunto-doc-subpage-show' : 'scribunto-doc-subpage-does-not-exist',
$doc->getPrefixedText()
)->inContentLanguage();
if ( !$msg->isDisabled() ) {
// We need the ParserOutput for categories and such, so we
// can't use $msg->parse().
$output = $wgParser->parse( $msg->plain(), $title, $options, true, true, $revId );
}
// Mark the /doc subpage as a transclusion, so we get purged when
// it changes.
$output->addTemplate( $doc, $doc->getArticleID(), $doc->getLatestRevID() );
}
if ( !$generateHtml ) {
// We don't need the actual HTML
$output->setText( '' );
return $output;
}
// Add HTML for the actual script
$engine = Scribunto::newDefaultEngine();
$language = $engine->getGeSHiLanguage();
if( $wgScribuntoUseGeSHi && $language ) {
$geshi = SyntaxHighlight_GeSHi::prepare( $text, $language );
$geshi->set_language( $language );
if( $geshi instanceof GeSHi && !$geshi->error() ) {
$code = $geshi->parse_code();
if( $code ) {
$output->addHeadItem( SyntaxHighlight_GeSHi::buildHeadItem( $geshi ), "source-{$language}" );
$output->setText( $output->getText() . "<div dir=\"ltr\">{$code}</div>" );
return $output;
}
}
}
// No GeSHi, or GeSHi can't parse it, use plain <pre>
$output->setText( $output->getText() .
"<pre class=\"mw-code mw-script\" dir=\"ltr\">\n" .
htmlspecialchars( $text ) .
"\n</pre>\n"
);
return $output;
}
/**
* Returns a Content object with pre-save transformations applied (or this
* object if no transformations apply).
*
* @param $title Title
* @param $user User
* @param $parserOptions null|ParserOptions
* @return Content
*/
public function preSaveTransform( Title $title, User $user, ParserOptions $parserOptions ) {
return $this;
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* Scribunto Content Handler
*
* @file
* @ingroup Extensions
* @ingroup Scribunto
*
* @author Brad Jorsch <bjorsch@wikimedia.org>
*/
class ScribuntoContentHandler extends TextContentHandler {
public function __construct( $modelId = 'Scribunto', $formats = array( 'CONTENT_FORMAT_TEXT' ) ) {
parent::__construct( $modelId, $formats );
}
/**
* Unserializes a ScribuntoContent object.
*
* @param $text string Serialized form of the content
* @param $format null|string The format used for serialization
* @return Content the ScribuntoContent object wrapping $text
*/
public function unserializeContent( $text, $format = null ) {
$this->checkFormat( $format );
return new ScribuntoContent( $text );
}
/**
* Creates an empty ScribuntoContent object.
*
* @return Content
*/
public function makeEmptyContent() {
return new ScribuntoContent( '' );
}
}