🚧 This instance is under construction; expect occasional downtime. Runners available in /repos. Questions? Ask in #wikimedia-gitlab on libera.chat, or under GitLab on Phabricator.

Commit 38c0a753 authored by Samwilson's avatar Samwilson
Browse files

Default to local rendering of graph files

If no Diagrams web service is defined in LocalSettings.php,
render graph files locally and add to the wiki's local file
repository.
parent cd83e298
......@@ -16,6 +16,7 @@
"source": "https://github.com/samwilson/diagrams-extension"
},
"require": {
"ext-libxml": "*",
"composer/installers": "^1.0"
},
"require-dev": {
......
......@@ -17,8 +17,8 @@
},
"config": {
"DiagramsServiceUrl": {
"description": "URL of the diagram-rendering service.",
"value": "https://example.org/diagrams/"
"description": "URL of the diagram-rendering service. If not provided, graphs will be locally rendered.",
"value": ""
}
},
"Hooks": {
......
......@@ -7,5 +7,6 @@
"diagrams-extensionname": "Diagrams",
"diagrams-desc": "Render Graphviz, Mscgen, and PlantUML diagrams in wiki pages.",
"diagrams-error-no-response": "Diagrams error: no response received from remote service.",
"diagrams-error-returned-0": "Diagrams service error:"
"diagrams-error-returned-0": "Diagrams service error:",
"diagrams-error-generic": "Diagrams error:"
}
......@@ -6,6 +6,7 @@
},
"diagrams-extensionname": "{{name}}",
"diagrams-desc": "{{desc|name=Diagrams|url=https://www.mediawiki.org/wiki/Extension:Diagrams}}",
"diagrams-error-generic": "Error message label for errors that don't have a more specific message.",
"diagrams-error-no-response": "Error message displayed when no response is received from the web service.",
"diagrams-error-returned-0": "Error message label for errors returned by the web service."
}
<?php
namespace MediaWiki\Extension\Diagrams;
use Html;
use Http;
use LocalRepo;
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
class Diagrams {
/** @var bool */
private $isPreview;
/**
* @param bool $isPreview
*/
public function __construct( bool $isPreview ) {
$this->isPreview = $isPreview;
}
/**
* Get HTML for an error message.
* @param string $error Error message. May contain HTML.
* @return string
*/
protected function formatError( $error ) {
return Html::rawElement( 'span', [ 'class' => 'ext-diagrams error' ], $error );
}
/**
* @param string $commandName The command to render the graph with.
* @param string $input The graph source.
* @param array $params Parameter to the wikitext tag (caption, format, etc.).
* @return string HTML to display the image and image map.
*/
public function renderLocally( string $commandName, string $input, array $params ) {
$localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
$diagramsRepo = new LocalRepo( [
'class' => 'LocalRepo',
'name' => 'local',
'backend' => $localRepo->getBackend(),
'directory' => $localRepo->getZonePath( 'public' ) . '/diagrams',
'url' => $localRepo->getZoneUrl( 'public' ) . '/diagrams',
'hashLevels' => 0,
'thumbUrl' => '',
'transformVia404' => false,
'deletedDir' => '',
'deletedHashLevels' => 0,
'zones' => [
'public' => [
'directory' => '/diagrams',
],
],
] );
$outputFormats = [
'image' => $params['format'] ?? 'png',
'map' => $commandName === 'mscgen' ? 'ismap' : 'cmapx',
];
$fileName = 'Diagrams ' . md5( $input ) . '.' . $outputFormats['image'];
$graphFile = $diagramsRepo->findFile( $fileName );
if ( !$graphFile ) {
$graphFile = $diagramsRepo->newFile( $fileName );
}
if ( $graphFile->exists() ) {
return $this->getHtml( $graphFile );
}
$tmpFactory = MediaWikiServices::getInstance()->getTempFSFileFactory();
$tmpGraphSourceFile = $tmpFactory->newTempFSFile( 'diagrams_in' );
// Render image and map files.
$mapData = null;
$tmpOutFiles = [];
foreach ( $outputFormats as $outputType => $outputFormat ) {
$tmpOutFiles[$outputType] = $tmpFactory->newTempFSFile( 'diagrams_out_', $outputFormat );
file_put_contents( $tmpGraphSourceFile->getPath(), $input );
$cmd = Shell::command(
$commandName,
'-T', $outputFormat,
'-o', $tmpOutFiles[$outputType]->getPath(),
$tmpGraphSourceFile->getPath()
);
$result = $cmd->execute();
if ( $result->getExitCode() !== 0 ) {
return $this->formatError( wfMessage( 'diagrams-error-generic' ) . ' ' . $result->getStderr() );
}
}
$status = $this->isPreview
? $diagramsRepo->storeTemp( $fileName, $tmpOutFiles['image'] )
: $graphFile->publish( $tmpOutFiles['image'] );
$mapData = file_get_contents( $tmpOutFiles['map']->getPath() );
return !$status->isGood()
? $this->formatError( $status->getHTML() )
: $this->getHtml( $graphFile->getUrl(), $mapData );
}
/**
* The main rendering method, handling all types.
* @param string $generator
* @param string $input
* @param string|null $type
* @return string
*/
public function renderWithService( $generator, $input, $type = null ) {
$baseUrl = MediaWikiServices::getInstance()->getMainConfig()->get( 'DiagramsServiceUrl' );
$url = trim( $baseUrl, '/' ) . '/render';
$params = [
'postData' => http_build_query( [
'generator' => $generator,
'types' => array_filter( [ 'png', $type ] ),
'source' => $input,
] ),
];
$result = Http::request( 'POST', $url, $params, __METHOD__ );
if ( $result === false ) {
return static::formatError( wfMessage( 'diagrams-error-no-response' ) );
}
$response = json_decode( $result );
if ( isset( $response->error ) ) {
$error = wfMessage( 'diagrams-error-returned-' . $response->error );
if ( isset( $response->message ) ) {
$error .= Html::element( 'br' ) . $response->message;
}
return static::formatError( $error );
}
$cmapx = $response->diagrams->cmapx->contents ?? null;
$ismapUrl = $response->diagrams->ismap->url ?? null;
return $this->getHtml( $response->diagrams->png->url, $cmapx, $ismapUrl );
}
/**
* Get the full Diagrams HTML output for a given URL and optional map.
* @param string $imgUrl URL to the diagram's image.
* @param string|null $mapData Image map in cmapx or ismap format.
* @param string|null $ismapUrl The URL to the ismap file to use. Only used
* if $mapData is not given.
* @return string
*/
private function getHtml(
string $imgUrl,
string $mapData = null,
string $ismapUrl = null
): string {
$imgAttrs = [ 'src' => $imgUrl ];
if ( $mapData ) {
// Image maps in an image map format.
$imageMap = new ImageMap( $mapData );
if ( $imageMap->hasAreas() ) {
$imgAttrs['usemap'] = '#' . $imageMap->getName();
}
$out = Html::element( 'img', $imgAttrs );
if ( $imageMap->hasAreas() ) {
$out .= $imageMap->getMap();
}
} elseif ( $ismapUrl ) {
// Image maps in imap format.
$imgAttrs['ismap'] = true;
$out = Html::rawElement(
'a',
[ 'href' => $ismapUrl ],
Html::element( 'img', $imgAttrs )
);
} else {
// No image map.
$out = Html::element( 'img', $imgAttrs );
}
return Html::rawElement( 'div', [ 'class' => 'ext-diagrams' ], $out );
}
}
......@@ -2,10 +2,9 @@
namespace MediaWiki\Extension\Diagrams;
use Html;
use Http;
use MediaWiki\MediaWikiServices;
use Parser;
use PPFrame;
class Hooks {
......@@ -14,88 +13,34 @@ class Hooks {
* @param Parser $parser
*/
public static function onParserFirstCallInit( Parser $parser ) {
$parserOptions = $parser->getOptions();
$isPreview = $parserOptions ? $parserOptions->getIsPreview() : false;
$diagrams = new Diagrams( $isPreview );
foreach ( [ 'graphviz', 'mscgen', 'uml' ] as $tag ) {
$parser->setHook( $tag, function ( string $input ) use ( $tag ) {
$parser->setHook( $tag, function (
string $input, array $params, Parser $parser, PPFrame $frame
) use (
$tag, $diagrams
) {
// Make sure there's something to render.
$input = trim( $input );
if ( $input === '' ) {
return '';
}
$renderMethod = MediaWikiServices::getInstance()->getMainConfig()->get( 'DiagramsServiceUrl' )
? 'renderWithService'
: 'renderLocally';
if ( $tag === 'graphviz' ) {
// GraphViz.
return static::render( $tag, $input, 'cmapx' );
return $diagrams->$renderMethod( $params['renderer'] ?? 'dot', $input, $params );
} elseif ( $tag === 'mscgen' ) {
// Mscgen.
return static::render( $tag, $input, 'ismap' );
return $diagrams->$renderMethod( 'mscgen', $input, $params );
} else {
// PlantUML.
return static::render( 'plantuml', $input );
return $diagrams->$renderMethod( 'plantuml', $input, $params );
}
} );
}
}
/**
* Get HTML for an error message.
* @param string $error Error message. May contain HTML.
* @return string
*/
protected static function formatError( $error ) {
return Html::rawElement( 'span', [ 'class' => 'ext-diagrams error' ], $error );
}
/**
* The main rendering method, handling all types.
* @param string $generator
* @param string $input
* @param string|null $type
* @return string
*/
protected static function render( $generator, $input, $type = null ) {
$baseUrl = MediaWikiServices::getInstance()->getMainConfig()->get( 'DiagramsServiceUrl' );
$url = trim( $baseUrl, '/' ) . '/render';
$params = [
'postData' => http_build_query( [
'generator' => $generator,
'types' => array_filter( [ 'png', $type ] ),
'source' => $input,
] ),
];
$result = Http::request( 'POST', $url, $params, __METHOD__ );
if ( $result === false ) {
return static::formatError( wfMessage( 'diagrams-error-no-response' ) );
}
$response = json_decode( $result );
if ( isset( $response->error ) ) {
$error = wfMessage( 'diagrams-error-returned-' . $response->error );
if ( isset( $response->message ) ) {
$error .= Html::element( 'br' ) . $response->message;
}
return static::formatError( $error );
}
$imgAttrs = [ 'src' => $response->diagrams->png->url ];
if ( isset( $response->diagrams->cmapx->contents ) ) {
// Image maps in cmapx format.
$imageMap = new ImageMap( $response->diagrams->cmapx->contents );
if ( $imageMap->hasAreas() ) {
$imgAttrs['usemap'] = '#' . $imageMap->getName();
}
$out = Html::element( 'img', $imgAttrs );
if ( $imageMap->hasAreas() ) {
$out .= $imageMap->getMap();
}
} elseif ( isset( $response->diagrams->ismap->contents ) ) {
// Image maps in imap format.
$imgAttrs['ismap'] = true;
$out = Html::rawElement(
'a',
[ 'href' => $response->diagrams->ismap->url ],
Html::element( 'img', $imgAttrs )
);
} else {
// No image map.
$out = Html::element( 'img', $imgAttrs );
}
return Html::rawElement( 'div', [ 'class' => 'ext-diagrams' ], $out );
}
}
......@@ -7,6 +7,7 @@ namespace MediaWiki\Extension\Diagrams;
use DOMDocument;
use DOMXPath;
use Html;
use Title;
/**
......@@ -25,9 +26,30 @@ class ImageMap {
* @param string $mapHtml HTML string of the <map> element.
*/
public function __construct( $mapHtml ) {
if ( strpos( $mapHtml, '<map' ) === false ) {
$mapHtml = $this->convertToMap( $mapHtml );
}
$this->map = $mapHtml;
}
/**
* Convert ismap format to map HTML.
* @param string $ismap
* @return string
*/
public function convertToMap( string $ismap ): string {
$areas = [];
foreach ( explode( "\n", $ismap ) as $line ) {
$parts = explode( ' ', $line );
$areas[] = Html::element( 'area', [
'shape' => array_shift( $parts ),
'href' => array_shift( $parts ),
'coords' => implode( ' ', $parts ),
] );
}
return Html::rawElement( 'map', [], implode( $areas ) );
}
/**
* Get the imagemap HTML.
*
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment