File Coverage

lib/Graph/Flowchart.pm
Criterion Covered Total %
statement 82 261 31.4
branch 17 120 14.1
condition 5 36 13.8
subroutine 16 38 42.1
pod 34 34 100.0
total 154 489 31.4


line stmt bran cond sub pod time code
1             #############################################################################
2             # Generate flowcharts as a Graph::Easy object
3             #
4              
5             package Graph::Flowchart;
6              
7             $VERSION = '0.11';
8              
9 1     1   25998 use strict;
  1         2  
  1         42  
10              
11 1     1   1526 use Graph::Easy;
  1         155453  
  1         52  
12 1         3802 use Graph::Flowchart::Node qw/
13             N_IF N_THEN N_ELSE
14             N_END N_START N_BLOCK N_JOINT
15             N_FOR N_CONTINUE N_GOTO
16 1     1   460 /;
  1         3  
17              
18             #############################################################################
19             #############################################################################
20              
21             sub new
22             {
23 1     1 1 1280 my $class = shift;
24              
25 1         4 my $self = bless {}, $class;
26              
27 1 50       2 my $args = $_[0]; $args = { @_ } if ref($args) ne 'HASH';
  1         6  
28              
29 1         4 $self->_init($args);
30             }
31              
32             sub _init
33             {
34 1     1   3 my ($self, $args) = @_;
35              
36 1         10 $self->{graph} = Graph::Easy->new();
37              
38             # make the chart flow down
39 1         128 my $g = $self->{graph};
40 1         8 $g->set_attribute('flow', 'down');
41              
42             # set class defaults
43 1         157 $g->set_attribute('node.joint', 'shape', 'point');
44 1         383 $g->set_attribute('node.start', 'border-style', 'bold');
45 1         263 $g->set_attribute('node.end', 'border-style', 'bold');
46 1         93 for my $s (qw/block if for/)
47             {
48 3         188 $g->set_attribute("node.$s", 'border-style', 'solid');
49             }
50             # $g->set_attribute('edge.true', 'flow', 'front');
51             # $g->set_attribute('edge.false', 'flow', 'front');
52            
53             # add the start node
54 1         97 $self->{_last} = $self->new_block ('start', N_START() );
55              
56 1         8 $g->add_node($self->{_last});
57             # $g->debug(1);
58              
59 1         28 $self->{_first} = $self->{_last};
60 1         3 $self->{_cur} = $self->{_last};
61            
62 1         4 $self->{_group} = undef;
63              
64 1         6 $self;
65             }
66              
67             sub as_graph
68             {
69             # return the internal Graph::Easy object
70 1     1 1 6 my $self = shift;
71              
72 1         3 $self->{graph};
73             }
74              
75             sub as_ascii
76             {
77 0     0 1 0 my $self = shift;
78              
79 0         0 $self->{graph}->as_ascii();
80             }
81              
82             sub as_html_file
83             {
84 0     0 1 0 my $self = shift;
85              
86 0         0 $self->{graph}->as_html_file();
87             }
88              
89             sub as_boxart
90             {
91 0     0 1 0 my $self = shift;
92              
93 0         0 $self->{graph}->as_boxart();
94             }
95              
96             #############################################################################
97              
98             sub last_block
99             {
100             # get/set the last block
101 2     2 1 5 my $self = shift;
102              
103 2 50 33     11 $self->{_last} = $_[0] if ref($_[0]) && $_[0]->isa('Graph::Flowchart::Node');
104              
105 2         11 $self->{_last};
106             }
107              
108             sub current_block
109             {
110             # get/set the current insertion point
111 4     4 1 13 my $self = shift;
112              
113 4 100 66     23 $self->{_cur} = $_[0] if ref($_[0]) && $_[0]->isa('Graph::Flowchart::Node');
114              
115 4         17 $self->{_cur};
116             }
117              
118             sub current
119             {
120             # get/set the current insertion point
121 0     0 1 0 my $self = shift;
122              
123 0 0 0     0 $self->{_cur} = $_[0] if ref($_[0]) && $_[0]->isa('Graph::Flowchart::Node');
124              
125 0         0 $self->{_cur};
126             }
127              
128             sub first_block
129             {
130             # get/set the first block
131 2     2 1 8 my $self = shift;
132              
133 2 50 33     8 $self->{_first} = $_[0] if ref($_[0]) && $_[0]->isa('Graph::Flowchart::Node');
134              
135 2         7 $self->{_first};
136             }
137              
138             sub make_current
139             {
140             # set the current insertion point, and convert it to a joint
141 0     0 1 0 my $self = shift;
142              
143 0 0 0     0 $self->{_cur} = $_[0] if ref($_[0]) && $_[0]->isa('Graph::Flowchart::Node');
144              
145 0         0 $self->{_cur}->{_type} = N_JOINT();
146              
147 0         0 $self->{_cur};
148             }
149              
150             #############################################################################
151              
152             sub add_group
153             {
154             # add a group, and set it as current.
155 0     0 1 0 my ($self, $name) =@_;
156              
157 0         0 my $g = $self->{graph};
158              
159 0         0 $self->{_group} = $g->add_group($name);
160             }
161            
162             sub no_group
163             {
164             # we are now outside the group, so forget it
165 0     0 1 0 my $self = shift;
166              
167 0         0 $self->{_group} = undef;
168             }
169              
170             #############################################################################
171              
172             sub new_block
173             {
174 5     5 1 13 my ($self, $text, $type, $label) = @_;
175              
176 5         34 Graph::Flowchart::Node->new( $text, $type, $label, $self->{_group} );
177             }
178              
179             #############################################################################
180              
181             sub merge_blocks
182             {
183             # if possible, merge the given two blocks
184 2     2 1 6 my ($self, $first, $second) = @_;
185              
186             # see if we should merge the blocks
187              
188 2 50 33     21 return $second
189             if ( ($first->{_type} != N_JOINT()) &&
190             ($first->{_type} != $second->{_type} ) );
191              
192 0         0 my $label = $first->label();
193 0 0       0 $label .= '\n' unless $label eq '';
194 0         0 $label .= $second->label();
195              
196             # print STDERR "# merge $first->{name} ", $first->label(), " $second->{name} ", $second->label(),"\n";
197              
198 0 0       0 $first->sub_class($second->sub_class()) if $first->{_type} == N_JOINT;
199              
200             # quote chars
201 0         0 $label =~ s/([^\\])\|/$1\\\|/g; # '|' to '\|' ("|" marks an attribute split)
202 0         0 $label =~ s/([^\\])\|/$1\\\|/g; # do it twice for "||"
203              
204 0         0 $first->set_attribute('label', $label);
205              
206 0         0 $first->{_type} = $second->{_type};
207              
208             # drop second node from graph
209 0         0 my $g = $self->{graph};
210 0         0 $g->merge_nodes($first, $second);
211              
212 0         0 $self->{_cur} = $first;
213             }
214              
215             #############################################################################
216              
217             sub connect
218             {
219 2     2 1 15 my ($self, $from, $to, $edge_label, $edge_class) = @_;
220              
221 2         5 my $g = $self->{graph};
222 2         7 my $edge = $g->add_edge($from, $to);
223              
224 2 50       158 $edge->set_attribute('label', $edge_label) if defined $edge_label;
225 2 50       234 $edge->sub_class($edge_class) if defined $edge_class;
226              
227 2         26 $edge;
228             }
229              
230             sub insert_block
231             {
232             # Insert a block to the current (or $where) block. Any outgoing connections
233             # from $where are moved to the new block (unless they are merged).
234 1     1 1 3 my ($self, $block, $where) = @_;
235              
236             # XXX TODO: if $where is a N_BLOCK() and $block a scalar, then
237             # simple append $block to $where->label() and spare us the
238             # creation of a new block, and then merging it into $where.
239              
240 1 50       4 $block = $self->new_block($block, N_BLOCK() ) unless ref $block;
241              
242 1 50       3 $where = $self->{_cur} unless defined $where;
243 1         4 my $g = $self->{graph};
244 1         6 $g->add_edge($where, $block);
245              
246 1         101 my $old = $block;
247 1         22 $block = $self->merge_blocks($where, $block);
248              
249 1 50       6 if ($block != $old)
250             {
251             # where not merged, so move outgoing connections from $where to $block
252              
253 0         0 for my $e (values %{$where->{edges}})
  0         0  
254             {
255             # move the edge, unless is an incoming edge or a selfloop
256 0 0 0     0 $e->start_at($block) if $e->{from} == $where && $e->{to} != $where;
257             }
258             }
259            
260 1         3 $self->{_cur} = $block; # set new _cur and return it
261             }
262              
263             sub add_block
264             {
265             # Add a block to the current (or $where) block. Any outgoing connections
266             # are left where they are, e.g. starting at $where.
267              
268 1     1 1 2807 my ($self, $block, $where, $edge_label) = @_;
269              
270             # XXX TODO: if $where is a N_BLOCK() and $block a scalar, then
271             # simple append $block to $where->label() and spare us the
272             # creation of a new block, and then merging it into $where.
273              
274 1 50       13 $block = $self->new_block($block, N_BLOCK() ) unless ref $block;
275              
276 1 50       27 $where = $self->{_cur} unless defined $where;
277 1         4 my $g = $self->{graph};
278 1         8 $g->add_edge($where, $block, $edge_label);
279              
280 1         143 $block = $self->merge_blocks($where, $block);
281              
282 1         6 $self->{_cur} = $block; # set new _cur and return it
283             }
284            
285             sub add_new_block
286             {
287             # shortcut for "add_block(new_block(...))"
288 0     0 1 0 my ($self, $text, $type, $label, $where, $edge_label) = @_;
289              
290 0         0 my $block = $self->new_block($text, $type, $label);
291              
292 0         0 $self->add_block($block,$where);
293             }
294              
295             sub insert_new_block
296             {
297             # shortcut for "insert_block(new_block(...))"
298 0     0 1 0 my ($self, $text, $type, $label, $where) = @_;
299              
300 0         0 my $block = $self->new_block($text, $type, $label);
301              
302 0         0 $self->insert_block($block,$where);
303             }
304              
305             sub add_new_joint
306             {
307             # shortcut for "add_block(new_block(.., N_JOINT()))"
308 0     0 1 0 my ($self, $where) = @_;
309              
310 0         0 my $block = $self->new_block('', N_JOINT());
311 0         0 $self->add_block($block,$where);
312             }
313              
314             sub insert_new_joint
315             {
316             # shortcut for "insert_block(new_block(.., N_JOINT()))"
317 0     0 1 0 my ($self, $where) = @_;
318              
319 0         0 my $block = $self->new_block('', N_JOINT());
320 0         0 $self->insert_block($block,$where);
321             }
322              
323             sub add_joint
324             {
325 1     1 1 2 my $self = shift;
326              
327 1         3 my $g = $self->{graph};
328              
329 1         4 my $joint = $self->new_block('', N_JOINT());
330 1         7 $g->add_node($joint);
331              
332             # connect the requested connection points to the joint
333 1         35 for my $node ( @_ )
334             {
335 1         5 $g->add_edge($node, $joint);
336             }
337              
338 1         76 $joint;
339             }
340              
341             sub find_target
342             {
343 0     0 1 0 my ($self, $label) = @_;
344              
345 0         0 my $g = $self->{graph};
346              
347 0         0 for my $n (values %{$g->{nodes}})
  0         0  
348             {
349 0 0 0     0 return $n if defined $n->{_label} && $n->{_label} eq $label; # found
350             }
351 0         0 undef; # not found
352             }
353              
354             sub collapse_joints
355             {
356             # find any left-over joints and remove them
357 0     0 1 0 my ($self) = @_;
358              
359 0         0 my $g = $self->{graph};
360              
361 0         0 my @joints;
362 0         0 for my $n (values %{$g->{nodes}})
  0         0  
363             {
364 0 0       0 push @joints, $n if $n->{_type} == N_JOINT();
365             }
366              
367 0         0 for my $j (@joints)
368             {
369             # a joint should have only one successor
370 0         0 my @out = $j->outgoing();
371 0 0       0 next if @out != 1;
372              
373 0         0 my $o = $out[0]->{to};
374              
375             # get the label from the outgoing edge, if any
376 0         0 my $label = $out[0]->label();
377              
378             # "next" to ", next"
379 0 0       0 $label = ', ' . $label if $label ne '';
380              
381             # get all incoming edges
382 0         0 my @in = $j->incoming();
383              
384             # now for each incoming edge, add one bypass
385 0         0 for my $e (@in)
386             {
387 0         0 my $from = $e->{from};
388 0         0 my $l = $e->label() . $label;
389              
390 0         0 $g->add_edge($e->{from}, $o, $l);
391             }
392            
393             # finally get rid of the joint (including all edges)
394 0         0 $g->del_node($j);
395             }
396              
397 0         0 $self;
398             }
399              
400             #############################################################################
401              
402             sub start_node
403             {
404             # return the START node
405 0     0 1 0 my $self = shift;
406              
407 0         0 $self->{_first};
408             }
409              
410             sub end_node
411             {
412             # return the END node (or, before finish is called, the current last node)
413 0     0 1 0 my $self = shift;
414              
415 0         0 $self->{_last};
416             }
417              
418             sub finish
419             {
420 0     0 1 0 my ($self, $where) = @_;
421              
422 0         0 my $end = $self->new_block ( 'end', N_END() );
423 0         0 $end = $self->add_block ($end, $where);
424 0         0 $self->{_last} = $end;
425              
426 0         0 $self->collapse_joints();
427              
428             # If there is only one connection from START, and it goes to END, delete
429             # both blocks. This makes things like "sub foo { $a++; }" look better.
430 0         0 my $start = $self->{_first};
431              
432 0         0 my $g = $self->{graph};
433              
434             # if we only have two node, then we parsed something like '' and let it be:
435 0 0       0 if ($g->nodes() > 2)
436             {
437             # XXX TODO: use ->edges() and Graph::Easy 0.50
438 0         0 my @edges = values %{$start->{edges}};
  0         0  
439 0 0 0     0 if (@edges == 1 && $edges[0]->to() == $end)
440             {
441 0         0 $g->del_node($start);
442 0         0 $g->del_node($end);
443             }
444             }
445              
446 0         0 $self;
447             }
448              
449             #############################################################################
450             #############################################################################
451             # convience methods, for constructs like if, for etc
452              
453             sub add_jump
454             {
455 0     0 1 0 my ($self, $text, $type, $label, $target, $where) = @_;
456              
457             # find target if it was not specified as block
458 0 0       0 $target = $self->find_target($target) unless ref($target);
459              
460 0 0       0 if (!defined $target)
461             {
462 0         0 $target = $self->new_block ('', N_JOINT(), $target);
463 0         0 $self->{graph}->add_node($target);
464             }
465              
466 0         0 my $jump = $self->insert_new_block($text, $type);
467              
468 0 0       0 my $l = $target->{_label}; $l = '' unless defined $l;
  0         0  
469 0 0       0 $l = ' '.$l if $l ne '';
470              
471             # connect to the target block
472 0         0 my $edge = $self->connect($jump, $target, "$type$l", $type);
473 0         0 $self->{_cur} = $target;
474              
475 0 0       0 return ($jump,$target) if wantarray;
476              
477 0         0 $jump;
478             }
479              
480             sub add_if_then
481             {
482 1     1 1 4 my ($self, $if, $then, $where) = @_;
483            
484 1 50       9 $if = $self->new_block($if, N_IF()) unless ref $if;
485 1 50       6 $then = $self->new_block($then, N_THEN()) unless ref $then;
486              
487 1 50       5 $where = $self->{_cur} unless defined $where;
488              
489 1         5 $if = $self->insert_block ($if, $where);
490              
491 1         5 $self->connect($if, $then, 'true', 'true');
492              
493             # then --> '*'
494 1         5 $self->{_cur} = $self->add_joint($then);
495              
496             # if -- false --> '*'
497 1         5 $self->connect($if, $self->{_cur}, 'false', 'false');
498              
499 1 50       8 return ($if, $then, $self->{_cur}) if wantarray;
500              
501 0           $self->{_cur};
502             }
503              
504             sub add_if_then_else
505             {
506 0     0 1   my ($self, $if, $then, $else, $where) = @_;
507              
508 0 0         return $self->add_if_then($if,$then,$where) unless defined $else;
509            
510 0 0         $if = $self->new_block($if, N_IF()) unless ref $if;
511 0 0         $then = $self->new_block($then, N_THEN()) unless ref $then;
512 0 0         $else = $self->new_block($else, N_ELSE()) unless ref $else;
513              
514 0 0         $where = $self->{_cur} unless defined $where;
515              
516 0           $if = $self->insert_block ($if, $where);
517            
518 0           $self->connect($if, $then, 'true', 'true');
519 0           $self->connect($if, $else, 'false', 'false');
520              
521             # then --> '*', else --> '*'
522 0           $self->{_cur} = $self->add_joint($then, $else);
523              
524 0 0         return ($if, $then, $else, $self->{_cur}) if wantarray;
525 0           $self->{_cur};
526             }
527              
528             #############################################################################
529             # for loop
530              
531             sub add_for
532             {
533             # add a for (my $i = 0; $i < 12; $i++) style loop
534 0     0 1   my ($self, $init, $while, $cont, $body, $where) = @_;
535            
536 0 0         $init = $self->new_block($init, N_FOR()) unless ref $init;
537 0 0         $while = $self->new_block($while, N_IF()) unless ref $while;
538 0 0         $cont = $self->new_block($cont, N_CONTINUE()) unless ref $cont;
539 0 0         $body = $self->new_block($body, N_BLOCK()) unless ref $body;
540              
541             # init -> if $while --> body --> cont --> (back to if)
542              
543 0 0         $where = $self->{_cur} unless defined $where;
544              
545 0           $init = $self->add_block ($init, $where);
546 0           $while = $self->add_block ($while, $init);
547            
548             # Make the for-head node a bigger because it has two edges leaving it, and
549             # one coming back and we want two of them on one side for easier layouts:
550 0           $while->set_attribute('rows',2);
551              
552 0           $self->connect($while, $body, 'true', 'true');
553              
554 0           $self->connect($body, $cont);
555 0           $self->connect($cont, $while);
556              
557 0           my $joint = $self->add_joint();
558 0           $self->connect($while, $joint, 'false', 'false');
559              
560 0           $self->{_cur} = $joint;
561              
562 0           ($joint, $body, $cont);
563             }
564              
565             sub add_foreach
566             {
567             # add a for (@list) style loop
568 0     0 1   my ($self, $list, $body, $cont, $where) = @_;
569            
570 0 0         $list = $self->new_block($list, N_FOR()) unless ref $list;
571 0 0         $body = $self->new_block($body, N_BLOCK()) unless ref $body;
572 0 0 0       $cont = $self->new_block($cont, N_CONTINUE()) if defined $cont && !ref $cont;
573              
574             # list --> body --> cont --> (back to list)
575              
576 0 0         $where = $self->{_cur} unless defined $where;
577              
578 0           $list = $self->add_block ($list, $where);
579              
580             # Make the for-head node a bigger because it has two edges leaving it, and
581             # one coming back and we want two of them on one side for easier layouts:
582 0           $list->set_attribute('rows',2);
583              
584 0           $self->connect($list, $body, 'true', 'true');
585              
586 0 0         if (defined $cont)
587             {
588 0           $self->connect($body, $cont);
589 0           $self->connect($cont, $list);
590             }
591             else
592             {
593 0           $self->connect($body, $list);
594             }
595              
596 0           my $joint = $self->add_joint();
597 0           $self->connect($list, $joint, 'false', 'false');
598              
599 0           $self->{_cur} = $joint;
600              
601 0           ($joint, $body, $cont);
602             }
603              
604             #############################################################################
605             # while loop
606              
607             sub add_while
608             {
609             # add a "while ($i < 12) { body } continue { cont }" style loop
610 0     0 1   my ($self, $while, $body, $cont, $where) = @_;
611            
612 0 0         $while = $self->new_block($while, N_IF()) unless ref $while;
613              
614             # no body?
615 0 0         $body = $self->new_block( '', N_JOINT()) if !defined $body;
616 0 0         $body = $self->new_block($body, N_BLOCK()) unless ref $body;
617              
618 0 0 0       $cont = $self->new_block($cont, N_CONTINUE()) if defined $cont && !ref $cont;
619              
620             # if $while --> body --> cont --> (back to if)
621              
622 0 0         $where = $self->{_cur} unless defined $where;
623              
624 0           $while = $self->add_block ($while, $where);
625            
626             # Make the head node a bigger because it has two edges leaving it, and
627             # one coming back and we want two of them on one side for easier layouts:
628 0           $while->set_attribute('rows',2);
629              
630 0           $self->connect($while, $body, 'true', 'true');
631              
632 0 0         if (defined $cont)
633             {
634 0           $cont = $self->add_block ($cont, $body);
635 0           $self->connect($cont, $while);
636             }
637             else
638             {
639 0           $self->connect($body, $while);
640             }
641              
642 0           my $joint = $self->add_joint();
643 0           $self->connect($while, $joint, 'false', 'false');
644              
645 0           $self->{_cur} = $joint;
646              
647 0           ($joint, $body, $cont);
648             }
649              
650             sub add_until
651             {
652             # add a "until ($i < 12) { body } continue { cont }" style loop
653 0     0 1   my ($self, $while, $body, $cont, $where) = @_;
654            
655 0 0         $while = $self->new_block($while, N_IF()) unless ref $while;
656              
657             # no body?
658 0 0         $body = $self->new_block( '', N_JOINT()) if !defined $body;
659 0 0         $body = $self->new_block($body, N_BLOCK()) unless ref $body;
660              
661 0 0 0       $cont = $self->new_block($cont, N_CONTINUE()) if defined $cont && !ref $cont;
662              
663             # if $while --> body --> cont --> (back to if)
664              
665 0 0         $where = $self->{_cur} unless defined $where;
666              
667 0           $while = $self->add_block ($while, $where);
668            
669             # Make the head node a bigger because it has two edges leaving it, and
670             # one coming back and we want two of them on one side for easier layouts:
671 0           $while->set_attribute('rows',2);
672              
673 0           $self->connect($while, $body, 'false', 'false');
674              
675 0 0         if (defined $cont)
676             {
677 0           $cont = $self->add_block ($cont, $body);
678 0           $self->connect($cont, $while);
679             }
680             else
681             {
682 0           $self->connect($body, $while);
683             }
684              
685 0           my $joint = $self->add_joint();
686 0           $self->connect($while, $joint, 'true', 'true');
687              
688 0           $self->{_cur} = $joint;
689              
690 0           ($joint, $body, $cont);
691             }
692              
693             1;
694             __END__