File Coverage

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


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.76';
8              
9             #############################################################################
10             #############################################################################
11              
12             package Graph::Easy;
13              
14 15     15   7485 use strict;
  15         16  
  15         425  
15 15     15   45 use warnings;
  15         13  
  15         7804  
16              
17             sub _as_txt
18             {
19 211     211   616 my ($self) = @_;
20              
21             # Convert the graph to a textual representation - does not need layout().
22 211         699 $self->_assign_ranks();
23              
24             # generate the class attributes first
25 211         236 my $txt = '';
26 211         295 my $att = $self->{att};
27 211         639 for my $class (sort keys %$att)
28             {
29              
30             my $out = $self->_remap_attributes(
31 544         1770 $class, $att->{$class}, {}, 'noquote', 'encode' );
32              
33 544         623 my $att = '';
34 544         1121 for my $atr (sort keys %$out)
35             {
36             # border is handled special below
37 302 100       431 next if $atr =~ /^border/;
38 287         594 $att .= " $atr: $out->{$atr};\n";
39             }
40              
41             # edges do not have a border
42 544 100       1072 if ($class !~ /^edge/)
43             {
44 437   50     1037 my $border = $self->border_attribute($class) || '';
45              
46             # 'solid 1px #000000' =~ /^solid/;
47             # 'solid 1px #000000' =~ /^solid 1px #000000/;
48 437 100       998 $border = '' if $self->default_attribute($class,'border') =~ /^$border/;
49              
50 437 100       953 $att .= " border: $border;\n" if $border ne '';
51             }
52              
53 544 100       1339 if ($att ne '')
54             {
55             # the following makes short, single definitions to fit on one line
56 170 100 66     805 if ($att !~ /\n.*\n/ && length($att) < 40)
57             {
58 109         267 $att =~ s/\n/ /; $att =~ s/^ / /;
  109         265  
59             }
60             else
61             {
62 61         117 $att = "\n$att";
63             }
64 170         570 $txt .= "$class {$att}\n";
65             }
66             }
67              
68 211 100       427 $txt .= "\n" if $txt ne ''; # insert newline
69              
70 211         543 my @nodes = $self->sorted_nodes('name','id');
71              
72 211         318 my $count = 0;
73             # output nodes with attributes first, sorted by their name
74 211         351 foreach my $n (@nodes)
75             {
76 860         1032 $n->{_p} = undef; # mark as not yet processed
77 860         1450 my $att = $n->attributes_as_txt();
78 860 100       1620 if ($att ne '')
79             {
80 250         302 $n->{_p} = 1; # mark as processed
81 250         243 $count++;
82 250         442 $txt .= $n->as_pure_txt() . $att . "\n";
83             }
84             }
85              
86 211 100       426 $txt .= "\n" if $count > 0; # insert a newline
87              
88             # output groups first, with their nodes
89 211         209 foreach my $gn (sort keys %{$self->{groups}})
  211         711  
90             {
91 42         64 my $group = $self->{groups}->{$gn};
92 42         107 $txt .= $group->as_txt(); # marks nodes as processed if nec.
93 42         57 $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         554 @nodes = $self->sorted_nodes('rank','name');
108 211         358 foreach my $n (@nodes)
109             {
110 860         1933 my @out = $n->sorted_successors();
111 860         1339 my $first = $n->as_pure_txt(); # [ A | B ]
112 860 100 100     2933 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     437 next if exists $n->{autosplit} && !defined $n->{autosplit};
116 75 100       170 $txt .= $first . "\n" unless defined $n->{_p};
117             }
118              
119 801         1327 $first = $n->_as_part_txt(); # [ A.0 ]
120             # for all outgoing connections
121 801         1123 foreach my $other (@out)
122             {
123             # in case there exists more than one edge from $n --> $other
124 576         1229 my @edges = $n->edges_to($other);
125 576         821 for my $edge (sort { $a->{id} <=> $b->{id} } @edges)
  43         64  
126             {
127 612         1375 $txt .= $first . $edge->as_txt() . $other->_as_part_txt() . "\n";
128             }
129             }
130             }
131              
132 211         262 foreach my $n (@nodes)
133             {
134 860         793 delete $n->{_p}; # clean up
135             }
136              
137 211         899 $txt;
138             }
139              
140             #############################################################################
141              
142             package Graph::Easy::Group;
143              
144 15     15   66 use strict;
  15         11  
  15         2659  
