2013-02-20 22:00:42 +00:00
|
|
|
<?php
|
2021-10-08 10:18:29 +00:00
|
|
|
|
2022-04-07 23:12:32 +00:00
|
|
|
namespace MediaWiki\Extension\Scribunto;
|
|
|
|
|
|
|
|
use CodeContentHandler;
|
|
|
|
use Content;
|
|
|
|
use ExtensionRegistry;
|
2021-10-08 10:18:29 +00:00
|
|
|
use MediaWiki\Content\Renderer\ContentParseParams;
|
2021-10-27 12:13:37 +00:00
|
|
|
use MediaWiki\Content\ValidationParams;
|
2024-01-05 18:24:06 +00:00
|
|
|
use MediaWiki\Html\Html;
|
2021-10-08 10:18:29 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2021-10-27 12:13:37 +00:00
|
|
|
use MediaWiki\Page\PageIdentity;
|
2024-01-05 18:24:06 +00:00
|
|
|
use MediaWiki\Parser\ParserOutput;
|
|
|
|
use MediaWiki\Status\Status;
|
2022-04-18 16:19:39 +00:00
|
|
|
use MediaWiki\SyntaxHighlight\SyntaxHighlight;
|
2023-08-19 18:18:41 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2022-04-07 23:12:32 +00:00
|
|
|
use TextContent;
|
2021-10-08 10:18:29 +00:00
|
|
|
|
2013-02-20 22:00:42 +00:00
|
|
|
/**
|
|
|
|
* Scribunto Content Handler
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @ingroup Scribunto
|
|
|
|
*
|
|
|
|
* @author Brad Jorsch <bjorsch@wikimedia.org>
|
|
|
|
*/
|
|
|
|
|
2014-09-15 08:43:15 +00:00
|
|
|
class ScribuntoContentHandler extends CodeContentHandler {
|
2013-02-20 22:00:42 +00:00
|
|
|
|
2014-10-10 09:02:03 +00:00
|
|
|
/**
|
|
|
|
* @param string $modelId
|
|
|
|
* @param string[] $formats
|
|
|
|
*/
|
2016-05-17 14:52:05 +00:00
|
|
|
public function __construct(
|
2017-06-15 17:19:00 +00:00
|
|
|
$modelId = CONTENT_MODEL_SCRIBUNTO, $formats = [ CONTENT_FORMAT_TEXT ]
|
2016-05-17 14:52:05 +00:00
|
|
|
) {
|
2013-02-20 22:00:42 +00:00
|
|
|
parent::__construct( $modelId, $formats );
|
|
|
|
}
|
|
|
|
|
2020-01-14 18:50:34 +00:00
|
|
|
/**
|
|
|
|
* @return string Class name
|
|
|
|
*/
|
2014-09-15 08:43:15 +00:00
|
|
|
protected function getContentClass() {
|
2020-01-14 18:50:34 +00:00
|
|
|
return ScribuntoContent::class;
|
2014-09-15 08:43:15 +00:00
|
|
|
}
|
|
|
|
|
2014-10-10 09:02:03 +00:00
|
|
|
/**
|
|
|
|
* @param string $format
|
|
|
|
* @return bool
|
|
|
|
*/
|
2013-04-29 20:51:39 +00:00
|
|
|
public function isSupportedFormat( $format ) {
|
|
|
|
// An error in an earlier version of Scribunto means we might see this.
|
|
|
|
if ( $format === 'CONTENT_FORMAT_TEXT' ) {
|
|
|
|
$format = CONTENT_FORMAT_TEXT;
|
|
|
|
}
|
|
|
|
return parent::isSupportedFormat( $format );
|
|
|
|
}
|
|
|
|
|
2015-07-24 14:17:45 +00:00
|
|
|
/**
|
|
|
|
* Only allow this content handler to be used in the Module namespace
|
|
|
|
* @param Title $title
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function canBeUsedOn( Title $title ) {
|
|
|
|
if ( $title->getNamespace() !== NS_MODULE ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::canBeUsedOn( $title );
|
|
|
|
}
|
2021-10-08 10:18:29 +00:00
|
|
|
|
2022-01-10 10:19:28 +00:00
|
|
|
/** @inheritDoc */
|
|
|
|
public function supportsPreloadContent(): bool {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-10-27 12:13:37 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function validateSave(
|
|
|
|
Content $content,
|
|
|
|
ValidationParams $validationParams
|
|
|
|
) {
|
|
|
|
'@phan-var ScribuntoContent $content';
|
|
|
|
return $this->validate( $content, $validationParams->getPageIdentity() );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether the script is valid
|
|
|
|
*
|
|
|
|
* @param TextContent $content
|
|
|
|
* @param PageIdentity $page
|
|
|
|
* @return Status
|
|
|
|
*/
|
|
|
|
public function validate( TextContent $content, PageIdentity $page ) {
|
|
|
|
if ( !( $page instanceof Title ) ) {
|
|
|
|
$titleFactory = MediaWikiServices::getInstance()->getTitleFactory();
|
2023-12-01 22:00:56 +00:00
|
|
|
$page = $titleFactory->newFromPageIdentity( $page );
|
2021-10-27 12:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$engine = Scribunto::newDefaultEngine();
|
|
|
|
$engine->setTitle( $page );
|
|
|
|
return $engine->validate( $content->getText(), $page->getPrefixedDBkey() );
|
|
|
|
}
|
|
|
|
|
2021-10-08 10:18:29 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
protected function fillParserOutput(
|
|
|
|
Content $content,
|
|
|
|
ContentParseParams $cpoParams,
|
2022-01-12 16:31:31 +00:00
|
|
|
ParserOutput &$parserOutput
|
2021-10-08 10:18:29 +00:00
|
|
|
) {
|
|
|
|
'@phan-var ScribuntoContent $content';
|
|
|
|
$page = $cpoParams->getPage();
|
2023-12-01 22:00:56 +00:00
|
|
|
$title = Title::newFromPageReference( $page );
|
2021-10-08 10:18:29 +00:00
|
|
|
$parserOptions = $cpoParams->getParserOptions();
|
|
|
|
$revId = $cpoParams->getRevId();
|
|
|
|
$generateHtml = $cpoParams->getGenerateHtml();
|
2022-06-20 06:51:19 +00:00
|
|
|
$parser = MediaWikiServices::getInstance()->getParserFactory()->getInstance();
|
2023-11-14 21:34:45 +00:00
|
|
|
$sourceCode = $content->getText();
|
|
|
|
$docTitle = Scribunto::getDocPage( $title );
|
|
|
|
$docMsg = $docTitle ? wfMessage(
|
|
|
|
$docTitle->exists() ? 'scribunto-doc-page-show' : 'scribunto-doc-page-does-not-exist',
|
|
|
|
$docTitle->getPrefixedText()
|
|
|
|
)->inContentLanguage() : null;
|
|
|
|
|
|
|
|
// Accumulate the following output:
|
|
|
|
// - docs (if any)
|
|
|
|
// - validation error (if any)
|
|
|
|
// - highlighted source code
|
|
|
|
$html = '';
|
|
|
|
|
2023-11-14 22:24:03 +00:00
|
|
|
if ( $docMsg && !$docMsg->isDisabled() ) {
|
|
|
|
// In order to allow the doc page to categorize the Module page,
|
|
|
|
// we need access to the ParserOutput of the doc page.
|
|
|
|
// This is why we can't simply use $docMsg->parse().
|
|
|
|
//
|
|
|
|
// We also can't use use ParserOutput::getText and ParserOutput::collectMetadata
|
|
|
|
// to merge the result into $parserOutput, because doing so would remove the
|
|
|
|
// ability for Skin/OutputPage to (post-cache) decide on the ParserOutput::getText
|
|
|
|
// parameters edit section links, TOC, and user language etc.
|
|
|
|
//
|
|
|
|
// So instead, this uses the doc page's ParserOutput as the actual ParserOutput
|
|
|
|
// we return, and add the other stuff to it. This is the only way to leave
|
|
|
|
// skin-decisions undecided and in-tact.
|
|
|
|
if ( $parserOptions->getTargetLanguage() === null ) {
|
|
|
|
$parserOptions->setTargetLanguage( $docTitle->getPageLanguage() );
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
2023-11-14 22:24:03 +00:00
|
|
|
$parserOutput = $parser->parse( $docMsg->plain(), $page, $parserOptions, true, true, $revId );
|
|
|
|
|
|
|
|
// Code is displayed and syntax highlighted as LTR, but the
|
|
|
|
// documentation can be RTL on RTL-language wikis.
|
|
|
|
//
|
|
|
|
// As long as we leave the $parserOutput in-tact, it will preserve the appropiate
|
|
|
|
// lang, dir, and class attributes (mw-content-ltr or mw-content-rtl) as needed
|
|
|
|
// for correct styling and accessiblity of the documentation page content.
|
|
|
|
// These will be applied when OutputPage eventually calls ParserOutput::getText()
|
|
|
|
$html .= $parserOutput->getRawText();
|
|
|
|
} else {
|
|
|
|
$parserOutput = new ParserOutput();
|
2023-11-14 21:34:45 +00:00
|
|
|
}
|
2021-10-08 10:18:29 +00:00
|
|
|
|
2023-11-14 21:34:45 +00:00
|
|
|
if ( $docTitle ) {
|
|
|
|
// Mark the doc page as transcluded, so that edits to the doc page will
|
|
|
|
// purge this Module page.
|
|
|
|
$parserOutput->addTemplate( $docTitle, $docTitle->getArticleID(), $docTitle->getLatestRevID() );
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the script, and include an error message and tracking
|
|
|
|
// category if it's invalid
|
2021-10-27 12:13:37 +00:00
|
|
|
$status = $this->validate( $content, $title );
|
2021-10-08 10:18:29 +00:00
|
|
|
if ( !$status->isOK() ) {
|
2023-11-14 21:34:45 +00:00
|
|
|
// FIXME: This uses a Status object, which in turn uses global RequestContext
|
|
|
|
// to localize the message. This would poison the ParserCache.
|
|
|
|
//
|
|
|
|
// But, this code is almost unreachable in practice because there has
|
|
|
|
// been no way to create a Module page with invalid content since 2014
|
|
|
|
// (we validate and abort on edit, undelete, content-model change etc.).
|
|
|
|
// See also T304381.
|
|
|
|
$html .= Html::rawElement( 'div', [ 'class' => 'errorbox' ],
|
|
|
|
$status->getHTML( 'scribunto-error-short', 'scribunto-error-long' )
|
2021-10-08 10:18:29 +00:00
|
|
|
);
|
|
|
|
$trackingCategories = MediaWikiServices::getInstance()->getTrackingCategories();
|
2022-01-12 16:31:31 +00:00
|
|
|
$trackingCategories->addTrackingCategory( $parserOutput, 'scribunto-module-with-errors-category', $page );
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( !$generateHtml ) {
|
2023-11-14 21:34:45 +00:00
|
|
|
// The doc page and validation error produce metadata and must happen
|
|
|
|
// unconditionally. The next step (syntax highlight) can be skipped if
|
|
|
|
// we don't actually need the HTML.
|
2024-02-15 03:32:55 +00:00
|
|
|
$parserOutput->setRawText( '' );
|
2021-10-08 10:18:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$engine = Scribunto::newDefaultEngine();
|
|
|
|
$engine->setTitle( $title );
|
2023-11-14 21:34:45 +00:00
|
|
|
$codeLang = $engine->getGeSHiLanguage();
|
|
|
|
$html .= $this->highlight( $sourceCode, $parserOutput, $codeLang );
|
2021-10-08 10:18:29 +00:00
|
|
|
|
2024-02-15 03:32:55 +00:00
|
|
|
$parserOutput->setRawText( $html );
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-11-14 21:34:45 +00:00
|
|
|
* Get syntax highlighted code and add metadata to output.
|
|
|
|
*
|
|
|
|
* If SyntaxHighlight is not possible, falls back to a `<pre>` element.
|
|
|
|
*
|
|
|
|
* @param string $source Source code
|
2022-01-12 16:31:31 +00:00
|
|
|
* @param ParserOutput $parserOutput
|
2023-11-14 21:34:45 +00:00
|
|
|
* @param string|false $codeLang
|
|
|
|
* @return string HTML
|
2021-10-08 10:18:29 +00:00
|
|
|
*/
|
2023-11-14 21:34:45 +00:00
|
|
|
private function highlight( $source, ParserOutput $parserOutput, $codeLang ) {
|
2024-03-10 12:42:04 +00:00
|
|
|
$useGeSHi = MediaWikiServices::getInstance()->getMainConfig()->get( 'ScribuntoUseGeSHi' );
|
2022-04-18 16:19:39 +00:00
|
|
|
if (
|
2024-03-10 12:42:04 +00:00
|
|
|
$useGeSHi && $codeLang && ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' )
|
2022-04-18 16:19:39 +00:00
|
|
|
) {
|
2023-11-14 21:34:45 +00:00
|
|
|
$status = SyntaxHighlight::highlight( $source, $codeLang, [ 'line' => true, 'linelinks' => 'L' ] );
|
2021-10-08 10:18:29 +00:00
|
|
|
if ( $status->isGood() ) {
|
|
|
|
// @todo replace addModuleStyles line with the appropriate call on
|
|
|
|
// SyntaxHighlight once one is created
|
2022-01-12 16:31:31 +00:00
|
|
|
$parserOutput->addModuleStyles( [ 'ext.pygments' ] );
|
|
|
|
$parserOutput->addModules( [ 'ext.pygments.linenumbers' ] );
|
2023-11-14 21:34:45 +00:00
|
|
|
return $status->getValue();
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
|
|
|
}
|
2023-11-14 21:34:45 +00:00
|
|
|
|
2023-11-14 23:05:54 +00:00
|
|
|
return Html::element( 'pre', [
|
|
|
|
// Same as CodeContentHandler
|
|
|
|
'lang' => 'en',
|
|
|
|
'dir' => 'ltr',
|
|
|
|
'class' => 'mw-code mw-script'
|
|
|
|
], "\n$source\n" );
|
2021-10-08 10:18:29 +00:00
|
|
|
}
|
2020-02-21 20:49:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a redirect version of the content
|
|
|
|
*
|
|
|
|
* @param Title $target
|
|
|
|
* @param string $text
|
|
|
|
* @return ScribuntoContent
|
|
|
|
*/
|
|
|
|
public function makeRedirectContent( Title $target, $text = '' ) {
|
|
|
|
return Scribunto::newDefaultEngine()->makeRedirectContent( $target, $text );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function supportsRedirects() {
|
|
|
|
return Scribunto::newDefaultEngine()->supportsRedirects();
|
|
|
|
}
|
2013-02-20 22:00:42 +00:00
|
|
|
}
|