summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.phpcs.xml13
-rw-r--r--Code.php234
-rw-r--r--LICENSE21
-rw-r--r--README.md39
-rw-r--r--composer.json11
-rw-r--r--examples/languages.php12
-rw-r--r--examples/namespacesWithCodePages.php1
-rw-r--r--extension.json43
-rw-r--r--i18n/en.json21
10 files changed, 397 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..987e2a2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+composer.lock
+vendor
diff --git a/.phpcs.xml b/.phpcs.xml
new file mode 100644
index 0000000..ed598e5
--- /dev/null
+++ b/.phpcs.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<ruleset>
+ <rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
+ <exclude name="Generic.Files.OneObjectStructurePerFile.MultipleFound"/>
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationProtected"/>
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic"/>
+ <exclude name="MediaWiki.Files.ClassMatchesFilename.NotMatch"/>
+ </rule>
+ <file>.</file>
+ <exclude-pattern>/examples/*</exclude-pattern>
+ <arg name="extensions" value="php"/>
+ <arg name="encoding" value="UTF-8"/>
+</ruleset>
diff --git a/Code.php b/Code.php
new file mode 100644
index 0000000..ca02b3c
--- /dev/null
+++ b/Code.php
@@ -0,0 +1,234 @@
+<?php
+namespace MediaWiki\Extension\Code;
+
+use Content;
+use ErrorPageError;
+use ExtensionRegistry;
+use Html;
+use MediaWiki\Content\Renderer\ContentParseParams;
+use MediaWiki\MediaWikiServices;
+use Parser;
+use ParserOutput;
+use Sanitizer;
+use SpecialPage;
+use TextContent;
+use TextContentHandler;
+use Title;
+use WikiPage;
+use Xml;
+
+class CodeHooks {
+ public static function onParserFirstCallInit( Parser $parser ) {
+ global $wgCode_languages;
+ foreach ( $wgCode_languages as $lang ) {
+ $parser->setHook(
+ $lang['tag'],
+ fn ( $input, $args ) => renderCode( $input, $lang, $parser->getOutput(), $args )
+ );
+ }
+ }
+
+ public static function onContentHandlerDefaultModelFor( Title $title, &$model ) {
+ if ( getLangByPageName( $title->getDBkey() ) ) {
+ $model = CodeContent::MODEL;
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+
+function getLangByPageName( string $dbKey ) {
+ global $wgCode_languages;
+ $suffix = '.' . pathinfo( $dbKey, PATHINFO_EXTENSION );
+ return current( array_filter( $wgCode_languages, fn( $lang ) => $lang['suffix'] == $suffix ) );
+}
+
+function pre( $code, $attr ) {
+ return Xml::element( 'pre', Sanitizer::validateTagAttributes( $attr, 'pre' ), $code );
+}
+
+/**
+ * Render the given code and return the HTML.
+ *
+ * @param string $code The code to be rendered.
+ * @param ? $lang The language configuration.
+ * @param OutputPage|ParserOutput|null $outputPage
+ * @param ?array $tagAttrs Associative array of tag attributes or null in case of a code page.
+ * @return string The rendered HTML.
+ */
+function renderCode( string $code, $lang, $outputPage, ?array $tagAttrs = null ) {
+ if ( ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) ) {
+ $result = \SyntaxHighlight::highlight( $code, $lang['pygmentsLexer'], $tagAttrs ?? [ 'line' => true ] );
+ if ( $result->isOk() ) {
+ $out = $result->getValue();
+ $outputPage->addModuleStyles( [ 'ext.pygments' ] );
+ } else {
+ $out = pre( $code, $tagAttrs );
+ }
+ } else {
+ $out = pre( $code, $tagAttrs );
+ }
+
+ foreach ( $lang['linkifiers'] as $regex => $href ) {
+ $out = preg_replace_callback( $regex, fn( $match ) =>
+ Xml::element( 'a', [
+ 'href' => str_replace( '$1', $match[0], $href ),
+ 'tabindex' => -1
+ ], $match[0] ),
+ $out );
+ }
+
+ foreach ( $lang['actions'] as $action => $url ) {
+ if ( str_contains( $url, '$url' ) ) {
+ // FUTURE: it would be nice to still display such actions for code pages
+ continue;
+ }
+
+ $encodedCode = rawurlencode( trim( $code ) );
+
+ // The wfMessage allows admins to create MediaWiki:codeaction-label with a template call
+ // if they want to enable their users to customize or localize the code action labels.
+ $out .= Xml::openElement( 'a', [ 'href' => str_replace( '$code', $encodedCode, $url ) ] );
+ $out .= wfMessage( "codeaction-label", $action )->parse();
+ $out .= Xml::closeElement( 'a' ) . ' ';
+ }
+
+ return $out;
+}
+
+class ContentHandler extends TextContentHandler {
+ public function __construct( $modelId = CodeContent::MODEL ) {
+ parent::__construct( $modelId, [ CONTENT_FORMAT_TEXT ] );
+ }
+
+ public function fillParserOutput( Content $content, ContentParseParams $params, ParserOutput &$output ) {
+ $lang = getLangByPageName( $params->getPage()->getDBkey() );
+ $out = '';
+ if ( !$lang ) {
+ global $wgCode_languages;
+ $supportedSuffixes = implode( ', ', array_map( fn( $lang ) => $lang['suffix'], $wgCode_languages ) );
+
+ MediaWikiServices::getInstance()->getTrackingCategories()
+ ->addTrackingCategory( $output, 'codepage-error-category', $params->getPage() );
+
+ $out = Html::errorBox( wfMessage( 'codepage-unknown-suffix', 'code', "[$supportedSuffixes]" ) );
+ $lang = [
+ 'pygmentsLexer' => 'text',
+ 'actions' => [],
+ 'linkifiers' => [],
+ ];
+ }
+ $out .= renderCode( $content->getText(), $lang, $output );
+ $output->setText( $out );
+ }
+
+ protected function getContentClass() {
+ return CodeContent::class;
+ }
+}
+
+class CodeContent extends TextContent {
+ // Has to match the ContentHandlers entry in extension.json.
+ public const MODEL = 'code';
+
+ public function __construct( $text, $model_id = self::MODEL ) {
+ parent::__construct( $text, $model_id );
+ }
+}
+
+class SpecialCodeAction extends SpecialPage {
+ public function __construct() {
+ parent::__construct( 'CodeAction' );
+ }
+
+ public function execute( $par ) {
+ $namespacesWithCodePages = $this->getConfig()->get( 'Code_namespacesWithCodePages' );
+ $languages = $this->getConfig()->get( 'Code_languages' );
+
+ if ( empty( $namespacesWithCodePages ) ) {
+ $exampleConfig = htmlspecialchars( file_get_contents( __DIR__ . '/examples/namespacesWithCodePages.php' ) );
+ throw new ErrorPageError( 'codeactions-unavailable-title', 'codeactions-unavailable-no-namespaces', "<pre>$exampleConfig</pre>" );
+ }
+
+ $exampleLang = current( array_filter( $languages, fn( $lang ) => count( $lang['actions'] ) > 0 ) );
+ if ( !$exampleLang ) {
+ $exampleConfig = htmlspecialchars( file_get_contents( __DIR__ . '/examples/languages.php' ) );
+ throw new ErrorPageError( 'codeactions-unavailable-title', 'codeactions-unavailable-no-lang-with-actions', "<pre>$exampleConfig</pre>" );
+ }
+
+ $contLang = MediaWikiServices::getInstance()->getContentLanguage();
+
+ $output = $this->getOutput();
+ $args = explode( '/', $par, 2 );
+
+ if ( count( $args ) != 2 ) {
+ $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
+
+ $title = $formatter->formatTitle( array_keys( $namespacesWithCodePages )[0], 'Example' . $exampleLang['suffix'] );
+ $action = htmlspecialchars( array_keys( $exampleLang['actions'] )[0] );
+ $output->setPageTitle( $this->msg( 'notargettitle' ) );
+ $output->addWikiMsg( 'codeaction-howto', $this->getPageTitle(), $action, $title );
+ $output->addWikiTextAsInterface(
+ Xml::element( 'h2', null, $this->msg( 'codeactions-available' ) )
+ );
+ $output->addHTML(
+ Xml::buildTable(
+ array_map( fn ( $lang ) => [ $lang['suffix'], implode( ', ', array_keys( $lang['actions'] ) ) ], $languages ),
+ [ 'class' => 'wikitable' ], [ $this->msg( 'code-pagesuffix' ), $this->msg( 'codeactions-available' ) ] )
+ );
+ return;
+ }
+ [ $action, $pagename ] = $args;
+
+ $title = Title::newFromText( $pagename );
+ if ( !$title ) {
+ throw new ErrorPageError( 'badtitle', 'title-invalid' );
+ }
+
+ if ( !array_key_exists( $title->getNamespace(), $namespacesWithCodePages ) ) {
+ $nsName = $this->getContext()->getLanguage()->getFormattedNsText( $title->getNamespace() );
+ throw new ErrorPageError( 'codeaction-unsupported-namespace-title', 'codeaction-unsupported-namespace-text', $nsName );
+ }
+
+ $lang = getLangByPageName( $title->getDBkey() );
+ if ( !$lang ) {
+ $supportedSuffixes = implode( ', ', array_map( fn( $lang ) => $lang['suffix'], $languages ) );
+ throw new ErrorPageError( 'codeaction-unknown-suffix-title', 'codeaction-unknown-suffix-text', [ $pagename, "[$supportedSuffixes]" ] );
+ }
+
+ $url = $lang['actions'][$action] ?? null;
+
+ if ( !$url ) {
+ $supportedActions = implode( ', ', array_keys( $lang['actions'] ) );
+ throw new ErrorPageError( 'codeaction-unknown-action-title', 'codeaction-unknown-action-text', [ $action, "[$supportedActions]" ] );
+ }
+
+ if ( str_contains( $url, '$code' ) ) {
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+ $errors = $permissionManager->getPermissionErrors( 'read', $this->getUser(), $title );
+ if ( !empty( $errors ) ) {
+ $output->showPermissionsErrorPage( $errors, 'read' );
+ return;
+ }
+
+ $content = WikiPage::factory( $title )->getContent();
+ if ( $content == null ) {
+ throw new ErrorPageError( 'codeaction-page-not-found-title', 'codeaction-page-not-found-text', $title );
+ }
+ $content = ContentHandler::getContentText( $content );
+
+ $codePrefix = $lang['codePrefix'] ?? null;
+ if ( $codePrefix ) {
+ $content = str_replace( '$url', $title->getFullURL(), $codePrefix ) . "\n\n" . $content;
+ }
+
+ $url = str_replace( '$code', rawurlencode( $content ), $url );
+ } else {
+ $url = str_replace( '$url', rawurlencode( $title->getFullURL() ), $url );
+ }
+
+ $output->redirect( $url );
+ }
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d0099f3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2022 Martin Fischer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9370d1f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+# Extension:Code
+
+A MediaWiki extension that builds on [SyntaxHighlight]
+to provide the following (all in a configurable manner):
+
+* shorter tags
+ e.g. `<query>` instead of `<syntaxhighlight lang=sparql>`
+
+* code actions
+ e.g. automatically link the [WDQS] for SPARQL code blocks[^1]
+
+* code linkification
+ e.g. automatically link Wikidata identifiers in code blocks
+
+* code pages
+ e.g. automatically higlight pages with names ending in `.rq`
+ as SPARQL (and also display the code actions for them)
+
+Note that code actions are also linkable from other pages via the
+`Special:CodeAction` special page, e.g. `Special:CodeAction/run/Example.rq`
+attempts to execute the `run` action for the `Example.rq` code page
+and redirect the user accordingly.
+
+## Installation
+
+1. Place the extension in your extensions directory.
+2. Add `wfLoadExtension('Code');` to your `LocalSettings.php`.
+3. Visit `Special:CodeAction` it will tell you what other configuration you need.
+
+(If you want syntax highlighting via [SyntaxHighlight], additionally add
+`wfLoadExtension('SyntaxHighlight_GeSHi');` to your `LocalSettings.php`).
+
+
+[^1]: While this can also be achieved just via MediaWiki templates,
+this bears the problem that `|` has to be escaped as `{{!}}`, which can
+be quite annoying for languages like SPARQL that use `|` as an operator.
+
+[SyntaxHighlight]: https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:SyntaxHighlight
+[WDQS]: https://query.wikidata.org/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..a2224fb
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,11 @@
+{
+ "require-dev": {
+ "mediawiki/mediawiki-codesniffer": "40.0.1"
+ },
+ "scripts": {
+ "test": [
+ "phpcs -p -s"
+ ],
+ "fix": "phpcbf"
+ }
+}
diff --git a/examples/languages.php b/examples/languages.php
new file mode 100644
index 0000000..3710f86
--- /dev/null
+++ b/examples/languages.php
@@ -0,0 +1,12 @@
+$wgCode_languages[] = [
+ 'tag' => 'query',
+ 'pygmentsLexer' => 'sparql',
+ 'actions' => [
+ 'run' => 'https://query.wikidata.org/#$code',
+ 'embed' => 'https://query.wikidata.org/embed.html#$code',
+ ],
+ 'linkifiers' => [
+ '/\\b[QP][0-9]+\\b/' => 'https://www.wikidata.org/entity/$1',
+ ],
+ 'suffix' => '.rq',
+];
diff --git a/examples/namespacesWithCodePages.php b/examples/namespacesWithCodePages.php
new file mode 100644
index 0000000..b3eb4e1
--- /dev/null
+++ b/examples/namespacesWithCodePages.php
@@ -0,0 +1 @@
+$wgCode_namespacesWithCodePages[NS_MAIN] = true;
diff --git a/extension.json b/extension.json
new file mode 100644
index 0000000..cfa2c2b
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,43 @@
+{
+ "name": "Code",
+ "manifest_version": 2,
+ "type": "other",
+ "license-name": "MIT",
+ "author": "[https://push-f.com/ Martin Fischer]",
+ "url": "https://www.mediawiki.org/wiki/Extension:Code",
+ "version": "0.1.0",
+ "description": "Provides code pages, code actions and code linkification.",
+ "config_prefix": "wgCode_",
+ "config": {
+ "namespacesWithCodePages": {
+ "value": {},
+ "merge_strategy": "array_plus"
+ },
+ "languages": {
+ "value": {}
+ }
+ },
+ "MessagesDirs": {
+ "VoteExtension": [
+ "i18n"
+ ]
+ },
+ "AutoloadClasses": {
+ "MediaWiki\\Extension\\Code\\CodeHooks": "Code.php",
+ "MediaWiki\\Extension\\Code\\SpecialCodeAction": "Code.php",
+ "MediaWiki\\Extension\\Code\\ContentHandler": "Code.php"
+ },
+ "ContentHandlers": {
+ "code": "MediaWiki\\Extension\\Code\\ContentHandler"
+ },
+ "SpecialPages": {
+ "CodeAction": "MediaWiki\\Extension\\Code\\SpecialCodeAction"
+ },
+ "Hooks": {
+ "ParserFirstCallInit": "MediaWiki\\Extension\\Code\\CodeHooks::onParserFirstCallInit",
+ "ContentHandlerDefaultModelFor": "MediaWiki\\Extension\\Code\\CodeHooks::onContentHandlerDefaultModelFor"
+ },
+ "TrackingCategories": [
+ "codepage-error-category"
+ ]
+}
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..2a4aae7
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,21 @@
+{
+ "codeaction": "Code action",
+ "codeactions-unavailable-title": "Code actions are not available",
+ "codeactions-unavailable-no-namespaces": "The wiki administrators have not enabled code pages in any namespaces, for example by setting: $1",
+ "codeactions-unavailable-no-lang-with-actions": "The wiki administrators have not configured code actions for any code language, for example by setting: $1",
+ "codeaction-howto": "You have not specified a code action and page to perform this function on. For example <code>$1/$2/$3</code> would execute the <code>$2</code> action for the page <code>$3</code>.",
+ "codeactions-available": "Available code actions",
+ "code-pagesuffix": "Page suffix",
+ "codeaction-page-not-found-title": "Code page not found",
+ "codeaction-page-not-found-text": "Could not execute code action because [[$1]] does not exist but you may create it.",
+ "codeaction-label": "$1",
+ "codeaction-unsupported-namespace-title": "Unsupported namespace",
+ "codeaction-unsupported-namespace-text": "Code pages are not enabled for the $1 namespace.",
+ "codeaction-unknown-suffix-title": "Unsupported title suffix",
+ "codeaction-unknown-suffix-text": "Could not recognize a suffix in \"$1\". Expected one of $2.",
+ "codeaction-unknown-action-title": "Unsupported action",
+ "codeaction-unknown-action-text": "The action \"$1\" is not supported for this language. Expected one of $2.",
+ "codepage-unknown-suffix": "The \"$1\" content model could not recognize a language from the page title. Expected the title to end with one of the following suffixes: $2",
+ "codepage-error-category": "Code pages with unrecognized languages",
+ "codepage-error-category-desc": "The page has the \"code\" content model but the title could not be recognized (wasn't configured in <code>$wgCode_languages</code>)."
+}