145              
146             sub as_txt
147             {
148 45     45 1 419 my $self = shift;
149              
150 45         49 my $n = '';
151 45 100       250 if (!$self->isa('Graph::Easy::Group::Anon'))
152             {
153 40         54 $n = $self->{name};
154             # quote special chars in name
155 40         99 $n =~ s/([\[\]\(\)\{\}\#])/\\$1/g;
156 40         80 $n = ' ' . $n;
157             }
158              
159 45         74 my $txt = "($n";
160              
161 45         54 $n = $self->{nodes};
162              
163 45 100       134 $txt .= (keys %$n > 0 ? "\n" : ' ');
164 45         122 for my $name ( sort keys %$n )
165             {
166 87         122 $n->{$name}->{_p} = 1; # mark as processed
167 87         165 $txt .= ' ' . $n->{$name}->as_pure_txt() . "\n";
168             }
169 45         113 $txt .= ")" . $self->attributes_as_txt() . "\n\n";
170              
171             # insert all the edges of the group
172              
173             #
174 45         75 $txt;
175             }
176              
177             #############################################################################
178              
179             package Graph::Easy::Node;
180              
181 15     15   62 use strict;
  15         123  
  15         14449  
182              
183             sub attributes_as_txt
184             {
185             # return the attributes of this node as text description
186 1553     1553 1 1890 my ($self, $remap) = @_;
187              
188             # nodes that were autosplit
189 1553 100       2202 if (exists $self->{autosplit})
190             {
191             # other nodes are invisible in as_txt:
192 128 100       242 return '' unless defined $self->{autosplit};
193             # the first one might have had a label set
194             }
195              
196 1470         1197 my $att = '';
197 1470         2712 my $class = $self->class();
198 1470         1402 my $g = $self->{graph};
199              
200             # XXX TODO: remove atttributes that are simple the default attributes
201              
202 1470         1333 my $attributes = $self->{att};
203 1470 100       2064 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         61 my $basename = $self->{autosplit_basename};
208 45         51 $attributes = { };
209              
210 45         87 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         48 my $names = {};
214 45         566 for my $child ($self, @$parts)
215             {
216 128         112 for my $k (sort keys %{$child->{att}})
  128         268  
217             {
218 40         67 $names->{$k} = undef;
219             }
220             }
221              
222 45         81 for my $k (sort keys %$names)
223             {
224 32 100       64 next if $k eq 'basename';
225 14         18 my $val = $self->{att}->{$k};
226 14 100       22 $val = '' unless defined $val;
227 14         13 my $first = $val; my $not_equal = 0;
  14         10  
228 14         13 $val .= '|';
229 14         17 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       21 my $v = $child->{att}->{$k}; $v = '' if !defined $v;
  21         29  
236 21 100       26 $not_equal ++ if $v ne $first;
237 21         27 $val .= $v . '|';
238             }
239             # all parts equal, so do "red|red|red" => "red"
240 14 100       25 $val = $first if $not_equal == 0;
241              
242 14         34 $val =~ s/\|+\z/\|/; # "silver|||" => "silver|"
243 14 100       39 $val =~ s/\|\z// if $val =~ /\|.*\|/; # "silver|" => "silver|"
244             # but "red|blue|" => "red|blue"
245 14 50       39 $attributes->{$k} = $val unless $val eq '|'; # skip '|'
246             }
247 45 100       174 $attributes->{basename} = $self->{att}->{basename} if defined $self->{att}->{basename};
248             }
249              
250 1470         3201 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         1435 delete $new->{group};
255              
256             # for groups inside groups, insert their group attribute
257             $new->{group} = $self->{group}->{name}
258 1470 100 66     5955 if $self->isa('Graph::Easy::Group') && exists $self->{group};
259              
260 1470 100       2256 if (defined $self->{origin})
261             {
262 69         112 $new->{origin} = $self->{origin}->{name};
263 69         201 $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       1878 if (exists $new->{columns})
269             {
270 18   50     80 $new->{size} = ($new->{columns}||1) . ',' . ($new->{rows}||1);
      100        
271 18         24 delete $new->{rows};
272 18         18 delete $new->{columns};
273             # don't output the default size
274 18 100       44 delete $new->{size} if $new->{size} eq '1,1';
275             }
276              
277 1470         2667 for my $atr (sort keys %$new)
278             {
279 542 100       905 next if $atr =~ /^border/; # handled special
280              
281 491         1131 $att .= "$atr: $new->{$atr}; ";
282             }
283              
284 1470 100       3221 if (!$self->isa_cell())
285             {
286 848         612 my $border;
287 848 100       1108 if (!exists $self->{autosplit})
288             {
289 803         1574 $border = $self->border_attribute();
290             }
291             else
292             {
293             $border = Graph::Easy::_border_attribute(
294             $attributes->{borderstyle}||'',
295             $attributes->{borderwidth}||'',
296 45   100     344 $attributes->{bordercolor}||'');
      50        
      50        
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     2352 $border = '' if ref $g && $g->attribute($class,'border') eq $border;
304 848 100       1517 $att .= "border: $border; " if $border ne '';
305             }
306              
307             # if we have a subclass, we probably need to include it
308 1470         1289 my $c = '';
309 1470 100       2756 $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     2999 $c = '' if ref($self->{group}) && $self->{group}->attribute('nodeclass') eq $c;
313              
314             # include our subclass as attribute
315 1470 100 100     2599 $att .= "class: $c; " if $c ne '' && $c ne 'anon';
316              
317             # generate attribute text if nec.
318 1470 100       2363 $att = ' { ' . $att . '}' if $att ne '';
319              
320 1470         3169 $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   1095 my $self = shift;
328              
329 1403         1391 my $name = $self->{name};
330              
331             # quote special chars in name
332 1403         1621 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
333              
334 1403         3222 '[ ' . $name . ' ]';
335             }
336              
337             sub as_pure_txt
338             {
339 1194     1194 1 1025 my $self = shift;
340              
341 1194 100 100     2661 if (exists $self->{autosplit} && defined $self->{autosplit})
342             {
343 74         102 my $name = $self->{autosplit};
344              
345             # quote special chars in name (but not |)
346 74         124 $name =~ s/([\[\]\{\}\#])/\\$1/g;
347              
348 74         200 return '[ '. $name .' ]'
349             }
350              
351 1120         1202 my $name = $self->{name};
352              
353             # quote special chars in name
354 1120         1542 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
355              
356 1120         2423 '[ ' . $name . ' ]';
357             }
358              
359             sub as_txt
360             {
361 23     23 1 595 my $self = shift;
362              
363 23 50       43 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         33 my $name = $self->{name};
373              
374             # quote special chars in name
375 23         122 $name =~ s/([\[\]\|\{\}\#])/\\$1/g;
376              
377 23         42 '[ ' . $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   520 my $self = shift;
398              
399             # '- Name ' or ''
400 622 100       789 my $n = $self->{att}->{label}; $n = '' unless defined $n;
  622         1059  
401              
402 622 100       489 my $left = ' '; $left = ' <' if $self->{bidirectional};
  622         1066  
403 622 100       479 my $right = '> '; $right = ' ' if $self->{undirected};
  622         903  
404              
405 622   50     1096 my $s = $self->style() || 'solid';
406              
407 622         571 my $style = '--';
408              
409             # suppress border on edges
410 622         1386 my $suppress = { all => { label => undef } };
411 622 100       1499 if ($s =~ /^(bold|bold-dash|broad|wide|invisible)\z/)
412             {
413             # output "--> { style: XXX; }"
414 15         18 $style = '--';
415             }
416             else
417             {
418             # output "-->" or "..>" etc
419 607         724 $suppress->{all}->{style} = undef;
420              
421 607         703 $style = $styles->{ $s };
422 607 50       961 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       1138 $n = $style . " $n " if $n ne '';
430              
431             # make " - " into " - - "
432 622 50 66     1192 $style = $style . $style if $self->{undirected} && substr($style,1,1) eq ' ';
433              
434             # ' - Name -->' or ' --> ' or ' -- '
435 622         1003 my $a = $self->attributes_as_txt($suppress) . ' '; $a =~ s/^\s//;
  622         1725  
436 622         2273 $left . $n . $style . $right . $a;
437             }
438              
439             1;
440             __END__