File Coverage

lib/Graph/Easy/As_txt.pm
Criterion Covered Total %
statement 175 181 96.6
branch 90 96 93.7
condition 37 46 80.4
subroutine 11 11 100.0
pod 4 4 100.0
total 317 338 93.7


line stmt bran cond sub pod time code
1             #############################################################################
2             # Output an Graph::Easy object as textual description
3             #
4              
5             package Graph::Easy::As_txt;
6              
7             $VERSION = '0.75';
8              
9             #############################################################################
10             #############################################################################
11              
12             package Graph::Easy;
13              
14 16     16   21630 use strict;
  16         40  
  16         781  
15 16     16   97 use warnings;
  16         35  
  16         15200  
16              
17             sub _as_txt
18             {
19 211     211   716 my ($self) = @_;
20              
21             # Convert the graph to a textual representation - does not need layout().
22 211         1936 $self->_assign_ranks();
23              
24             # generate the class attributes first
25 211         716 my $txt = '';
26 211         604 my $att = $self->{att};
27 211         1330 for my $class (sort keys %$att)
28             {
29              
30 544         3287 my $out = $self->_remap_attributes(
31             $class, $att->{$class}, {}, 'noquote', 'encode' );
32              
33 544         1467 my $att = '';
34 544         2266 for my $atr (sort keys %$out)
35             {
36             # border is handled special below
37 302 100       792 next if $atr =~ /^border/;
38 287         1319 $att .= " $atr: $out->{$atr};\n";
39             }
40              
41             # edges do not have a border
42 544 100       2731 if ($class !~ /^edge/)
43             {
44 437   50     1911 my $border = $self->border_attribute($class) || '';
45              
46             # 'solid 1px #000000' =~ /^solid/;
47             # 'solid 1px #000000' =~ /^solid 1px #000000/;
48 437 100       4626 $border = '' if $self->default_attribute($class,'border') =~ /^$border/;
49              
50 437 100       1853 $att .= " border: $border;\n" if $border ne '';
51             }
52              
53 544 100       2926 if ($att ne '')
54             {
55             # the following makes short, single definitions to fit on one line
56 170 100 66     1682 if ($att !~ /\n.*\n/ && length($att) < 40)
57             {
58 109         525 $att =~ s/\n/ /; $att =~ s/^ / /;
  109         571  
59             }
60             else
61             {
62 61         192 $att = "\n$att";
63             }
64 170         1185 $txt .= "$class {$att}\n";
65             }
66             }
67              
68 211 100       908 $txt .= "\n" if $txt ne ''; # insert newline
69              
70 211         1190 my @nodes = $self->sorted_nodes('name','id');
71              
72 211         839 my $count = 0;
73             # output nodes with attributes first, sorted by their name
74 211         717 foreach my $n (@nodes)
75             {
76 860         3057 $n->{_p} = undef; # mark as not yet processed
77 860         3110 my $att = $n->attributes_as_txt();
78 860 100       3365 if ($att ne '')
79             {
80 250         601 $n->{_p} = 1; # mark as processed
81 250         481 $count++;
82 250         1083 $txt .= $n->as_pure_txt() . $att . "\n";
83             }
84             }
85            
86 211 100       835 $txt .= "\n" if $count > 0; # insert a newline
87              
88             # output groups first, with their nodes
89 211         374 foreach my $gn (sort keys %{$self->{groups}})
  211         1038  
90             {
91 42         144 my $group = $self->{groups}->{$gn};
92 42         223 $txt .= $group->as_txt(); # marks nodes as processed if nec.
93 42         129 $count++;
94             }
95              
96             # XXX TODO:
97             # Output all nodes with rank=0 first, and also follow their successors
98             # What is left will then be done next, with rank=1 etc.
99             # This output order let's us output node chains in compact form as:
100             # [A]->[B]->[C]->[D]
101             # [B]->[E]
102             # instead of having:
103             # [A]->[B]
104             # [B]->[E]
105             # [B]->[C] etc
106            
107 211         1635 @nodes = $self->sorted_nodes('rank','name');
108 211         791 foreach my $n (@nodes)
109             {
110 860         4407 my @out = $n->sorted_successors();
111 860         2440 my $first = $n->as_pure_txt(); # [ A | B ]
112 860 100 100     12219 if ( defined $n->{autosplit} || ((@out == 0) && ( (scalar $n->predecessors() || 0) == 0)))
      100        
      66        
113             {
114             # single node without any connections (unless already output)
115 134 100 100     780 next if exists $n->{autosplit} && !defined $n->{autosplit};
116 75 100       287 $txt .= $first . "\n" unless defined $n->{_p};
117             }
118              
119 801         2656 $first = $n->_as_part_txt(); # [ A.0 ]
120             # for all outgoing connections
121 801         2405 foreach my $other (@out)
122             {
123             # in case there exists more than one edge from $n --> $other
124 576         2586 my @edges = $n->edges_to($other);
125 576         4964 for my $edge (sort { $a->{id} <=> $b->{id} } @edges)
  43         143  
126             {
127 612         3017 $txt .= $first . $edge->as_txt() . $other->_as_part_txt() . "\n";
128             }
129             }
130             }
131              
132 211         1724 foreach my $n (@nodes)
133             {
134 860         3246 delete $n->{_p}; # clean up
135             }
136              
137 211         2755 $txt;
138             }
139              
140             #############################################################################
141              
142             package Graph::Easy::Group;
143              
144 16     16   206 use strict;
  16         173  
  16         7300  
