diff --git a/broken.svg b/broken.svg new file mode 100644 index 0000000..e3b6d97 --- /dev/null +++ b/broken.svg @@ -0,0 +1,23 @@ + + + + + + +broken + + +broke + +?? + + +broke->broke + + + + + diff --git a/conf/default.php b/conf/default.php index 76c8989..114a181 100644 --- a/conf/default.php +++ b/conf/default.php @@ -1,3 +1,5 @@ _imgfile($data); -if(!$cache) _fail(); +// Update: show svg or png depending on format. +if(!$cache) _fail($data['format']); +// Update: support both png and svg +$img_format = ($data['format'] == 'png' ? 'png' : 'svg'); +if ($img_format == 'svg'){ + header('Content-Type: image/svg+xml'); +} +else +{ + header('Content-Type: image/png'); +} -header('Content-Type: image/png;'); header('Expires: '.gmdate("D, d M Y H:i:s", time()+max($conf['cachetime'], 3600)).' GMT'); header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.max($conf['cachetime'], 3600)); header('Pragma: public'); @@ -22,10 +31,16 @@ echo io_readFile($cache,false); -function _fail(){ - header("HTTP/1.0 404 Not Found"); - header('Content-Type: image/png'); - echo io_readFile('broken.png',false); +function _fail($format){ + if ($format == 'svg'){ + header("HTTP/1.0 404 Not Found"); + header('Content-Type: image/svg+xml'); + echo io_readFile('broken.svg',false); + }else{ + header("HTTP/1.0 404 Not Found"); + header('Content-Type: image/png'); + echo io_readFile('broken.png',false); + } exit; } diff --git a/lang/en/settings.php b/lang/en/settings.php index 69bf6b0..15c515a 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -1,3 +1,6 @@ /usr/bin/dot). Leave empty to use remote rendering at google.com.'; +$lang['use_svg'] = 'Use embeded svg objects. Svg objects support clickable dokuwiki links in the graphs. This is automatically disabled if "path" is left empty.'; +$lang['styles'] = 'A block of DOT codes, that can be included to the graphs. Meant to include styling. To set a style put it inside <style name="stylename"> style code here lgt;/style>. The stylename can only contain alphanumeric and underscore characters.'. + 'E.g. '.htmlspecialchars('').''; diff --git a/syntax.php b/syntax.php index 518d027..1530f57 100644 --- a/syntax.php +++ b/syntax.php @@ -8,12 +8,26 @@ */ -if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); -if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); +if (!defined('DOKU_INC')) define('DOKU_INC',realpath(DOKU_PLUGIN.'/../../').'/'); require_once(DOKU_PLUGIN.'syntax.php'); class syntax_plugin_graphviz extends DokuWiki_Syntax_Plugin { + /** + * array holding defined styles. + */ + protected $_styles = array(); + + + /** + * Constructor to check config + */ + function __construct(){ + // disable use_svg if 'path' is not set. + if(!$this->getConf('path')) $this->conf['use_svg'] = 0; + $this->_parseStyles($s); + } + /** * What about paragraphs? */ @@ -39,7 +53,8 @@ function getSort(){ * Connect pattern to lexer */ function connectTo($mode) { - $this->Lexer->addSpecialPattern('\n.*?\n',$mode,'plugin_graphviz'); + //Fix: removed newline requirements, as a accidental space before newline can break the pattern + $this->Lexer->addSpecialPattern('.*?',$mode,'plugin_graphviz'); } /** @@ -47,7 +62,6 @@ function connectTo($mode) { */ function handle($match, $state, $pos, &$handler) { $info = $this->getInfo(); - // prepare default data $return = array( 'width' => 0, @@ -57,10 +71,17 @@ function handle($match, $state, $pos, &$handler) { 'version' => $info['date'], //force rebuild of images on update ); - // prepare input - $lines = explode("\n",$match); - $conf = array_shift($lines); - array_pop($lines); + // prepare input, UPDATE: separate tags from data by preg instead of first/last line, but leave the old behavior as fallback + if (preg_match('~]*)>((?:[\n]|.)*)~Ui',$match,$parts)){ + $input = $parts[2]; + $conf = $parts[1]; + } + else{ + $lines = explode("\n",$match); + $conf = array_shift($lines); + array_pop($lines); + $input = join("\n",$lines); + } // match config options if(preg_match('/\b(left|center|right)\b/i',$conf,$match)) $return['align'] = $match[1]; @@ -73,17 +94,69 @@ function handle($match, $state, $pos, &$handler) { } if(preg_match('/\bwidth=([0-9]+)\b/i', $conf,$match)) $return['width'] = $match[1]; if(preg_match('/\bheight=([0-9]+)\b/i', $conf,$match)) $return['height'] = $match[1]; - - - $input = join("\n",$lines); + + // Added code for styles. + $styles = array(); + // match stylenames that are written into the graphviz tag + if(preg_match_all('/\b(\w+)\b/i',$conf,$match)){ + foreach ($match[1] as $style){ + if (array_key_exists($tmp = strtolower($style),$this->_styles)){ + $styles[] = $tmp; + } + } + } + $styles = array_unique($styles); // remove duplicated styles + + $this->_injectStyles($input,$styles); + + //Update: add dokulink support to URL tags (they can contain [[dokulink]] urls. + if ($this->getConf('use_svg')){ + global $ID; + $return['url'] = md5(wl($ID,true)); // add current page's url to 'data' to ensure exported links work on page + $input = preg_replace_callback( + '~\b(|head|label|tail|edge)'. // match 0 -> prefix for URL + 'URL="\[\[(?:'. // alternative urls to catch + '(\w+\://[^\]\|]*)'. // simple url with any shema + '|(?:(\w+)>([^\]\|]*))'. // interwiki link + '|([^\]\|#]+)?(#[^\]\|]*)?)'. // dokuwiki link and/or fragment + '(?:\|([^\]]*))?'. // text + '\]\]"~U', + array(__CLASS__,'_parse_links'), + $input); + } $return['md5'] = md5($input); // we only pass a hash around - // store input for later use io_saveFile($this->_cachename($return,'txt'),$input); - return $return; } + /** + * callback function to preg_replace_callback, that fixes urls. + */ + public static function _parse_links($matches){ + global $ID; + $pref = @$matches[1]; // the prefix of the URL (i.e. "head" from "headURL") + $url = @$matches[2]; // external link (it's already url) + $int_t = @$matches[3]; // interwiki prefix (i.e. "wp" from "[[wp>blabla]]") + $int_u = @$matches[4]; // interwiki url (i.e. "blabla" from "[[wp>blabla]]") + $page = @$matches[5]; // dokuwiki internal page link + $frag = @$matches[6] ? '#'.preg_replace('~\W+~','-',strtolower(ltrim($matches[6],"#"))) : ""; // fragment for dokuwiki internal link, simple cleaning + $text = @$matches[7]; // text of link (i.e. "blabla" from "[[.:mylink:|blabla]]" + if ($page || $frag){ + resolve_pageid(getNS($ID),$page,$exists); + $url = wl($page).$frag; + } + elseif($int_t){ + static $xhtml_renderer = null; + if(is_null($xhtml_renderer)){ + $xhtml_renderer = p_get_renderer('xhtml'); + $xhtml_renderer->interwiki = getInterwiki(); + } + $url = $xhtml_renderer->_resolveInterWiki($int_t,$int_u,$exists); + } + return "{$pref}URL=\"{$url}\";{$pref}target=_top".($text ? ";{$pref}tooltip=\"{$text}\"" : ""); + } + /** * Cache file is based on parameters that influence the result image */ @@ -91,6 +164,7 @@ function _cachename($data,$ext){ unset($data['width']); unset($data['height']); unset($data['align']); + unset($data['format']); return getcachename(join('x',array_values($data)),'.graphviz.'.$ext); } @@ -98,14 +172,35 @@ function _cachename($data,$ext){ * Create output */ function render($format, &$R, $data) { - if($format == 'xhtml'){ - $img = DOKU_BASE.'lib/plugins/graphviz/img.php?'.buildURLparams($data); - $R->doc .= 'getConf('use_svg') && get_class($R) != 'renderer_plugin_dw2pdf'){ + //Update: generate both png and svg, embed svg but with fallback to png. + $img_svg = DOKU_BASE.'lib/plugins/graphviz/img.php?'.buildURLparams(array_merge($data,array('format'=>'svg'))); + // embed svg as object: the links in svg can be clicked (and also animations are supported) + $R->doc .= 'doc .= ' width="'.$data['width'].'"'; + if($data['height']) $R->doc .= ' height="'.$data['height'].'"'; + if($data['align'] == 'right') $R->doc .= ' align="right"'; + if($data['align'] == 'left') $R->doc .= ' align="left"'; + $R->doc .= '>'; + } + if (get_class($R) == 'renderer_plugin_dw2pdf'){ + // for pdf export: mpdf fetches the url, and without authorization it is automatic fail. We need to set a file src as real path. + $cache = $this->_imgfile(array_merge($data,array('format'=>'png'))); + $img_png = $cache; + }else{ + $img_png = DOKU_BASE.'lib/plugins/graphviz/img.php?'.buildURLparams(array_merge($data,array('format'=>'png'))); + } + // embed fallback: if browser does not support svg bia object embed, it will display the png image instead. + $R->doc .= 'doc .= ' width="'.$data['width'].'"'; if($data['height']) $R->doc .= ' height="'.$data['height'].'"'; if($data['align'] == 'right') $R->doc .= ' align="right"'; if($data['align'] == 'left') $R->doc .= ' align="left"'; $R->doc .= '/>'; + if ($this->getConf('use_svg') && get_class($R) != 'renderer_plugin_dw2pdf'){ + $R->doc .=''; + } return true; }elseif($format == 'odt'){ $src = $this->_imgfile($data); @@ -119,7 +214,9 @@ function render($format, &$R, $data) { * Return path to the rendered image on our local system */ function _imgfile($data){ - $cache = $this->_cachename($data,'png'); + // Update: support pnd and svg format as well. + $format = ($data['format'] == 'png' ? 'png' : 'svg'); + $cache = $this->_cachename($data,$format); // create the file if needed if(!file_exists($cache)){ @@ -182,11 +279,11 @@ function _run($data,$in,$out) { } $cmd = $this->getConf('path'); - $cmd .= ' -Tpng'; + // Update: support both svg and png + $cmd .= ' -T'.($data['format'] == 'png' ? 'png' : 'svg'); $cmd .= ' -K'.$data['layout']; $cmd .= ' -o'.escapeshellarg($out); //output $cmd .= ' '.escapeshellarg($in); //input - exec($cmd, $output, $error); if ($error != 0){ @@ -198,6 +295,33 @@ function _run($data,$in,$out) { return true; } + /** + * parse styles form configuration + */ + function _parseStyles(){ + if (preg_match_all('~(.*)~U',$this->getConf('styles'),$matches,PREG_SET_ORDER)){ + foreach ($matches as $match){ + $this->_styles[strtolower($match[2])] = $match[3]; + } + } + } + + /** + * parse styles form configuration + */ + function _injectStyles(&$input,$styles){ + // if there are no styles or the beginning of the graph is not found, leave it as is + if (!empty($styles) && (($p = strpos($input,"{")) !== false)){ + $beg = substr($input,0,$p+1); // beginning, i.e. "digraph name{" + $rest = substr($input,$p+1); // the rest of the string + $input = $beg; + foreach ($styles as $style){ // add each matched style right after the beginning. + $input .= "\n".$this->_styles[$style]."\n"; + } + $input .= $rest; // the graph code itself. + } + } + }