Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 69 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
ScribuntoContentHandler | |
0.00% |
0 / 69 |
|
0.00% |
0 / 11 |
702 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getContentClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isSupportedFormat | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
canBeUsedOn | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
supportsPreloadContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateSave | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
validate | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
fillParserOutput | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
90 | |||
highlight | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
makeRedirectContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsRedirects | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto; |
4 | |
5 | use CodeContentHandler; |
6 | use Content; |
7 | use ExtensionRegistry; |
8 | use MediaWiki\Content\Renderer\ContentParseParams; |
9 | use MediaWiki\Content\ValidationParams; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\MediaWikiServices; |
12 | use MediaWiki\Page\PageIdentity; |
13 | use MediaWiki\Parser\ParserOutput; |
14 | use MediaWiki\Status\Status; |
15 | use MediaWiki\SyntaxHighlight\SyntaxHighlight; |
16 | use MediaWiki\Title\Title; |
17 | use TextContent; |
18 | |
19 | /** |
20 | * Scribunto Content Handler |
21 | * |
22 | * @file |
23 | * @ingroup Extensions |
24 | * @ingroup Scribunto |
25 | * |
26 | * @author Brad Jorsch <bjorsch@wikimedia.org> |
27 | */ |
28 | |
29 | class ScribuntoContentHandler extends CodeContentHandler { |
30 | |
31 | /** |
32 | * @param string $modelId |
33 | * @param string[] $formats |
34 | */ |
35 | public function __construct( |
36 | $modelId = CONTENT_MODEL_SCRIBUNTO, $formats = [ CONTENT_FORMAT_TEXT ] |
37 | ) { |
38 | parent::__construct( $modelId, $formats ); |
39 | } |
40 | |
41 | /** |
42 | * @return string Class name |
43 | */ |
44 | protected function getContentClass() { |
45 | return ScribuntoContent::class; |
46 | } |
47 | |
48 | /** |
49 | * @param string $format |
50 | * @return bool |
51 | */ |
52 | public function isSupportedFormat( $format ) { |
53 | // An error in an earlier version of Scribunto means we might see this. |
54 | if ( $format === 'CONTENT_FORMAT_TEXT' ) { |
55 | $format = CONTENT_FORMAT_TEXT; |
56 | } |
57 | return parent::isSupportedFormat( $format ); |
58 | } |
59 | |
60 | /** |
61 | * Only allow this content handler to be used in the Module namespace |
62 | * @param Title $title |
63 | * @return bool |
64 | */ |
65 | public function canBeUsedOn( Title $title ) { |
66 | if ( $title->getNamespace() !== NS_MODULE ) { |
67 | return false; |
68 | } |
69 | |
70 | return parent::canBeUsedOn( $title ); |
71 | } |
72 | |
73 | /** @inheritDoc */ |
74 | public function supportsPreloadContent(): bool { |
75 | return true; |
76 | } |
77 | |
78 | /** |
79 | * @inheritDoc |
80 | */ |
81 | public function validateSave( |
82 | Content $content, |
83 | ValidationParams $validationParams |
84 | ) { |
85 | '@phan-var ScribuntoContent $content'; |
86 | return $this->validate( $content, $validationParams->getPageIdentity() ); |
87 | } |
88 | |
89 | /** |
90 | * Checks whether the script is valid |
91 | * |
92 | * @param TextContent $content |
93 | * @param PageIdentity $page |
94 | * @return Status |
95 | */ |
96 | public function validate( TextContent $content, PageIdentity $page ) { |
97 | if ( !( $page instanceof Title ) ) { |
98 | $titleFactory = MediaWikiServices::getInstance()->getTitleFactory(); |
99 | $page = $titleFactory->newFromPageIdentity( $page ); |
100 | } |
101 | |
102 | $engine = Scribunto::newDefaultEngine(); |
103 | $engine->setTitle( $page ); |
104 | return $engine->validate( $content->getText(), $page->getPrefixedDBkey() ); |
105 | } |
106 | |
107 | /** |
108 | * @inheritDoc |
109 | */ |
110 | protected function fillParserOutput( |
111 | Content $content, |
112 | ContentParseParams $cpoParams, |
113 | ParserOutput &$parserOutput |
114 | ) { |
115 | '@phan-var ScribuntoContent $content'; |
116 | $page = $cpoParams->getPage(); |
117 | $title = Title::newFromPageReference( $page ); |
118 | $parserOptions = $cpoParams->getParserOptions(); |
119 | $revId = $cpoParams->getRevId(); |
120 | $generateHtml = $cpoParams->getGenerateHtml(); |
121 | $parser = MediaWikiServices::getInstance()->getParserFactory()->getInstance(); |
122 | $sourceCode = $content->getText(); |
123 | $docTitle = Scribunto::getDocPage( $title ); |
124 | $docMsg = $docTitle ? wfMessage( |
125 | $docTitle->exists() ? 'scribunto-doc-page-show' : 'scribunto-doc-page-does-not-exist', |
126 | $docTitle->getPrefixedText() |
127 | )->inContentLanguage() : null; |
128 | |
129 | // Accumulate the following output: |
130 | // - docs (if any) |
131 | // - validation error (if any) |
132 | // - highlighted source code |
133 | $html = ''; |
134 | |
135 | if ( $docMsg && !$docMsg->isDisabled() ) { |
136 | // In order to allow the doc page to categorize the Module page, |
137 | // we need access to the ParserOutput of the doc page. |
138 | // This is why we can't simply use $docMsg->parse(). |
139 | // |
140 | // We also can't use use ParserOutput::getText and ParserOutput::collectMetadata |
141 | // to merge the result into $parserOutput, because doing so would remove the |
142 | // ability for Skin/OutputPage to (post-cache) decide on the ParserOutput::getText |
143 | // parameters edit section links, TOC, and user language etc. |
144 | // |
145 | // So instead, this uses the doc page's ParserOutput as the actual ParserOutput |
146 | // we return, and add the other stuff to it. This is the only way to leave |
147 | // skin-decisions undecided and in-tact. |
148 | if ( $parserOptions->getTargetLanguage() === null ) { |
149 | $parserOptions->setTargetLanguage( $docTitle->getPageLanguage() ); |
150 | } |
151 | $parserOutput = $parser->parse( $docMsg->plain(), $page, $parserOptions, true, true, $revId ); |
152 | |
153 | // Code is displayed and syntax highlighted as LTR, but the |
154 | // documentation can be RTL on RTL-language wikis. |
155 | // |
156 | // As long as we leave the $parserOutput in-tact, it will preserve the appropiate |
157 | // lang, dir, and class attributes (mw-content-ltr or mw-content-rtl) as needed |
158 | // for correct styling and accessiblity of the documentation page content. |
159 | // These will be applied when OutputPage eventually calls ParserOutput::getText() |
160 | $html .= $parserOutput->getRawText(); |
161 | } else { |
162 | $parserOutput = new ParserOutput(); |
163 | } |
164 | |
165 | if ( $docTitle ) { |
166 | // Mark the doc page as transcluded, so that edits to the doc page will |
167 | // purge this Module page. |
168 | $parserOutput->addTemplate( $docTitle, $docTitle->getArticleID(), $docTitle->getLatestRevID() ); |
169 | } |
170 | |
171 | // Validate the script, and include an error message and tracking |
172 | // category if it's invalid |
173 | $status = $this->validate( $content, $title ); |
174 | if ( !$status->isOK() ) { |
175 | // FIXME: This uses a Status object, which in turn uses global RequestContext |
176 | // to localize the message. This would poison the ParserCache. |
177 | // |
178 | // But, this code is almost unreachable in practice because there has |
179 | // been no way to create a Module page with invalid content since 2014 |
180 | // (we validate and abort on edit, undelete, content-model change etc.). |
181 | // See also T304381. |
182 | $html .= Html::rawElement( 'div', [ 'class' => 'errorbox' ], |
183 | $status->getHTML( 'scribunto-error-short', 'scribunto-error-long' ) |
184 | ); |
185 | $trackingCategories = MediaWikiServices::getInstance()->getTrackingCategories(); |
186 | $trackingCategories->addTrackingCategory( $parserOutput, 'scribunto-module-with-errors-category', $page ); |
187 | } |
188 | |
189 | if ( !$generateHtml ) { |
190 | // The doc page and validation error produce metadata and must happen |
191 | // unconditionally. The next step (syntax highlight) can be skipped if |
192 | // we don't actually need the HTML. |
193 | $parserOutput->setRawText( '' ); |
194 | return; |
195 | } |
196 | |
197 | $engine = Scribunto::newDefaultEngine(); |
198 | $engine->setTitle( $title ); |
199 | $codeLang = $engine->getGeSHiLanguage(); |
200 | $html .= $this->highlight( $sourceCode, $parserOutput, $codeLang ); |
201 | |
202 | $parserOutput->setRawText( $html ); |
203 | } |
204 | |
205 | /** |
206 | * Get syntax highlighted code and add metadata to output. |
207 | * |
208 | * If SyntaxHighlight is not possible, falls back to a `<pre>` element. |
209 | * |
210 | * @param string $source Source code |
211 | * @param ParserOutput $parserOutput |
212 | * @param string|false $codeLang |
213 | * @return string HTML |
214 | */ |
215 | private function highlight( $source, ParserOutput $parserOutput, $codeLang ) { |
216 | $useGeSHi = MediaWikiServices::getInstance()->getMainConfig()->get( 'ScribuntoUseGeSHi' ); |
217 | if ( |
218 | $useGeSHi && $codeLang && ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) |
219 | ) { |
220 | $status = SyntaxHighlight::highlight( $source, $codeLang, [ 'line' => true, 'linelinks' => 'L' ] ); |
221 | if ( $status->isGood() ) { |
222 | // @todo replace addModuleStyles line with the appropriate call on |
223 | // SyntaxHighlight once one is created |
224 | $parserOutput->addModuleStyles( [ 'ext.pygments' ] ); |
225 | $parserOutput->addModules( [ 'ext.pygments.linenumbers' ] ); |
226 | return $status->getValue(); |
227 | } |
228 | } |
229 | |
230 | return Html::element( 'pre', [ |
231 | // Same as CodeContentHandler |
232 | 'lang' => 'en', |
233 | 'dir' => 'ltr', |
234 | 'class' => 'mw-code mw-script' |
235 | ], "\n$source\n" ); |
236 | } |
237 | |
238 | /** |
239 | * Create a redirect version of the content |
240 | * |
241 | * @param Title $target |
242 | * @param string $text |
243 | * @return ScribuntoContent |
244 | */ |
245 | public function makeRedirectContent( Title $target, $text = '' ) { |
246 | return Scribunto::newDefaultEngine()->makeRedirectContent( $target, $text ); |
247 | } |
248 | |
249 | /** |
250 | * @return bool |
251 | */ |
252 | public function supportsRedirects() { |
253 | return Scribunto::newDefaultEngine()->supportsRedirects(); |
254 | } |
255 | } |