🚧 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.

Unverified Commit 6469ee86 authored by Samwilson's avatar Samwilson Committed by GitHub
Browse files

Merge pull request #10 from samwilson/local-rendering

Default to local rendering of graph files
parents cd83e298 38c0a753
......@@ -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