—#############################################################################
# Parse text definition into a Graph::Easy object
#
#############################################################################
package
Graph::Easy::Parser;
use
Graph::Easy;
$VERSION
=
'0.35'
;
use
Graph::Easy::Base;
@ISA
=
qw/Graph::Easy::Base/
;
use
strict;
sub
_init
{
my
(
$self
,
$args
) =
@_
;
$self
->{error} =
''
;
$self
->{debug} = 0;
$self
->{fatal_errors} = 1;
foreach
my
$k
(
keys
%$args
)
{
if
(
$k
!~ /^(debug|fatal_errors)\z/)
{
my
$class
=
ref
(
$self
);
Carp::confess (
"Invalid argument '$k' passed to $class"
.
'->new()'
);
}
$self
->{
$k
} =
$args
->{
$k
};
}
# what to replace the matched text with
$self
->{replace} =
''
;
$self
->{attr_sep} =
':'
;
# An optional regexp to remove parts of an autosplit label, used by Graphviz
# to remove " <p1> ":
$self
->{_qr_part_clean} =
undef
;
# setup default class names for generated objects
$self
->{use_class} = {
edge
=>
'Graph::Easy::Edge'
,
group
=>
'Graph::Easy::Group'
,
graph
=>
'Graph::Easy'
,
node
=>
'Graph::Easy::Node'
,
};
$self
;
}
sub
reset
{
# reset the status of the parser, clear errors etc.
my
$self
=
shift
;
$self
->{error} =
''
;
$self
->{anon_id} = 0;
$self
->{cluster_id} =
''
;
# each cluster gets a unique ID
$self
->{line_nr} = -1;
$self
->{match_stack} = [];
# patterns and their handlers
$self
->{clusters} = {};
# cluster names we already created
Graph::Easy::Base::_reset_id();
# start with the same set of IDs
# After "[ 1 ] -> [ 2 ]" we push "2" on the stack and when we encounter
# " -> [ 3 ]" treat the stack as a node-list left of "3".
# In addition, for " [ 1 ], [ 2 ] => [ 3 ]", the stack will contain
# "1" and "2" when we encounter "3".
$self
->{stack} = [];
$self
->{group_stack} = [];
# all the (nested) groups we are currently in
$self
->{left_stack} = [];
# stack for the left side for "[]->[],[],..."
$self
->{left_edge} =
undef
;
# for -> [A], [B] continuations
Graph::Easy->_drop_special_attributes();
$self
->{_graph} =
$self
->{use_class}->{graph}->new( {
debug
=>
$self
->{debug},
strict
=> 0,
fatal_errors
=>
$self
->{fatal_errors},
} );
$self
;
}
sub
from_file
{
# read in entire file and call from_text() on the contents
my
(
$self
,
$file
) =
@_
;
$self
=
$self
->new()
unless
ref
$self
;
my
$doc
;
local
$/ =
undef
;
# slurp mode
# if given a reference, assume it is a glob, or something like that
if
(
ref
(
$file
))
{
binmode
$file
,
':utf8'
or
die
(
"binmode '$file', ':utf8' failed: $!"
);
$doc
= <
$file
>;
}
else
{
open
my
$PARSER_FILE
,
$file
or
die
(
ref
(
$self
).
": Cannot read $file: $!"
);
binmode
$PARSER_FILE
,
':utf8'
or
die
(
"binmode '$file', ':utf8' failed: $!"
);
$doc
= <
$PARSER_FILE
>;
# read entire file
close
$PARSER_FILE
;
}
$self
->from_text(
$doc
);
}
sub
use_class
{
# use the provided class for generating objects of the type $object
my
(
$self
,
$object
,
$class
) =
@_
;
$self
->_croak(
"Expected one of node, edge, group or graph, but got $object"
)
unless
$object
=~ /^(node|group|graph|edge)\z/;
$self
->{use_class}->{
$object
} =
$class
;
$self
;
}
sub
_register_handler
{
# register a pattern and a handler for it
my
$self
=
shift
;
push
@{
$self
->{match_stack}}, [
@_
];
$self
;
}
sub
_register_attribute_handler
{
# register a handler for attributes like "{ color: red; }"
my
(
$self
,
$qr_attr
,
$target
) =
@_
;
# $object is either undef (for Graph::Easy, meaning "node", or "parent" for Graphviz)
# { attributes }
$self
->_register_handler(
qr/^$qr_attr/
,
sub
{
my
$self
=
shift
;
# This happens in the case of "[ Test ]\n { ... }", the node is consumed
# first, and the attributes are left over:
my
$stack
=
$self
->{stack};
$stack
=
$self
->{group_stack}
if
@{
$self
->{stack}} == 0;
my
$object
=
$target
;
if
(
$target
&&
$target
eq
'parent'
)
{
# for Graphviz, stray attributes always apply to the parent
$stack
=
$self
->{group_stack};
$object
=
$stack
->[-1]
if
ref
$stack
;
if
(!
defined
$object
)
{
# try the scope stack next:
$stack
=
$self
->{scope_stack};
$object
=
$self
->{_graph};
if
(!
$stack
||
@$stack
<= 1)
{
$object
=
$self
->{_graph};
$stack
= [
$self
->{_graph} ];
}
}
}
my
(
$a
,
$max_idx
) =
$self
->_parse_attributes($1||
''
,
$object
);
return
undef
if
$self
->{error};
# wrong attributes or empty stack?
if
(
ref
(
$stack
->[-1]) eq
'HASH'
)
{
# stack is a scope stack
# XXX TODO: Find out wether the attribute goes to graph, node or edge
for
my
$k
(
keys
%$a
)
{
$stack
->[-1]->{graph}->{
$k
} =
$a
->{
$k
};
}
return
1;
}
STDERR
"max_idx = $max_idx, stack contains "
,
join
(
" , "
,
@$stack
),
"\n"
if
$self
->{debug} &&
$self
->{debug} > 1;
if
(
$max_idx
!= 1)
{
my
$i
= 0;
for
my
$n
(
@$stack
)
{
$n
->set_attributes(
$a
,
$i
++);
}
}
else
{
# set attributes on all nodes/groups on stack
for
my
$n
(
@$stack
) {
$n
->set_attributes(
$a
); }
}
# This happens in the case of "[ a | b ]\n { ... }", the node is consumed
# first, and the attributes are left over. And if we encounter a basename
# attribute here, the node-parts will already have been created with the
# wrong basename, so correct this:
if
(
defined
$a
->{basename})
{
for
my
$s
(
@$stack
)
{
# for every node on the stack that is the primary one
$self
->_set_new_basename(
$s
,
$a
->{basename})
if
exists
$s
->{autosplit_parts};
}
}
1;
} );
}
sub
_register_node_attribute_handler
{
# register a handler for attributes like "[ A ] { ... }"
my
(
$self
,
$qr_node
,
$qr_oatr
) =
@_
;
$self
->_register_handler(
qr/^$qr_node$qr_oatr/
,
sub
{
my
$self
=
shift
;
my
$n1
= $1;
my
$a1
=
$self
->_parse_attributes($2||
''
);
return
undef
if
$self
->{error};
$self
->{stack} = [
$self
->_new_node (
$self
->{_graph},
$n1
,
$self
->{group_stack},
$a1
) ];
# forget left stack
$self
->{left_edge} =
undef
;
$self
->{left_stack} = [];
1;
} );
}
sub
_new_group
{
# create a new (possible anonymous) group
my
(
$self
,
$name
) =
@_
;
$name
=
''
unless
defined
$name
;
my
$gr
=
$self
->{use_class}->{group};
my
$group
;
if
(
$name
eq
''
)
{
STDERR
"# Creating new anon group.\n"
if
$self
->{debug};
$gr
.=
'::Anon'
;
$group
=
$gr
->new();
}
else
{
$name
=
$self
->_unquote(
$name
);
STDERR
"# Creating new group '$name'.\n"
if
$self
->{debug};
$group
=
$gr
->new(
name
=>
$name
);
}
$self
->{_graph}->add_group(
$group
);
my
$group_stack
=
$self
->{group_stack};
if
(
@$group_stack
> 0)
{
$group
->set_attribute(
'group'
,
$group_stack
->[-1]->{name});
}
$group
;
}
sub
_add_group_match
{
# register two handlers for group start/end
my
$self
=
shift
;
my
$qr_group_start
=
$self
->_match_group_start();
my
$qr_group_end
=
$self
->_match_group_end();
my
$qr_oatr
=
$self
->_match_optional_attributes();
# "( group start [" or empty group like "( Group )"
$self
->_register_handler(
qr/^$qr_group_start/
,
sub
{
my
$self
=
shift
;
my
$graph
=
$self
->{_graph};
my
$end
= $2;
$end
=
''
unless
defined
$end
;
# repair the start of the next node/group
$self
->{replace} =
'['
if
$end
eq
'['
;
$self
->{replace} =
'('
if
$end
eq
'('
;
# create the new group
my
$group
=
$self
->_new_group($1);
if
(
$end
eq
')'
)
{
# we matched an empty group like "()", or "( group name )"
$self
->{stack} = [
$group
];
STDERR
"# Seen end of group '$group->{name}'.\n"
if
$self
->{debug};
}
else
{
# only put the group on the stack if it is still open
push
@{
$self
->{group_stack}},
$group
;
}
1;
} );
# ") { }" # group end (with optional attributes)
$self
->_register_handler(
qr/^$qr_group_end$qr_oatr/
,
sub
{
my
$self
=
shift
;
my
$group
=
pop
@{
$self
->{group_stack}};
return
$self
->parse_error(0)
if
!
defined
$group
;
STDERR
"# Seen end of group '$group->{name}'.\n"
if
$self
->{debug};
my
$a1
=
$self
->_parse_attributes($1||
''
,
'group'
, NO_MULTIPLES);
return
undef
if
$self
->{error};
$group
->set_attributes(
$a1
);
# the new left side is the group itself
$self
->{stack} = [
$group
];
1;
} );
}
sub
_build_match_stack
{
# put all known patterns and their handlers on the match stack
my
$self
=
shift
;
# regexps for the different parts
my
$qr_node
=
$self
->_match_node();
my
$qr_attr
=
$self
->_match_attributes();
my
$qr_oatr
=
$self
->_match_optional_attributes();
my
$qr_edge
=
$self
->_match_edge();
my
$qr_comma
=
$self
->_match_comma();
my
$qr_class
=
$self
->_match_class_selector();
my
$e
=
$self
->{use_class}->{edge};
# node { color: red; }
# node.graph { ... }
# .foo { ... }
# .foo, node, edge.red { ... }
$self
->_register_handler(
qr/^\s*$qr_class$qr_attr/
,
sub
{
my
$self
=
shift
;
my
$class
=
lc
($1 ||
''
);
my
$att
=
$self
->_parse_attributes($2 ||
''
,
$class
, NO_MULTIPLES );
return
undef
unless
defined
$att
;
# error in attributes?
my
$graph
=
$self
->{_graph};
$graph
->set_attributes (
$class
,
$att
);
# forget stacks
$self
->{stack} = [];
$self
->{left_edge} =
undef
;
$self
->{left_stack} = [];
1;
} );
$self
->_add_group_match();
$self
->_register_attribute_handler(
$qr_attr
);
$self
->_register_node_attribute_handler(
$qr_node
,
$qr_oatr
);
# , [ Berlin ] { color: red; }
$self
->_register_handler(
qr/^$qr_comma$qr_node$qr_oatr/
,
sub
{
my
$self
=
shift
;
my
$graph
=
$self
->{_graph};
my
$n1
= $1;
my
$a1
=
$self
->_parse_attributes($2||
''
);
return
undef
if
$self
->{error};
push
@{
$self
->{stack}},
$self
->_new_node (
$graph
,
$n1
,
$self
->{group_stack},
$a1
,
$self
->{stack});
if
(
defined
$self
->{left_edge})
{
my
(
$style
,
$edge_label
,
$edge_atr
,
$edge_bd
,
$edge_un
) = @{
$self
->{left_edge}};
foreach
my
$node
(@{
$self
->{left_stack}})
{
my
$edge
=
$e
->new( {
style
=>
$style
,
name
=>
$edge_label
} );
$edge
->set_attributes(
$edge_atr
);
# "<--->": bidirectional
$edge
->bidirectional(1)
if
$edge_bd
;
$edge
->undirected(1)
if
$edge_un
;
$graph
->add_edge (
$node
,
$self
->{stack}->[-1],
$edge
);
}
}
1;
} );
# Things like "[ Node ]" will be consumed before, so we do not need a case
# for "[ A ] -> [ B ]":
# node chain continued like "-> { ... } [ Kassel ] { ... }"
$self
->_register_handler(
qr/^$qr_edge$qr_oatr$qr_node$qr_oatr/
,
sub
{
my
$self
=
shift
;
return
if
@{
$self
->{stack}} == 0;
# only match this if stack non-empty
my
$graph
=
$self
->{_graph};
my
$eg
= $1;
# entire edge ("-- label -->" etc)
my
$edge_bd
= $2 || $4;
# bidirectional edge ('<') ?
my
$edge_un
= 0;
# undirected edge?
$edge_un
= 1
if
!
defined
$2 && !
defined
$5;
# optional edge label
my
$edge_label
= $7;
my
$ed
= $3 || $5 || $1;
# edge pattern/style ("--")
my
$edge_atr
= $11 ||
''
;
# save edge attributes
my
$n
= $12;
# node name
my
$a1
=
$self
->_parse_attributes($13||
''
);
# node attributes
$edge_atr
=
$self
->_parse_attributes(
$edge_atr
,
'edge'
);
return
undef
if
$self
->{error};
# allow undefined edge labels for setting them from the class
# strip trailing spaces and convert \[ => [
$edge_label
=
$self
->_unquote(
$edge_label
)
if
defined
$edge_label
;
# strip trailing spaces
$edge_label
=~ s/\s+\z//
if
defined
$edge_label
;
# the right side node(s) (multiple in case of autosplit)
my
$nodes_b
= [
$self
->_new_node (
$self
->{_graph},
$n
,
$self
->{group_stack},
$a1
) ];
my
$style
=
$self
->_link_lists(
$self
->{stack},
$nodes_b
,
$ed
,
$edge_label
,
$edge_atr
,
$edge_bd
,
$edge_un
);
# remember the left side
$self
->{left_edge} = [
$style
,
$edge_label
,
$edge_atr
,
$edge_bd
,
$edge_un
];
$self
->{left_stack} =
$self
->{stack};
# forget stack and remember the right side instead
$self
->{stack} =
$nodes_b
;
1;
} );
my
$qr_group_start
=
$self
->_match_group_start();
# Things like ")" will be consumed before, so we do not need a case
# for ") -> { ... } ( Group [ B ]":
# edge to a group like "-> { ... } ( Group ["
$self
->_register_handler(
qr/^$qr_edge$qr_oatr$qr_group_start/
,
sub
{
my
$self
=
shift
;
return
if
@{
$self
->{stack}} == 0;
# only match this if stack non-empty
my
$eg
= $1;
# entire edge ("-- label -->" etc)
my
$edge_bd
= $2 || $4;
# bidirectional edge ('<') ?
my
$edge_un
= 0;
# undirected edge?
$edge_un
= 1
if
!
defined
$2 && !
defined
$5;
# optional edge label
my
$edge_label
= $7;
my
$ed
= $3 || $5 || $1;
# edge pattern/style ("--")
my
$edge_atr
= $11 ||
''
;
# save edge attributes
my
$gn
= $12;
# matched "-> ( Group [" or "-> ( Group ("
$self
->{replace} =
'['
if
defined
$13 && $13 eq
'['
;
$self
->{replace} =
'('
if
defined
$13 && $13 eq
'('
;
$edge_atr
=
$self
->_parse_attributes(
$edge_atr
,
'edge'
);
return
undef
if
$self
->{error};
# get the last group of the stack, lest the new one gets nested in it
pop
@{
$self
->{group_stack}};
$self
->{group_stack} = [
$self
->_new_group(
$gn
) ];
# allow undefined edge labels for setting them from the class
$edge_label
=
$self
->_unquote(
$edge_label
)
if
$edge_label
;
# strip trailing spaces
$edge_label
=~ s/\s+\z//
if
$edge_label
;
my
$style
=
$self
->_link_lists(
$self
->{stack},
$self
->{group_stack},
$ed
,
$edge_label
,
$edge_atr
,
$edge_bd
,
$edge_un
);
# remember the left side
$self
->{left_edge} = [
$style
,
$edge_label
,
$edge_atr
,
$edge_bd
,
$edge_un
];
$self
->{left_stack} =
$self
->{stack};
# forget stack
$self
->{stack} = [];
# matched "->()" so remember the group on the stack
$self
->{stack} = [
$self
->{group_stack}->[-1] ]
if
defined
$13 && $13 eq
')'
;
1;
} );
}
sub
_line_insert
{
# what to insert between two lines, '' for Graph::Easy, ' ' for Graphviz;
''
;
}
sub
_clean_line
{
# do some cleanups on a line before handling it
my
(
$self
,
$line
) =
@_
;
chomp
(
$line
);
# convert #808080 into \#808080, and "#fff" into "\#fff"
my
$sep
=
$self
->{attr_sep};
$line
=~ s/
$sep
\s*(
"?)(#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3}))("
?)/
$sep
$1\\$2$3/g;
# remove comment at end of line (but leave \# alone):
$line
=~ s/(:[^\\]|)
$self
->{qr_comment}.*/$1/;
# remove white space at end (but not at the start, to keep " ||" intact
$line
=~ s/\s+\z//;
# print STDERR "# at line '$line' stack: ", join(",",@{ $self->{stack}}),"\n";
$line
;
}
sub
from_text
{
my
(
$self
,
$txt
) =
@_
;
# matches a multi-line comment
my
$o_cmt
=
qr#((\s*/\*.*?\*/\s*)*\s*|\s+)#
;
if
((
ref
(
$self
)||
$self
) eq
'Graph::Easy::Parser'
&&
# contains "digraph GRAPH {" or something similiar
(
$txt
=~ /^(\s*|\s*\/\*.*?\*\/\s*)(strict)?
$o_cmt
(di)?graph
$o_cmt
(
"[^"
]*"|[\w_]+)
$o_cmt
\{/im ||
# contains "digraph {" or something similiar
$txt
=~ /^(\s*|\s*\/\*.*?\*\/\s*)(strict)?${o_cmt}digraph
$o_cmt
\{/im ||
# contains "strict graph {" or something similiar
$txt
=~ /^(\s*|\s*\/\*.*?\*\/\s*)strict${o_cmt}(di)?graph
$o_cmt
\{/im))
{
# recreate ourselfes, and pass our arguments along
my
$debug
= 0;
my
$old_self
=
$self
;
if
(
ref
(
$self
))
{
$debug
=
$self
->{debug};
$self
->{fatal_errors} = 0;
}
$self
= Graph::Easy::Parser::Graphviz->new(
debug
=>
$debug
,
fatal_errors
=> 0 );
$self
->
reset
();
$self
->{_old_self} =
$old_self
if
ref
(
$self
);
}
if
((
ref
(
$self
)||
$self
) eq
'Graph::Easy::Parser'
&&
# contains "graph: {"
$txt
=~ /^([\s\n\t]*|\s*\/\*.*?\*\/\s*)graph\s*:\s*\{/m)
{
# recreate ourselfes, and pass our arguments along
my
$debug
= 0;
my
$old_self
=
$self
;
if
(
ref
(
$self
))
{
$debug
=
$self
->{debug};
$self
->{fatal_errors} = 0;
}
$self
= Graph::Easy::Parser::VCG->new(
debug
=>
$debug
,
fatal_errors
=> 0 );
$self
->
reset
();
$self
->{_old_self} =
$old_self
if
ref
(
$self
);
}
$self
=
$self
->new()
unless
ref
$self
;
$self
->
reset
();
my
$graph
=
$self
->{_graph};
return
$graph
if
!
defined
$txt
||
$txt
=~ /^\s*\z/;
# empty text?
my
$uc
=
$self
->{use_class};
# instruct the graph to use the custom classes, too
for
my
$o
(
keys
%$uc
)
{
$graph
->use_class(
$o
,
$uc
->{
$o
})
unless
$o
eq
'graph'
;
# group, node and edge
}
my
@lines
=
split
/(\r\n|\n|\r)/,
$txt
;
my
$backbuffer
=
''
;
# left over fragments to be combined with next line
my
$qr_comment
=
$self
->_match_commented_line();
$self
->{qr_comment} =
$self
->_match_comment();
# cache the value of this since it can be expensive to construct:
$self
->{_match_single_attribute} =
$self
->_match_single_attribute();
$self
->_build_match_stack();
###########################################################################
# main parsing loop
my
$handled
= 0;
# did we handle a fragment?
my
$line
;
# my $counts = {};
LINE:
while
(
@lines
> 0 ||
$backbuffer
ne
''
)
{
# only accumulate more text if we didn't handle a fragment
if
(
@lines
> 0 &&
$handled
== 0)
{
$self
->{line_nr}++;
my
$curline
=
shift
@lines
;
# discard empty lines, or completely commented out lines
next
if
$curline
=~
$qr_comment
;
# convert tabs to spaces (the regexps don't expect tabs)
$curline
=~
tr
/\t/ /d;
# combine backbuffer, what to insert between two lines and next line:
$line
=
$backbuffer
.
$self
->_line_insert() .
$self
->_clean_line(
$curline
);
}
STDERR
"# Line is '$line'\n"
if
$self
->{debug} &&
$self
->{debug} > 2;
STDERR
"# Backbuffer is '$backbuffer'\n"
if
$self
->{debug} &&
$self
->{debug} > 2;
$handled
= 0;
#debug my $count = 0;
PATTERN:
for
my
$entry
(@{
$self
->{match_stack}})
{
# nothing to match against?
last
PATTERN
if
$line
eq
''
;
$self
->{replace} =
''
;
# as default just remove the matched text
my
(
$pattern
,
$handler
,
$replace
) =
@$entry
;
STDERR
"# Matching against $pattern\n"
if
$self
->{debug} &&
$self
->{debug} > 3;
if
(
$line
=~
$pattern
)
{
#debug $counts->{$count}++;
STDERR
"# Matched, calling handler\n"
if
$self
->{debug} &&
$self
->{debug} > 2;
my
$rc
= 1;
$rc
=
&$handler
(
$self
)
if
defined
$handler
;
if
(
$rc
)
{
$replace
=
$self
->{replace}
unless
defined
$replace
;
$replace
=
&$replace
(
$self
,
$line
)
if
ref
(
$replace
);
STDERR
"# Handled it successfully.\n"
if
$self
->{debug} &&
$self
->{debug} > 2;
$line
=~ s/
$pattern
/
$replace
/;
STDERR
"# Line is now '$line' (replaced with '$replace')\n"
if
$self
->{debug} &&
$self
->{debug} > 2;
$handled
++;
last
PATTERN;
}
}
#debug $count ++;
}
#debug if ($handled == 0) { $counts->{'-1'}++; }
# couldn't handle that fragement, so accumulate it and try again
$backbuffer
=
$line
;
# stop at the very last line
last
LINE
if
$handled
== 0 &&
@lines
== 0;
# stop at parsing errors
last
LINE
if
$self
->{error};
}
$self
->error(
"'$backbuffer' not recognized by "
.
ref
(
$self
))
if
$backbuffer
ne
''
;
# if something was left on the stack, file ended unexpectedly
$self
->parse_error(7)
if
!
$self
->{error} &&
$self
->{scope_stack} && @{
$self
->{scope_stack}} > 0;
return
undef
if
$self
->{error} &&
$self
->{fatal_errors};
#debug use Data::Dumper; print Dumper($counts);
STDERR
"# Parsing done.\n"
if
$graph
->{debug};
# Do final cleanup (for parsing Graphviz)
$self
->_parser_cleanup()
if
$self
->can(
'_parser_cleanup'
);
$graph
->_drop_special_attributes();
# turn on strict checking on returned graph
$graph
->strict(1);
$graph
->fatal_errors(1);
$graph
;
}
#############################################################################
# internal routines
sub
_edge_style
{
my
(
$self
,
$ed
) =
@_
;
my
$style
=
undef
;
# default is "inherit from class"
$style
=
'double-dash'
if
$ed
=~ /^(= )+\z/;
$style
=
'double'
if
$ed
=~ /^=+\z/;
$style
=
'dotted'
if
$ed
=~ /^\.+\z/;
$style
=
'dashed'
if
$ed
=~ /^(- )+\z/;
$style
=
'dot-dot-dash'
if
$ed
=~ /^(..-)+\z/;
$style
=
'dot-dash'
if
$ed
=~ /^(\.-)+\z/;
$style
=
'wave'
if
$ed
=~ /^\~+\z/;
$style
=
'bold'
if
$ed
=~ /^
#+\z/;
$style
;
}
sub
_link_lists
{
# Given two node lists and an edge style, links each node from list
# one to list two.
my
(
$self
,
$left
,
$right
,
$ed
,
$label
,
$edge_atr
,
$edge_bd
,
$edge_un
) =
@_
;
my
$graph
=
$self
->{_graph};
my
$style
=
$self
->_edge_style(
$ed
);
my
$e
=
$self
->{use_class}->{edge};
# add edges for all nodes in the left list
for
my
$node
(
@$left
)
{
for
my
$node_b
(
@$right
)
{
my
$edge
=
$e
->new( {
style
=>
$style
,
name
=>
$label
} );
$graph
->add_edge (
$node
,
$node_b
,
$edge
);
# 'string' => [ 'string' ]
# [ { hash }, 'string' ] => [ { hash }, 'string' ]
my
$e
=
$edge_atr
;
$e
= [
$edge_atr
]
unless
ref
(
$e
) eq
'ARRAY'
;
for
my
$a
(
@$e
)
{
if
(
ref
$a
)
{
$edge
->set_attributes(
$a
);
}
else
{
# deferred parsing with the object as param:
my
$out
=
$self
->_parse_attributes(
$a
,
$edge
);
return
undef
if
$self
->{error};
$edge
->set_attributes(
$out
);
}
}
# "<--->": bidirectional
$edge
->bidirectional(1)
if
$edge_bd
;
$edge
->undirected(1)
if
$edge_un
;
}
}
$style
;
}
sub
_unquote_attribute
{
my
(
$self
,
$name
,
$value
) =
@_
;
$self
->_unquote(
$value
);
}
sub
_unquote
{
my
(
$self
,
$name
,
$no_collapse
) =
@_
;
$name
=
''
unless
defined
$name
;
# unquote special chars
$name
=~ s/\\([\[\(\{\}\]\)
#<>\-\.\=])/$1/g;
# collapse multiple spaces
$name
=~ s/\s+/ /g
unless
$no_collapse
;
$name
;
}
sub
_add_node
{
# add a node to the graph, overidable by subclasses
my
(
$self
,
$graph
,
$name
) =
@_
;
$graph
->add_node(
$name
);
# add unless exists
}
sub
_get_cluster_name
{
# create a unique name for an autosplit node
my
(
$self
,
$base_name
) =
@_
;
# Try to find a unique cluster name in case some one get's creative and names the
# last part "-1":
# does work without cluster-id?
if
(
exists
$self
->{clusters}->{
$base_name
})
{
my
$g
= 1;
while
(
$g
== 1)
{
my
$base_try
=
$base_name
;
$base_try
.=
'-'
.
$self
->{cluster_id}
if
$self
->{cluster_id};
last
if
!
exists
$self
->{clusters}->{
$base_try
};
$self
->{cluster_id}++;
}
$base_name
.=
'-'
.
$self
->{cluster_id}
if
$self
->{cluster_id};
$self
->{cluster_id}++;
}
$self
->{clusters}->{
$base_name
} =
undef
;
# reserve this name
$base_name
;
}
sub
_set_new_basename
{
# when encountering something like:
# [ a | b ]
# { basename: foo; }
# the Parser will create two nodes, ab.0 and ab.1, and then later see
# the "basename: foo". Sowe need to rename the already created nodes
# due to the changed basename:
my
(
$self
,
$node
,
$new_basename
) =
@_
;
# nothing changes?
return
if
$node
->{autosplit_basename} eq
$new_basename
;
my
$g
=
$node
->{graph};
my
@parts
= @{
$node
->{autosplit_parts}};
my
$nr
= 0;
for
my
$part
(
$node
,
@parts
)
{
STDERR
"# Setting new basename $new_basename for node $part->{name}\n"
if
$self
->{debug} > 1;
$part
->{autosplit_basename} =
$new_basename
;
$part
->set_attribute(
'basename'
,
$new_basename
);
# delete it from the list of nodes
delete
$g
->{nodes}->{
$part
->{name}};
$part
->{name} =
$new_basename
.
'.'
.
$nr
;
$nr
++;
# and re-insert it with the right name
$g
->{nodes}->{
$part
->{name}} =
$part
;
# we do not need to care for edges here, as they are stored with refs
# to the nodes and not the node names itself
}
}
sub
_autosplit_node
{
# Takes a node name like "a|b||c" and splits it into "a", "b", and "c".
# Returns the individual parts.
my
(
$self
,
$graph
,
$name
,
$att
,
$allow_empty
) =
@_
;
# Default is to have empty parts. Graphviz sets this to true;
$allow_empty
= 1
unless
defined
$allow_empty
;
my
@rc
;
my
$uc
=
$self
->{use_class};
my
$qr_clean
=
$self
->{_qr_part_clean};
# build base name: "A|B |C||D" => "ABCD"
my
$base_name
=
$name
;
$base_name
=~ s/\s*\|\|?\s*//g;
# use user-provided base name
$base_name
=
$att
->{basename}
if
exists
$att
->{basename};
# strip trailing/leading spaces on basename
$base_name
=~ s/\s+\z//;
$base_name
=~ s/^\s+//;
# first one gets: "ABC", second one "ABC.1" and so on
$base_name
=
$self
->_get_cluster_name(
$base_name
);
STDERR
"# Parser: Autosplitting node with basename '$base_name'\n"
if
$graph
->{debug};
my
$first_in_row
;
# for relative placement of new row
my
$x
= 0;
my
$y
= 0;
my
$idx
= 0;
my
$remaining
=
$name
;
my
$sep
;
my
$last_sep
=
''
;
my
$add
= 0;
while
(
$remaining
ne
''
)
{
# XXX TODO: parsing of "\|" and "|" in one node
$remaining
=~ s/^((\\\||[^\|])*)(\|\|?|\z)//;
my
$part
= $1 ||
' '
;
$sep
= $3;
my
$port_name
=
''
;
# possible cleanup for this part
if
(
$qr_clean
)
{
$part
=~ s/^
$qr_clean
//;
$port_name
= $1;
}
# fix [|G|] to have one empty part as last part
if
(
$add
== 0 &&
$remaining
eq
''
&&
$sep
=~ /\|\|?/)
{
$add
++;
# only do it once
$remaining
.=
'|'
}
STDERR
"# Parser: Found autosplit part '$part'\n"
if
$graph
->{debug};
my
$class
=
$uc
->{node};
if
(
$allow_empty
&&
$part
eq
' '
)
{
# create an empty node with no border
$class
.=
"::Empty"
;
}
elsif
(
$part
=~ /^[ ]{2,}\z/)
{
# create an empty node with border
$part
=
' '
;
}
else
{
$part
=~ s/^\s+//;
# rem spaces at front
$part
=~ s/\s+\z//;
# rem spaces at end
}
my
$node_name
=
"$base_name.$idx"
;
if
(
$graph
->{debug})
{
my
$empty
=
''
;
$empty
=
' empty'
if
$class
ne
$self
->{use_class}->{node};
STDERR
"# Parser: Creating$empty autosplit part '$part'\n"
if
$graph
->{debug};
}
# if it doesn't exist, add it, otherwise retrieve node object to $node
if
(
$class
=~ /::Empty/)
{
my
$node
=
$graph
->node(
$node_name
);
if
(!
defined
$node
)
{
# create node object from the correct class
$node
=
$class
->new(
$node_name
);
$graph
->add_node(
$node
);
}
}
my
$node
=
$graph
->add_node(
$node_name
);
$node
->{autosplit_label} =
$part
;
# remember these two for Graphviz
$node
->{autosplit_portname} =
$port_name
;
$node
->{autosplit_basename} =
$base_name
;
push
@rc
,
$node
;
if
(
@rc
== 1)
{
# for correct as_txt output
$node
->{autosplit} =
$name
;
$node
->{autosplit} =~ s/\s+\z//;
# strip trailing spaces
$node
->{autosplit} =~ s/^\s+//;
# strip leading spaces
$node
->{autosplit} =~ s/([^\|])\s+\|/$1 \|/g;
# 'foo |' => 'foo |'
$node
->{autosplit} =~ s/\|\s+([^\|])/\| $1/g;
# '| foo' => '| foo'
$node
->set_attribute(
'basename'
,
$att
->{basename})
if
defined
$att
->{basename};
# list of all autosplit parts so as_txt() can find them easily again
$node
->{autosplit_parts} = [ ];
$first_in_row
=
$node
;
}
else
{
# second, third etc. get previous as origin
my
(
$sx
,
$sy
) = (1,0);
my
$origin
=
$rc
[-2];
if
(
$last_sep
eq
'||'
)
{
(
$sx
,
$sy
) = (0,1);
$origin
=
$first_in_row
;
$first_in_row
=
$node
;
}
$node
->relative_to(
$origin
,
$sx
,
$sy
);
push
@{
$rc
[0]->{autosplit_parts}},
$node
;
weaken @{
$rc
[0]->{autosplit_parts}}[-1];
# suppress as_txt output for other parts
$node
->{autosplit} =
undef
;
}
# nec. for border-collapse
$node
->{autosplit_xy} =
"$x,$y"
;
$idx
++;
# next node ID
$last_sep
=
$sep
;
$x
++;
# || starts a new row:
if
(
$sep
eq
'||'
)
{
$x
= 0;
$y
++;
}
}
# end for all parts
@rc
;
# return all created nodes
}
sub
_new_node
{
# Create a new node unless it doesn't already exist. If the group stack
# contains entries, the new node appears first in this/these group(s), so
# add it to these groups. If the newly created node contains "|", we auto
# split it up into several nodes and cluster these together.
my
(
$self
,
$graph
,
$name
,
$group_stack
,
$att
,
$stack
) =
@_
;
STDERR
"# Parser: new node '$name'\n"
if
$graph
->{debug};
$name
=
$self
->_unquote(
$name
,
'no_collapse'
);
my
$autosplit
;
my
$uc
=
$self
->{use_class};
my
@rc
= ();
if
(
$name
=~ /^\s*\z/)
{
STDERR
"# Parser: Creating anon node\n"
if
$graph
->{debug};
# create a new anon node and add it to the graph
my
$class
=
$uc
->{node} .
'::Anon'
;
my
$node
=
$class
->new();
@rc
= (
$graph
->add_node(
$node
) );
}
# nodes to be autosplit will be done in a sep. pass for Graphviz
elsif
((
ref
(
$self
) eq
'Graph::Easy::Parser'
) &&
$name
=~ /[^\\]\|/)
{
$autosplit
= 1;
@rc
=
$self
->_autosplit_node(
$graph
,
$name
,
$att
);
}
else
{
# strip trailing and leading spaces
$name
=~ s/\s+\z//;
$name
=~ s/^\s+//;
# collapse multiple spaces
$name
=~ s/\s+/ /g;
# unquote \|
$name
=~ s/\\\|/\|/g;
if
(
$self
->{debug})
{
if
(!
$graph
->node(
$name
))
{
STDERR
"# Parser: Creating normal node from name '$name'.\n"
;
}
else
{
STDERR
"# Parser: Found node '$name' already in graph.\n"
;
}
}
@rc
= (
$self
->_add_node(
$graph
,
$name
) );
# add to graph, unless exists
}
$self
->parse_error(5)
if
exists
$att
->{basename} && !
$autosplit
;
my
$b
=
$att
->{basename};
delete
$att
->{basename};
# on a node list "[A],[B] { ... }" set attributes on all nodes
# encountered so far, too:
if
(
defined
$stack
)
{
for
my
$node
(
@$stack
)
{
$node
->set_attributes (
$att
, 0);
}
}
my
$index
= 0;
my
$group
=
$self
->{group_stack}->[-1];
for
my
$node
(
@rc
)
{
$node
->add_to_group(
$group
)
if
$group
;
$node
->set_attributes (
$att
,
$index
);
$index
++;
}
$att
->{basename} =
$b
if
defined
$b
;
# return list of created nodes (usually one, but more for "A|B")
@rc
;
}
sub
_match_comma
{
# return a regexp that matches something like " , " like in:
# "[ Bonn ], [ Berlin ] => [ Hamburg ]"
qr/\s*,\s*/
;
}
sub
_match_comment
{
# match the start of a comment
qr/(^|[^\\])#/
;
}
sub
_match_commented_line
{
# match empty lines or a completely commented out line
qr/^\s*(#|\z)/
;
}
sub
_match_attributes
{
# return a regexp that matches something like " { color: red; }" and returns
# the inner text without the {}
qr/\s*\{\s*([^\}]+?)\s*\}/
;
}
sub
_match_optional_attributes
{
# return a regexp that matches something like " { color: red; }" and returns
# the inner text with the {}
qr/(\s*\{[^\}]+?\})?/
;
}
sub
_match_node
{
# return a regexp that matches something like " [ bonn ]" and returns
# the inner text without the [] (might leave some spaces)
qr/\s*\[ # '[' start of the node
(
(?: # non-capturing group
\\. # either '\]' or '\N' etc.
| # or
[^\]\\] # not ']' and not '\'
)* # 0 times for '[]'
)
\]/
x;
# followed by ']'
}
sub
_match_class_selector
{
my
$class
=
qr/(?:\.\w+|graph|(?:edge|group|node)(?:\.\w+)?)/
;
qr/($class(?:\s*,\s*$class)*)/
;
}
sub
_match_single_attribute
{
qr/\s*([^:]+?)\s*:\s*("(?:\\"|[^"])+"|(?:\\;|[^;])+?)(?:\s*;\s*|\s*\z)/
;
# "name: value"
}
sub
_match_group_start
{
# Return a regexp that matches something like " ( group [" and returns
# the text between "(" and "[". Also matches empty groups like "( group )"
# or even "()":
qr/\s*\(\s*([^\[\)\(]*?)\s*([\[\)\(])/
;
}
sub
_match_group_end
{
# return a regexp that matches something like " )".
qr/\s*\)\s*/
;
}
sub
_match_edge
{
# Matches all possible edge variants like:
# -->, ---->, ==> etc
# <-->, <---->, <==>, <..> etc
# <-- label -->, <.- label .-> etc
# -- label -->, .- label .-> etc
# "- " must come before "-"!
# likewise, "..-" must come before ".-" must come before "."
# XXX TODO: convert the first group into a non-matching group
qr/\s*
( # egde without label ("-->")
(<?) # optional left "<"
(=\s|=|-\s|-|\.\.-|\.-|\.|~)+> # pattern (style) of edge
| # edge with label ("-- label -->")
(<?) # optional left "<"
((=\s|=|-\s|-|\.\.-|\.-|\.|~)+) # pattern (style) of edge
\s+ # followed by at least a space
((?:\\.|[^>\[\{])*?) # either \\, \[ etc, or not ">", "[", "{"
(\s+\5)> # a space and pattern before ">"
# inserting this needs mucking with all the code that access $5 etc
# | # undirected edge (without arrows, but with label)
# ((=\s|=|-\s|-|\.\.-|\.-|\.|~)+) # pattern (style) of edge
# \s+ # followed by at least a space
# ((?:\\.|[^>\[\{])*?) # either \\, \[ etc, or not ">", "[", "{"
# (\s+\10) # a space and pattern
| # undirected edge (without arrows and label)
(\.\.-|\.-)+ # pattern (style) of edge (at least once)
|
(=\s|=|-\s|-|\.|~){2,} # these at least two times
)
/
x;
}
sub
_clean_attributes
{
my
(
$self
,
$text
) =
@_
;
$text
=~ s/^\s*\{\s*//;
# remove left-over "{" and spaces
$text
=~ s/\s*\}\s*\z//;
# remove left-over "}" and spaces
$text
;
}
sub
_parse_attributes
{
# Takes a text like "attribute: value; attribute2 : value2;" and
# returns a hash with the attributes. $class defaults to 'node'.
# In list context, also returns a flag that is maxlevel-1 when one
# of the attributes was a multiple one (aka 2 for "red|green", 1 for "red");
my
(
$self
,
$text
,
$object
,
$no_multiples
) =
@_
;
my
$class
=
$object
;
$class
=
$object
->{class}
if
ref
(
$object
);
$class
=
'node'
unless
defined
$class
;
$class
=~ s/\..*//;
# remove subclass
my
$out
;
my
$att
= {};
my
$multiples
= 0;
$text
=
$self
->_clean_attributes(
$text
);
my
$qr_att
=
$self
->{_match_single_attribute};
my
$qr_cmt
;
$qr_cmt
=
$self
->_match_multi_line_comment()
if
$self
->can(
'_match_multi_line_comment'
);
my
$qr_satt
;
$qr_satt
=
$self
->_match_special_attribute()
if
$self
->can(
'_match_special_attribute'
);
return
{}
if
$text
=~ /^\s*\z/;
STDERR
"attr parsing: matching\n '$text'\n against $qr_att\n"
if
$self
->{debug} > 3;
while
(
$text
ne
''
)
{
STDERR
"attr parsing: matching '$text'\n"
if
$self
->{debug} > 3;
# remove a possible comment
$text
=~ s/^
$qr_cmt
//g
if
$qr_cmt
;
# if the last part was a comment, we end up with an empty text here:
last
if
$text
=~ /^\s*\z/;
# match and remove "name: value"
my
$done
= (
$text
=~ s/^
$qr_att
//) || 0;
# match and remove "name" if "name: value;" didn't match
$done
++
if
$done
== 0 &&
$qr_satt
&& (
$text
=~ s/^
$qr_satt
//);
return
$self
->error (
"Error in attribute: '$text' doesn't look valid to me."
)
if
$done
== 0;
my
$name
= $1;
my
$v
= $2;
$v
=
''
unless
defined
$v
;
# for special attributes w/o value
# unquote and store
$out
->{
$name
} =
$self
->_unquote_attribute(
$name
,
$v
);
}
if
(
$self
->{debug} &&
$self
->{debug} > 1)
{
STDERR
"# "
,
join
(
" "
,
caller
),
"\n"
;
STDERR
"# Parsed attributes into:\n"
, Data::Dumper::Dumper(
$out
),
"\n"
;
}
# possible remap attributes (for parsing Graphviz)
$out
=
$self
->_remap_attributes(
$out
,
$object
)
if
$self
->can(
'_remap_attributes'
);
my
$g
=
$self
->{_graph};
# check for being valid and finally create hash with name => value pairs
for
my
$name
(
sort
keys
%$out
)
{
my
(
$rc
,
$newname
,
$v
) =
$g
->validate_attribute(
$name
,
$out
->{
$name
},
$class
,
$no_multiples
);
$self
->error(
$g
->{error})
if
defined
$rc
;
$multiples
=
scalar
@$v
if
ref
(
$v
) eq
'ARRAY'
;
$att
->{
$newname
} =
$v
if
defined
$v
;
# undef => ignore attribute
}
return
$att
unless
wantarray
;
(
$att
,
$multiples
|| 1);
}
sub
parse_error
{
# take a msg number, plus params, and throws an exception
my
$self
=
shift
;
my
$msg_nr
=
shift
;
# XXX TODO: should really use the msg nr mapping
my
$msg
=
"Found unexpected group end"
;
# 0
$msg
=
"Error in attribute: '##param2##' is not a valid attribute for a ##param3##"
# 1
if
$msg_nr
== 1;
$msg
=
"Error in attribute: '##param1##' is not a valid ##param2## for a ##param3##"
if
$msg_nr
== 2;
# 2
$msg
=
"Error: Found attributes, but expected group or node start"
if
$msg_nr
== 3;
# 3
$msg
=
"Error in attribute: multi-attribute '##param1##' not allowed here"
if
$msg_nr
== 4;
# 4
$msg
=
"Error in attribute: basename not allowed for non-autosplit nodes"
if
$msg_nr
== 5;
# 5
# for graphviz parsing
$msg
=
"Error: Already seen graph start"
if
$msg_nr
== 6;
# 6
$msg
=
"Error: Expected '}', but found file end"
if
$msg_nr
== 7;
# 7
my
$i
= 1;
foreach
my
$p
(
@_
)
{
$msg
=~ s/
##param$i##/$p/g; $i++;
}
$self
->error(
$msg
.
' at line '
.
$self
->{line_nr});
}
sub
_parser_cleanup
{
# After initial parsing, do a cleanup pass.
my
(
$self
) =
@_
;
my
$g
=
$self
->{_graph};
for
my
$n
(
values
%{
$g
->{nodes}})
{
next
if
$n
->{autosplit};
$self
->
warn
(
"Node '"
.
$self
->_quote(
$n
->{name}) .
"' has an offset but no origin"
)
if
((
$n
->attribute(
'offset'
) ne
'0,0'
) &&
$n
->attribute(
'origin'
) eq
''
);
}
$self
;
}
sub
_quote
{
# make a node name safe for error message output
my
(
$self
,
$n
) =
@_
;
$n
=~ s/
'/\\'
/g;
$n
;
}
1;
__END__
=head1 NAME
Graph::Easy::Parser - Parse Graph::Easy from textual description
=head1 SYNOPSIS
# creating a graph from a textual description
use Graph::Easy::Parser;
my $parser = Graph::Easy::Parser->new();
my $graph = $parser->from_text(
'[ Bonn ] => [ Berlin ]'.
'[ Berlin ] => [ Rostock ]'.
);
print $graph->as_ascii();
print $parser->from_file('mygraph.txt')->as_ascii();
# Also works automatically on graphviz code:
print Graph::Easy::Parser->from_file('mygraph.dot')->as_ascii();
=head1 DESCRIPTION
C<Graph::Easy::Parser> lets you parse simple textual descriptions
of graphs, and constructs a C<Graph::Easy> object from them.
The resulting object can than be used to layout and output the graph.
=head2 Input
The input consists of text describing the graph, encoded in UTF-8.
Example:
[ Bonn ] --> [ Berlin ]
[ Frankfurt ] <=> [ Dresden ]
[ Bonn ] --> [ Frankfurt ]
[ Bonn ] = > [ Frankfurt ]
=head3 Graphviz
In addition there is a bit of magic that detects graphviz code, so
input of the following form will also work:
digraph Graph1 {
"Bonn" -> "Berlin"
}
Note that the magic detection only works for B<named> graphs or graph
with "digraph" at their start, so the following will not be detected as
graphviz code because it looks exactly like valid Graph::Easy code
at the start:
graph {
"Bonn" -> "Berlin"
}
See L<Graph::Easy::Parser::Graphviz> for more information about parsing
graphs in the DOT language.
=head3 VCG
In addition there is a bit of magic that detects VCG code, so
input of the following form will also work:
graph: {
node: { title: Bonn; }
node: { title: Berlin; }
edge: { sourcename: Bonn; targetname: Berlin; }
}
See L<Graph::Easy::Parser::VCG> for more information about parsing
graphs in the VCG language.
=head2 Input Syntax
This is a B<very> brief description of the syntax for the Graph::Easy
language, for a full specification, please see L<Graph::Easy::Manual>.
=over 2
=item nodes
Nodes are rendered (or "quoted", if you wish) with enclosing square brackets:
[ Single node ]
[ Node A ] --> [ Node B ]
Anonymous nodes do not have a name and cannot be refered to again:
[ ] -> [ Bonn ] -> [ ]
This creates three nodes, two of them anonymous.
=item edges
The edges between the nodes can have the following styles:
-> solid
=> double
.> dotted
~> wave
- > dashed
.-> dot-dash
..-> dot-dot-dash
= > double-dash
There are also the styles C<bold>, C<wide> and C<broad>. Unlike the others,
these can only be set via the (optional) edge attributes:
[ AB ] --> { style: bold; } [ ABC ]
You can repeat each of the style-patterns as much as you like:
--->
==>
=>
~~~~~>
..-..-..->
Note that in patterns longer than one character, the entire
pattern must be repeated e.g. all characters of the pattern must be
present. Thus:
..-..-..-> # valid dot-dot-dash
..-..-..> # invalid!
.-.-.-> # valid dot-dash
.-.-> # invalid!
In additon to the styles, the following two directions are possible:
-- edge without arrow heads
--> arrow at target node (end point)
<--> arrow on both the source and target node
(end and start point)
Of course you can combine all directions with all styles. However,
note that edges without arrows cannot use the shortcuts for styles:
--- # valid
.-.- # valid
.- # invalid!
- # invalid!
~ # invalid!
Just remember to use at least two repititions of the full pattern
for arrow-less edges.
You can also give edges a label, either by inlining it into the style,
or by setting it via the attributes:
[ AB ] --> { style: bold; label: foo; } [ ABC ]
-- foo -->
... baz ...>
-- solid -->
== double ==>
.. dotted ..>
~~ wave ~~>
- dashed - >
= double-dash = >
.- dot-dash .->
..- dot-dot-dash ..->
Note that the two patterns on the left and right of the label must be
the same, and that there is a space between the left pattern and the
label, as well as the label and the right pattern.
You may use inline label only with edges that have an arrow. Thus:
<-- label --> # valid
-- label --> # valid
-- label -- # invalid!
To use a label with an edge without arrow heads, use the attributes:
[ AB ] -- { label: edgelabel; } [ CD ]
=item groups
Round brackets are used to group nodes together:
( Cities:
[ Bonn ] -> [ Berlin ]
)
Anonymous groups do not have a name and cannot be refered to again:
( [ Bonn ] ) -> [ Berlin ]
This creates an anonymous group with the node C<Bonn> in it, and
links it to the node C<Berlin>.
=back
Please see L<Graph::Easy::Manual> for a full description of the syntax rules.
=head2 Output
The output will be a L<Graph::Easy|Graph::Easy> object (unless overrriden
with C<use_class()>), see the documentation for Graph::Easy what you can do
with it.
=head1 EXAMPLES
See L<Graph::Easy> for an extensive list of examples.
=head1 METHODS
C<Graph::Easy::Parser> supports the following methods:
=head2 new()
use Graph::Easy::Parser;
my $parser = Graph::Easy::Parser->new();
Creates a new parser object. The valid parameters are:
debug
fatal_errors
The first will enable debug output to STDERR:
my $parser = Graph::Easy::Parser->new( debug => 1 );
$parser->from_text('[A] -> [ B ]');
Setting C<fatal_errors> to 0 will make parsing errors not die, but
just set an error string, which can be retrieved with L<error()>.
my $parser = Graph::Easy::Parser->new( fatal_errors => 0 );
$parser->from_text(' foo ' );
print $parser->error();
See also L<catch_messages()> for how to catch errors and warnings.
=head2 reset()
$parser->reset();
Reset the status of the parser, clear errors etc. Automatically called
when you call any of the C<from_XXX()> methods below.
=head2 use_class()
$parser->use_class('node', 'Graph::Easy::MyNode');
Override the class to be used to constructs objects while parsing. The
first parameter can be one of the following:
node
edge
graph
group
The second parameter should be a class that is a subclass of the
appropriate base class:
package Graph::Easy::MyNode;
use base qw/Graph::Easy::Node/;
# override here methods for your node class
######################################################
# when overriding nodes, we also need ::Anon
package Graph::Easy::MyNode::Anon;
use base qw/Graph::Easy::MyNode/;
use base qw/Graph::Easy::Node::Anon/;
######################################################
# and :::Empty
package Graph::Easy::MyNode::Empty;
use base qw/Graph::Easy::MyNode/;
######################################################
package main;
use Graph::Easy::Parser;
use Graph::Easy;
use Graph::Easy::MyNode;
use Graph::Easy::MyNode::Anon;
use Graph::Easy::MyNode::Empty;
my $parser = Graph::Easy::Parser;
$parser->use_class('node', 'Graph::Easy::MyNode');
my $graph = $parser->from_text(...);
The object C<$graph> will now contain nodes that are of your
custom class instead of plain C<Graph::Easy::Node>.
When overriding nodes, you also should provide subclasses
for C<Graph::Easy::Node::Anon> and C<Graph::Easy::Node::Empty>,
and make these subclasses of your custom node class as shown
above. For edges, groups and graphs, you need just one subclass.
=head2 from_text()
my $graph = $parser->from_text( $text );
Create a L<Graph::Easy|Graph::Easy> object from the textual description in C<$text>.
Returns undef for error, you can find out what the error was
with L<error()>.
This method will reset any previous error, and thus the C<$parser> object
can be re-used to parse different texts by just calling C<from_text()>
multiple times.
=head2 from_file()
my $graph = $parser->from_file( $filename );
my $graph = Graph::Easy::Parser->from_file( $filename );
Creates a L<Graph::Easy|Graph::Easy> object from the textual description in the file
C<$filename>.
The second calling style will create a temporary C<Graph::Easy::Parser> object,
parse the file and return the resulting C<Graph::Easy> object.
Returns undef for error, you can find out what the error was
with L<error()> when using the first calling style.
=head2 error()
my $error = $parser->error();
Returns the last error, or the empty string if no error occured.
If you want to catch warnings from the parser, enable catching
of warnings or errors:
$parser->catch_messages(1);
# Or individually:
# $parser->catch_warnings(1);
# $parser->catch_errors(1);
# something which warns or throws an error:
...
if ($parser->error())
{
my @errors = $parser->errors();
}
if ($parser->warning())
{
my @warnings = $parser->warnings();
}
See L<Graph::Easy::Base> for more details on error/warning message capture.
=head2 parse_error()
$parser->parse_error( $msg_nr, @params);
Sets an error message from a message number and replaces embedded
templates like C<##param1##> with the passed parameters.
=head2 _parse_attributes()
my $attributes = $parser->_parse_attributes( $txt, $class );
my ($att, $multiples) = $parser->_parse_attributes( $txt, $class );
B<Internal usage only>. Takes a text like this:
attribute: value; attribute2 : value2;
and returns a hash with the attributes.
In list context, also returns the max count of multiple attributes, e.g.
3 when it encounters something like C<< red|green|blue >>. When
=head1 EXPORT
Exports nothing.
=head1 SEE ALSO
L<Graph::Easy>. L<Graph::Easy::Parser::Graphviz> and L<Graph::Easy::Parser::VCG>.
=head1 AUTHOR
Copyright (C) 2004 - 2007 by Tels L<http://bloodgate.com>
See the LICENSE file for information.
=cut