Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

- PEAR Coding Standard fixes:

  + TRUE vs true, FALSE vs false,
  + @return docblock
  + Missing docblocks
  + switch/case blocks
  + Better E_ALL compliance
- Support for HTML-like labels
- Support for node port (Bug #4924)
- Proper escaping of IDs/values
- Support for multiline values
- Support for multiple edges between same nodes (Req #6630)
- Choice of GraphViz command to use for rendering (Bug #10753)
- Support for "strict" graphs
- Fix for undirected vs. directed graph (Bug #10753)
- Base binary path (Req #8295)
- Better error handling (more checking)
- Nodes in subgraphs
- Indentation in DOT file
# Patch by Philippe Jausions <Philippe.Jausions@11abacus.com>.


git-svn-id: http://svn.php.net/repository/pear/packages/Image_GraphViz/trunk@246430 c90b9560-bf6c-de11-be94-00142212c4b1
  • Loading branch information...
commit a952adb7def6aeeef873ae928b0ec0f4f7dd1901 1 parent 35ede04
Sebastian Bergmann authored
Showing with 399 additions and 159 deletions.
  1. +373 −155 GraphViz.php
  2. +26 −4 package.xml
View
528 GraphViz.php
@@ -4,7 +4,7 @@
/**
* Image_GraphViz
*
- * Copyright (c) 2001-2006, Dr. Volker Göbbels <vmg@arachnion.de> and
+ * Copyright (c) 2001-2007, Dr. Volker Göbbels <vmg@arachnion.de> and
* Sebastian Bergmann <sb@sebastian-bergmann.de>. All rights reserved.
*
* LICENSE: This source file is subject to version 3.0 of the PHP license
@@ -19,19 +19,24 @@
* @author Sebastian Bergmann <sb@sebastian-bergmann.de>
* @author Karsten Dambekalns <k.dambekalns@fishfarm.de>
* @author Michael Lively Jr. <mlively@ft11.net>
- * @copyright 2001-2006 Sebastian Bergmann <sb@sebastian-bergmann.de>
+ * @author Philippe Jausions <Philippe.Jausions@11abacus.com>
+ * @copyright 2001-2007 Sebastian Bergmann <sb@sebastian-bergmann.de>
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version CVS: $Id$
* @link http://pear.php.net/package/Image_GraphViz
+ * @link http://www.graphviz.org/
* @since File available since Release 0.1
*/
+/**
+ * Required PEAR classes
+ */
require_once 'System.php';
/**
* Interface to AT&T's GraphViz tools.
*
- * The GraphViz class allows for the creation of and the work with directed
+ * The GraphViz class allows for the creation of and to work with directed
* and undirected graphs and their visualization with AT&T's GraphViz tools.
*
* <code>
@@ -93,15 +98,24 @@
* @author Dr. Volker Göbbels <vmg@arachnion.de>
* @author Karsten Dambekalns <k.dambekalns@fishfarm.de>
* @author Michael Lively Jr. <mlively@ft11.net>
- * @copyright Copyright &copy; 2001-2006 Dr. Volker Göbbels <vmg@arachnion.de> and Sebastian Bergmann <sb@sebastian-bergmann.de>
+ * @author Philippe Jausions <Philippe.Jausions@11abacus.com>
+ * @copyright Copyright &copy; 2001-2007 Dr. Volker Göbbels <vmg@arachnion.de> and Sebastian Bergmann <sb@sebastian-bergmann.de>
* @license http://www.php.net/license/3_0.txt The PHP License, Version 3.0
* @version Release: @package_version@
* @link http://pear.php.net/package/Image_GraphViz
+ * @link http://www.graphviz.org/
* @since Class available since Release 0.1
*/
class Image_GraphViz
{
/**
+ * Base path to GraphViz commands
+ *
+ * @var string
+ */
+ var $binPath = '';
+
+ /**
* Path to GraphViz/dot command
*
* @var string
@@ -120,7 +134,14 @@ class Image_GraphViz
*
* @var array
*/
- var $graph;
+ var $graph = array('edgesFrom' => array(),
+ 'nodes' => array(),
+ 'attributes' => array(),
+ 'directed' => true,
+ 'clusters' => array(),
+ 'name' => 'G',
+ 'strict' => true,
+ );
/**
* Constructor.
@@ -129,15 +150,22 @@ class Image_GraphViz
* one page. If not set, the graph will be named 'G'.
*
* @param boolean $directed Directed (TRUE) or undirected (FALSE) graph.
+ * Note: You MUST pass a boolean, and not just an expression that evaluates
+ * to TRUE or FALSE (i.e. NULL, empty string, 0 will not work)
* @param array $attributes Attributes of the graph
* @param string $name Name of the Graph
+ * @param boolean $strict whether to collapse multiple edges between
+ * same nodes
+ *
* @access public
*/
- function Image_GraphViz($directed = TRUE, $attributes = array(), $name = NULL)
+ function Image_GraphViz($directed = true, $attributes = array(),
+ $name = 'G', $strict = true)
{
$this->setDirected($directed);
$this->setAttributes($attributes);
$this->graph['name'] = $name;
+ $this->graph['strict'] = (boolean)$strict;
}
/**
@@ -145,39 +173,37 @@ function Image_GraphViz($directed = TRUE, $attributes = array(), $name = NULL)
*
* @param string Format of the output image.
* This may be one of the formats supported by GraphViz.
+ * @param string $command "dot" or "neato"
+ *
* @access public
*/
- function image($format = 'svg')
+ function image($format = 'svg', $command = null)
{
- if ($data = $this->fetch($format)) {
- $sendContentLengthHeader = TRUE;
+ if ($data = $this->fetch($format, $command)) {
+ $sendContentLengthHeader = true;
switch ($format) {
case 'gif':
case 'png':
- case 'wbmp': {
+ case 'wbmp':
header('Content-Type: image/' . $format);
- }
- break;
+ break;
- case 'jpg': {
+ case 'jpg':
+ case 'jpeg':
header('Content-Type: image/jpeg');
- }
- break;
+ break;
- case 'pdf': {
+ case 'pdf':
header('Content-Type: application/pdf');
- }
- break;
+ break;
- case 'svg': {
+ case 'svg':
header('Content-Type: image/svg+xml');
- }
- break;
+ break;
- default: {
- $sendContentLengthHeader = FALSE;
- }
+ default:
+ $sendContentLengthHeader = false;
}
if ($sendContentLengthHeader) {
@@ -193,32 +219,48 @@ function image($format = 'svg')
*
* @param string Format of the output image.
* This may be one of the formats supported by GraphViz.
- * @return string The image (data) created by GraphViz.
+ * @param string $command "dot" or "neato"
+ *
+ * @return string The image (data) created by GraphViz or FALSE on error
* @access public
* @since Method available since Release 1.1.0
*/
- function fetch($format = 'svg')
+ function fetch($format = 'svg', $command = null)
{
- if ($file = $this->saveParsedGraph()) {
- $outputfile = $file . '.' . $format;
- $command = $this->graph['directed'] ? $this->dotCommand : $this->neatoCommand;
- $command .= ' -T' . escapeshellarg($format) . ' -o' . escapeshellarg($outputfile) . ' ' . escapeshellarg($file);
-
- @`$command`;
- @unlink($file);
-
- $fp = fopen($outputfile, 'rb');
-
- if ($fp) {
- $data = fread($fp, filesize($outputfile));
- fclose($fp);
- @unlink($outputfile);
- }
-
- return $data;
+ $file = $this->saveParsedGraph();
+ if (!$file) {
+ return false;
}
-
- return FALSE;
+
+ $outputfile = $file . '.' . $format;
+
+ switch ($command) {
+ case 'dot':
+ case 'neato':
+ break;
+ default:
+ $command = $this->graph['directed'] ? 'dot' : 'neato';
+ }
+
+ $command = $this->binPath . (($command == 'dot')
+ ? $this->dotCommand : $this->neatoCommand);
+ $command .= ' -T'.escapeshellarg($format).' -o'
+ .escapeshellarg($outputfile).' '.escapeshellarg($file);
+
+ @`$command`;
+ @unlink($file);
+
+ $fp = fopen($outputfile, 'rb');
+
+ if (!$fp) {
+ return false;
+ }
+
+ $data = fread($fp, filesize($outputfile));
+ fclose($fp);
+ @unlink($outputfile);
+
+ return $data;
}
/**
@@ -228,25 +270,39 @@ function fetch($format = 'svg')
* @param string The absolute path of the file to save to.
* @param string Format of the output image.
* This may be one of the formats supported by GraphViz.
+ * @param string $command "dot" or "neato"
+ *
* @return bool True if the file was saved, false otherwise.
* @access public
*/
- function renderDotFile($dotfile, $outputfile, $format = 'svg')
+ function renderDotFile($dotfile, $outputfile, $format = 'svg',
+ $command = null)
{
if (file_exists($dotfile)) {
$oldmtime = file_exists($outputfile) ? filemtime($outputfile) : 0;
- $command = $this->graph['directed'] ? $this->dotCommand : $this->neatoCommand;
- $command .= ' -T' . escapeshellarg($format) . ' -o' . escapeshellarg($outputfile) . ' ' . escapeshellarg($dotfile);
-
+ switch ($command) {
+ case 'dot':
+ case 'neato':
+ break;
+ default:
+ $command = $this->graph['directed'] ? 'dot' : 'neato';
+ }
+
+ $command = $this->binPath . (($command == 'dot')
+ ? $this->dotCommand
+ : $this->neatoCommand);
+ $command .= ' -T'.escapeshellarg($format)
+ .' -o'.escapeshellarg($outputfile)
+ .' '.escapeshellarg($dotfile);
@`$command`;
-
+
if (file_exists($outputfile) && filemtime($outputfile) > $oldmtime) {
- return TRUE;
+ return true;
}
}
- return FALSE;
+ return false;
}
/**
@@ -255,6 +311,8 @@ function renderDotFile($dotfile, $outputfile, $format = 'svg')
* @param string ID.
* @param array Title.
* @param array Attributes of the cluster.
+ *
+ * @return void
* @access public
*/
function addCluster($id, $title, $attributes = array())
@@ -269,6 +327,8 @@ function addCluster($id, $title, $attributes = array())
* @param string Name of the node.
* @param array Attributes of the node.
* @param string Group of the node.
+ *
+ * @return void
* @access public
*/
function addNode($name, $attributes = array(), $group = 'default')
@@ -279,7 +339,11 @@ function addNode($name, $attributes = array(), $group = 'default')
/**
* Remove a node from the graph.
*
+ * This method doesn't remove edges associated with the node.
+ *
* @param Name of the node to be removed.
+ *
+ * @return void
* @access public
*/
function removeNode($name, $group = 'default')
@@ -292,64 +356,96 @@ function removeNode($name, $group = 'default')
/**
* Add an edge to the graph.
*
- * Caveat! This cannot handle multiple identical edges. If you use non-numeric
- * IDs for the nodes, this will not do (too much) harm. For numeric IDs the
- * array_merge() that is used will change the keys when merging arrays, leading
- * to new nodes appearing in the graph.
+ * Examples:
+ * <code>
+ * $g->addEdge(array('node1' => 'node2'));
+ * $attr = array(
+ * 'label' => '+1',
+ * 'style' => 'dashed',
+ * );
+ * $g->addEdge(array('node3' => 'node4'), $attr);
+ *
+ * // With port specification
+ * $g->addEdge(array('node5' => 'node6'), $attr, array('node6' => 'portA'));
+ * $g->addEdge(array('node7' => 'node8'), null, array('node7' => 'portC',
+ * 'node8' => 'portD'));
+ * </code>
*
- * @param array Start and End node of the edge.
+ * @param array Start => End node of the edge.
* @param array Attributes of the edge.
+ * @param array $ports Start node => port, End node => port
+ *
+ * @return integer an edge ID that can be used with {@link removeEdge()}
* @access public
*/
- function addEdge($edge, $attributes = array())
+ function addEdge($edge, $attributes = array(), $ports = array())
{
- if (is_array($edge)) {
- $from = key($edge);
- $to = $edge[$from];
- $id = $from . '_' . $to;
+ if (!is_array($edge)) {
+ return;
+ }
- if (!isset($this->graph['edges'][$id])) {
- $this->graph['edges'][$id] = $edge;
- } else {
- $this->graph['edges'][$id] = array_merge(
- $this->graph['edges'][$id],
- $edge
- );
+ $from = key($edge);
+ $to = $edge[$from];
+ $info = array();
+
+ if (is_array($ports)) {
+ if (array_key_exists($from, $ports)) {
+ $info['portFrom'] = $ports[$from];
}
- if (is_array($attributes)) {
- if (!isset($this->graph['edgeAttributes'][$id])) {
- $this->graph['edgeAttributes'][$id] = $attributes;
- } else {
- $this->graph['edgeAttributes'][$id] = array_merge(
- $this->graph['edgeAttributes'][$id],
- $attributes
- );
- }
+ if (array_key_exists($to, $ports)) {
+ $info['portTo'] = $ports[$to];
}
}
+
+ if (is_array($attributes)) {
+ $info['attributes'] = $attributes;
+ }
+
+ if (!empty($this->graph['strict'])) {
+ if (!isset($this->graph['edgesFrom'][$from][$to][0])) {
+ $this->graph['edgesFrom'][$from][$to][0] = $info;
+ } else {
+ $this->graph['edgesFrom'][$from][$to][0] = array_merge(
+ $this->graph['edgesFrom'][$from][$to][0], $info
+ );
+ }
+ } else {
+ $this->graph['edgesFrom'][$from][$to][] = $info;
+ }
+
+ return count($this->graph['edgesFrom'][$from][$to]) - 1;
}
/**
* Remove an edge from the graph.
*
* @param array Start and End node of the edge to be removed.
+ * @param integer $id specific edge ID (only usefull when multiple edges
+ * exist between the same 2 nodes)
+ *
+ * @return void
* @access public
*/
- function removeEdge($edge)
+ function removeEdge($edge, $id = null)
{
- if (is_array($edge)) {
- $from = key($edge);
- $to = $edge[$from];
- $id = $from . '_' . $to;
+ if (!is_array($edge)) {
+ return;
+ }
- if (isset($this->graph['edges'][$id])) {
- unset($this->graph['edges'][$id]);
- }
+ $from = key($edge);
+ $to = $edge[$from];
+
+ if (!is_null($id)) {
+ if (isset($this->graph['edgesFrom'][$from][$to][$id])) {
+ unset($this->graph['edgesFrom'][$from][$to][$id]);
- if (isset($this->graph['edgeAttributes'][$id])) {
- unset($this->graph['edgeAttributes'][$id]);
+ if (count($this->graph['edgesFrom'][$from][$to]) == 0) {
+ unset($this->graph['edgesFrom'][$from][$to]);
+ }
}
+ } elseif (isset($this->graph['edgesFrom'][$from][$to])) {
+ unset($this->graph['edgesFrom'][$from][$to]);
}
}
@@ -357,6 +453,8 @@ function removeEdge($edge)
* Add attributes to the graph.
*
* @param array Attributes to be added to the graph.
+ *
+ * @return void
* @access public
*/
function addAttributes($attributes)
@@ -373,6 +471,8 @@ function addAttributes($attributes)
* Set attributes of the graph.
*
* @param array Attributes to be set for the graph.
+ *
+ * @return void
* @access public
*/
function setAttributes($attributes)
@@ -383,9 +483,85 @@ function setAttributes($attributes)
}
/**
+ * Escapes an (attribute) array
+ *
+ * Detects if an attribute is <html>, contains double-quotes, etc...
+ *
+ * @param array $input
+ *
+ * @return array input escaped
+ * @access protected
+ */
+ function _escapeArray($input)
+ {
+ $output = array();
+
+ foreach ((array)$input as $k => $v) {
+ switch ($k) {
+ case 'label':
+ case 'headlabel':
+ case 'taillabel':
+ $v = $this->_escape($v, true);
+ break;
+ default:
+ $v = $this->_escape($v);
+ $k = $this->_escape($k);
+ }
+
+ $output[$k] = $v;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Returns a safe "ID" in DOT syntax
+ *
+ * @param string $input string to use as "ID"
+ * @param boolean $html whether to attempt detecting HTML-like content
+ *
+ * @return string
+ * @access protected
+ */
+ function _escape($input, $html = false)
+ {
+ switch (strtolower($input)) {
+ case 'node':
+ case 'edge':
+ case 'graph':
+ case 'digraph':
+ case 'subgraph':
+ case 'strict':
+ return '"'.$input.'"';
+ }
+
+ if (is_bool($input)) {
+ return ($input) ? 'true' : 'false';
+ }
+
+ if ($html && (strpos($input, '</') !== false
+ || strpos($input, '/>') !== false)) {
+ return '<'.$input.'>';
+ }
+
+ if (preg_match('/^([a-z_][a-z_0-9]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$/i',
+ $input)) {
+ return $input;
+ }
+
+ return '"'.str_replace(array("\r\n", "\n", "\r", '"'),
+ array('\n', '\n', '\n', '\"'), $input).'"';
+ }
+
+ /**
* Set directed/undirected flag for the graph.
*
+ * Note: You MUST pass a boolean, and not just an expression that evaluates
+ * to TRUE or FALSE (i.e. NULL, empty string, 0 will not work)
+ *
* @param boolean Directed (TRUE) or undirected (FALSE) graph.
+ *
+ * @return void
* @access public
*/
function setDirected($directed)
@@ -399,12 +575,43 @@ function setDirected($directed)
* Load graph from file.
*
* @param string File to load graph from.
+ *
+ * @return void
* @access public
*/
function load($file)
{
if ($serializedGraph = implode('', @file($file))) {
- $this->graph = unserialize($serializedGraph);
+ $g = unserialize($serializedGraph);
+
+ if (!is_array($g)) {
+ return;
+ }
+
+ // Convert old storage format to new one
+ $defaults = array('edgesFrom' => array(),
+ 'nodes' => array(),
+ 'attributes' => array(),
+ 'directed' => true,
+ 'clusters' => array(),
+ 'name' => 'G',
+ 'strict' => true,
+ );
+
+ $this->graph = array_merge($defaults, $g);
+
+ if (isset($this->graph['edges'])) {
+ foreach ($this->graph['edges'] as $id => $nodes) {
+ $attr = (isset($this->graph['edgeAttributes'][$id]))
+ ? $this->graph['edgeAttributes'][$id]
+ : array();
+
+ $this->addEdge($nodes, $attr);
+ }
+
+ unset($this->graph['edges']);
+ unset($this->graph['edgeAttributes']);
+ }
}
}
@@ -423,14 +630,14 @@ function save($file = '')
$file = System::mktemp('graph_');
}
- if ($fp = @fopen($file, 'w')) {
+ if ($fp = @fopen($file, 'wb')) {
@fputs($fp, $serializedGraph);
@fclose($fp);
return $file;
}
- return FALSE;
+ return false;
}
/**
@@ -441,92 +648,103 @@ function save($file = '')
*/
function parse()
{
- if (isset($this->graph['name']) && is_string($this->graph['name'])) {
- $parsedGraph = "digraph " . $this->graph['name'] . " {\n";
- } else {
- $parsedGraph = "digraph G {\n";
- }
+ $parsedGraph = (empty($this->graph['strict'])) ? '' : 'strict ';
+ $parsedGraph .= (empty($this->graph['directed'])) ? 'graph ' : 'digraph ';
+ $parsedGraph .= $this->_escape($this->graph['name'])." {\n";
- if (isset($this->graph['attributes'])) {
- foreach ($this->graph['attributes'] as $key => $value) {
- $attributeList[] = $key . '="' . $value . '"';
- }
+ $indent = ' ';
- if (!empty($attributeList)) {
- $parsedGraph .= 'graph [ '.implode(',', $attributeList) . " ];\n";
- }
+ $attr = $this->_escapeArray($this->graph['attributes']);
+
+ foreach ($attr as $key => $value) {
+ $parsedGraph .= $indent.$key.'='.$value.";\n";
}
- if (isset($this->graph['nodes'])) {
- foreach($this->graph['nodes'] as $group => $nodes) {
- if ($group != 'default') {
- $parsedGraph .= sprintf(
- "subgraph \"cluster_%s\" {\nlabel=\"%s\";\n",
+ foreach ($this->graph['nodes'] as $group => $nodes) {
+ if ($group != 'default') {
+ $parsedGraph .= $indent.'subgraph '.$this->_escape($group)." {\n";
+ $indent .= ' ';
- $group,
- isset($this->graph['clusters'][$group]) ? $this->graph['clusters'][$group]['title'] : ''
- );
+ if (isset($this->graph['clusters'][$group])) {
+ $cluster = $this->graph['clusters'][$group];
+ $attr = $this->_escapeArray($cluster['attributes']);
- if (isset($this->graph['clusters'][$group]['attributes'])) {
- unset($attributeList);
+ foreach ($attr as $key => $value) {
+ $attr[] = $key.'='.$value;
+ }
- foreach ($this->graph['clusters'][$group]['attributes'] as $key => $value) {
- $attributeList[] = $key . '="' . $value . '"';
- }
+ if (strlen($cluster['title'])) {
+ $attr[] = 'label='
+ .$this->_escape($cluster['title'], true);
+ }
- if (!empty($attributeList)) {
- $parsedGraph .= implode(',', $attributeList) . ";\n";
- }
+ if ($attr) {
+ $parsedGraph .= $indent.'graph [ '.implode(',', $attr)
+ ." ];\n";
}
}
+ }
- foreach($nodes as $node => $attributes) {
- unset($attributeList);
+ foreach ($nodes as $node => $attributes) {
+ $parsedGraph .= $indent.$this->_escape($node);
- foreach($attributes as $key => $value) {
- $attributeList[] = $key . '="' . $value . '"';
- }
+ $attributeList = array();
- if (!empty($attributeList)) {
- $parsedGraph .= sprintf(
- "\"%s\" [ %s ];\n",
- addslashes(stripslashes($node)),
- implode(',', $attributeList)
- );
- }
+ foreach ($this->_escapeArray($attributes) as $key => $value) {
+ $attributeList[] = $key.'='.$value;
}
- if ($group != 'default') {
- $parsedGraph .= "}\n";
+ if (!empty($attributeList)) {
+ $parsedGraph .= ' [ '.implode(',', $attributeList).' ]';
}
+
+ $parsedGraph .= ";\n";
+ }
+
+ if ($group != 'default') {
+ $indent = substr($indent, 0, -4);
+ $parsedGraph .= $indent."}\n";
}
}
- if (isset($this->graph['edges'])) {
- foreach($this->graph['edges'] as $label => $node) {
- unset($attributeList);
+ if (!empty($this->graph['directed'])) {
+ $separator = ' -> ';
+ } else {
+ $separator = ' -- ';
+ }
+
+ foreach ($this->graph['edgesFrom'] as $from => $toNodes) {
+ $from = $this->_escape($from);
- $from = key($node);
- $to = $node[$from];
+ foreach ($toNodes as $to => $edges) {
+ $to = $this->_escape($to);
- foreach($this->graph['edgeAttributes'][$label] as $key => $value) {
- $attributeList[] = $key . '="' . $value . '"';
- }
+ foreach ($edges as $info) {
+ $f = $from;
+ $t = $to;
- $parsedGraph .= sprintf(
- '"%s" -> "%s"',
- addslashes(stripslashes($from)),
- addslashes(stripslashes($to))
- );
-
- if (!empty($attributeList)) {
- $parsedGraph .= sprintf(
- ' [ %s ]',
- implode(',', $attributeList)
- );
- }
+ if (array_key_exists('portFrom', $info)) {
+ $f .= ':'.$this->_escape($info['portFrom']);
+ }
- $parsedGraph .= ";\n";
+ if (array_key_exists('portTo', $info)) {
+ $t .= ':'.$this->_escape($info['portTo']);
+ }
+
+ $parsedGraph .= $indent.$f.$separator.$t;
+
+ if (!empty($info['attributes'])) {
+ $attributeList = array();
+
+ foreach ($this->_escapeArray($info['attributes']) as $key => $value) {
+ $attributeList[] = $key.'='.$value;
+ }
+
+ $parsedGraph .= ' [ '.implode(',', $attributeList).' ]';
+ }
+
+ $parsedGraph .= ";\n";
+ }
}
}
@@ -558,7 +776,7 @@ function saveParsedGraph($file = '')
}
}
- return FALSE;
+ return false;
}
}
View
30 package.xml
@@ -18,12 +18,29 @@
</maintainers>
<license>PHP License</license>
<release>
- <version>1.2.1</version>
- <date>2006-05-23</date>
- <state>stable</state>
+ <version>1.3.0RC1</version>
+ <date>2007-11-19</date>
+ <state>beta</state>
<notes>
<![CDATA[
-* Bugfix release.
+- PEAR Coding Standard fixes:
+ + TRUE vs true, FALSE vs false,
+ + @return docblock
+ + Missing docblocks
+ + switch/case blocks
+ + Better E_ALL compliance
+- Support for HTML-like labels
+- Support for node port (Bug #4924)
+- Proper escaping of IDs/values
+- Support for multiline values
+- Support for multiple edges between same nodes (Req #6630)
+- Choice of GraphViz command to use for rendering (Bug #10753)
+- Support for "strict" graphs
+- Fix for undirected vs. directed graph (Bug #10753)
+- Base binary path (Req #8295)
+- Better error handling (more checking)
+- Nodes in subgraphs
+- Indentation in DOT file
]]>
</notes>
<filelist>
@@ -35,6 +52,11 @@
</filelist>
</release>
<changelog>
+ <release>
+ <version>1.2.1</version>
+ <date>2006-05-23</date>
+ <state>stable</state>
+ </release>
<release>
<version>1.2.0</version>
<date>2006-05-11</date>
Please sign in to comment.
Something went wrong with that request. Please try again.