145              
146             sub as_txt
147             {
148 45     45 1 1164 my $self = shift;
149              
150 45         87 my $n = '';
151 45 100       593 if (!$self->isa('Graph::Easy::Group::Anon'))
152             {
153 40         118 $n = $self->{name};
154             # quote special chars in name
155 40         133 $n =~ s/([\[\]\(\)\{\}\#])/\\$1/g;
156 40         113 $n = ' ' . $n;
157             }
158              
159 45         333 my $txt = "($n";
160              
161 45         131 $n = $self->{nodes};
162              
163 45 100       219 $txt .= (keys %$n > 0 ? "\n" : ' ');
164 45         10113 for my $name ( sort keys %$n )
165             {
166 87         267 $n->{$name}->{_p} = 1; # mark as processed
167 87         342 $txt .= ' ' . $n->{$name}->as_pure_txt() . "\n";
168             }
169 45         280 $txt .= ")" . $self->attributes_as_txt() . "\n\n";
170              
171             # insert all the edges of the group
172              
173             #
174 45         241 $txt;
175             }
176              
177             #############################################################################
178              
179             package Graph::Easy::Node;
180              
181 16     16   112 use strict;
  16         237  
  16         32688  
182              
183             sub attributes_as_txt
184             {
185             # return the attributes of this node as text description
186 1553     1553 1 3457 my ($self, $remap) = @_;
187              
188             # nodes that were autosplit
189 1553 100       5280 if (exists $self->{autosplit})
190             {
191             # other nodes are invisible in as_txt:
192 128 100       475 return '' unless defined $self->{autosplit};
193             # the first one might have had a label set
194             }
195              
196 1470         2174 my $att = '';
197 1470         8074 my $class = $self->class();
198 1470         3243 my $g = $self->{graph};
199              
200             # XXX TODO: remove atttributes that are simple the default attributes
201              
202 1470         2653 my $attributes = $self->{att};
203 1470 100       6131 if (exists $self->{autosplit})
204             {
205             # for the first node in a row of autosplit nodes, we need to create
206             # the correct attributes, e.g. "silver|red|" instead of just silver:
207 45         132 my $basename = $self->{autosplit_basename};
208 45         85 $attributes = { };
209              
210 45         326 my $parts = $self->{autosplit_parts};
211             # gather all possible attribute names, otherwise an attribute set
212             # on only one part (like via "color: |red;" would not show up:
213 45         81 my $names = {};
214 45         1023 for my $child ($self, @$parts)
215             {
216 128         179 for my $k (sort keys %{$child->{att}})
  128         516  
217             {
218 40         144 $names->{$k} = undef;
219             }
220             }
221              
222 45         165 for my $k (sort keys %$names)
223             {
224 32 100       115 next if $k eq 'basename';
225 14         39 my $val = $self->{att}->{$k};
226 14 100       35 $val = '' unless defined $val;
227 14         20 my $first = $val; my $not_equal = 0;
  14         18  
228 14         28 $val .= '|';
229 14         28 for my $child (@$parts)
230             {
231             # only consider our own autosplit parts (check should not be nec.)
232             # next if !exists $child->{autosplit_basename} ||
233             # $child->{autosplit_basename} ne $basename;
234              
235 21 100       46 my $v = $child->{att}->{$k}; $v = '' if !defined $v;
  21         291  
236 21 100       51 $not_equal ++ if $v ne $first;
237 21         62 $val .= $v . '|';
238             }
239             # all parts equal, so do "red|red|red" => "red"
240 14 100       89 $val = $first if $not_equal == 0;
241              
242 14         71 $val =~ s/\|+\z/\|/; # "silver|||" => "silver|"
243 14 100       67 $val =~ s/\|\z// if $val =~ /\|.*\|/; # "silver|" => "silver|"
244             # but "red|blue|" => "red|blue"
245 14 50       66 $attributes->{$k} = $val unless $val eq '|'; # skip '|'
246             }
247 45 100       452 $attributes->{basename} = $self->{att}->{basename} if defined $self->{att}->{basename};
248             }
249              
250 1470         9345 my $new = $g->_remap_attributes( $self, $attributes, $remap, 'noquote', 'encode' );
251              
252             # For nodes, we do not output their group attribute, since they simple appear
253             # at the right place in the txt:
254 1470         3468 delete $new->{group};
255              
256             # for groups inside groups, insert their group attribute
257 1470 100 100     17172 $new->{group} = $self->{group}->{name}
258             if $self->isa('Graph::Easy::Group') && exists $self->{group};
259              
260 1470 100       5291 if (defined $self->{origin})
261             {
262 69         228 $new->{origin} = $self->{origin}->{name};
263 69         380 $new->{offset} = join(',', $self->offset());
264             }
265              
266             # shorten output for multi-celled nodes
267             # for "rows: 2;" still output "rows: 2;", because it is shorter
268 1470 100       4895 if (exists $new->{columns})
269             {
270 18   50     165 $new->{size} = ($new->{columns}||1) . ',' . ($new->{rows}||1);
      100        
271 18         233 delete $new->{rows};
272 18         39 delete $new->{columns};
273             # don't output the default size
274 18 100       62 delete $new->{size} if $new->{size} eq '1,1';
275             }
276              
277 1470         5368 for my $atr (sort keys %$new)
278             {
279 542 100       1976 next if $atr =~ /^border/; # handled special
280              
281 491         2399 $att .= "$atr: $new->{$atr}; ";
282             }
283              
284 1470 100       6943 if (!$self->isa_cell())
285             {
286 848         1445 my $border;
287 848 100       2124 if (!exists $self->{autosplit})
288             {
289 803         4562 $border = $self->border_attribute();
290             }
291             else
292             {
293 45   100     890 $border = Graph::Easy::_border_attribute(
      50        
      50        
294             $attributes->{borderstyle}||'',
295             $attributes->{borderwidth}||'',
296             $attributes->{bordercolor}||'');
297             }
298              
299             # XXX TODO: should do this for all attributes, not only for border
300             # XXX TODO: this seems wrong anyway
301              
302             # don't include default border
303 848 100 66     5263 $border = '' if ref $g && $g->attribute($class,'border') eq $border;
304 848 100       2599 $att .= "border: $border; " if $border ne '';
305             }
306              
307             # if we have a subclass, we probably need to include it
308 1470         2501 my $c = '';
309 1470 100       6105 $c = $1 if $class =~ /\.(\w+)/;
310              
311             # but we do not need to include it if our group has a nodeclass attribute
312 1470 100 100     6520 $c = '' if ref($self->{group}) && $self->{group}->attribute('nodeclass') eq $c;
313              
314             # include our subclass as attribute
315 1470 100 100     5316 $att .= "class: $c; " if $c ne '' && $c ne 'anon';
316              
317             # generate attribute text if nec.
318 1470 100       4804 $att = ' { ' . $att . '}' if $att ne '';
319              
320 1470         8047 $att;
321             }
322              
323             sub _as_part_txt
324             {
325             # for edges, we need the name of the part of the first part, not the entire
326             # autosplit text
327 1403     1403   2200 my $self = shift;
328              
329 1403         3126 my $name = $self->{name};
330              
331             # quote special chars in name
332 1403         12565 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
333              
334 1403         7640 '[ ' . $name . ' ]';
335             }
336              
337             sub as_pure_txt
338             {
339 1194     1194 1 1829 my $self = shift;
340              
341 1194 100 100     4959 if (exists $self->{autosplit} && defined $self->{autosplit})
342             {
343 74         148 my $name = $self->{autosplit};
344              
345             # quote special chars in name (but not |)
346 74         190 $name =~ s/([\[\]\{\}\#])/\\$1/g;
347            
348 74         347 return '[ '. $name .' ]'
349             }
350              
351 1120         2910 my $name = $self->{name};
352              
353             # quote special chars in name
354 1120         3367 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
355              
356 1120         4919 '[ ' . $name . ' ]';
357             }
358              
359             sub as_txt
360             {
361 23     23 1 919 my $self = shift;
362              
363 23 50       98 if (exists $self->{autosplit})
364             {
365 0 0       0 return '' unless defined $self->{autosplit};
366 0         0 my $name = $self->{autosplit};
367             # quote special chars in name (but not |)
368 0         0 $name =~ s/([\[\]\{\}\#])/\\$1/g;
369 0         0 return '[ ' . $name . ' ]'
370             }
371              
372 23         62 my $name = $self->{name};
373              
374             # quote special chars in name
375 23         230 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
376              
377 23         97 '[ ' . $name . ' ]' . $self->attributes_as_txt();
378             }
379              
380             #############################################################################
381              
382             package Graph::Easy::Edge;
383              
384             my $styles = {
385             solid => '--',
386             dotted => '..',
387             double => '==',
388             'double-dash' => '= ',
389             dashed => '- ',
390             'dot-dash' => '.-',
391             'dot-dot-dash' => '..-',
392             wave => '~~',
393             };
394              
395             sub _as_txt
396             {
397 622     622   929 my $self = shift;
398              
399             # '- Name ' or ''
400 622 100       1849 my $n = $self->{att}->{label}; $n = '' unless defined $n;
  622         1834  
401              
402 622 100       902 my $left = ' '; $left = ' <' if $self->{bidirectional};
  622         2086  
403 622 100       889 my $right = '> '; $right = ' ' if $self->{undirected};
  622         2188  
404            
405 622   50     2806 my $s = $self->style() || 'solid';
406              
407 622         985 my $style = '--';
408              
409             # suppress border on edges
410 622         3145 my $suppress = { all => { label => undef } };
411 622 100       3102 if ($s =~ /^(bold|bold-dash|broad|wide|invisible)\z/)
412             {
413             # output "--> { style: XXX; }"
414 15         33 $style = '--';
415             }
416             else
417             {
418             # output "-->" or "..>" etc
419 607         1318 $suppress->{all}->{style} = undef;
420              
421 607         1410 $style = $styles->{ $s };
422 607 50       1530 if (!defined $style)
423             {
424 0         0 require Carp;
425 0         0 Carp::confess ("Unknown edge style '$s'\n");
426             }
427             }
428            
429 622 100       1576 $n = $style . " $n " if $n ne '';
430              
431             # make " - " into " - - "
432 622 50 66     2334 $style = $style . $style if $self->{undirected} && substr($style,1,1) eq ' ';
433              
434             # ' - Name -->' or ' --> ' or ' -- '
435 622         2005 my $a = $self->attributes_as_txt($suppress) . ' '; $a =~ s/^\s//;
  622         2572  
436 622         4706 $left . $n . $style . $right . $a;
437             }
438              
439             1;
440             __END__