File Coverage

blib/lib/Mail/Thread/Chronological.pm
Criterion Covered Total %
statement 18 94 19.1
branch 0 24 0.0
condition 0 22 0.0
subroutine 6 12 50.0
pod 3 3 100.0
total 27 155 17.4


line stmt bran cond sub pod time code
1 1     1   898 use strict;
  1         2  
  1         56  
2             package Mail::Thread::Chronological;
3 1     1   918 use Mail::Thread ();
  1         41122  
  1         29  
4 1     1   958 use Date::Parse qw( str2time );
  1         8014  
  1         82  
5 1     1   9 use List::Util qw( max );
  1         1  
  1         94  
6 1     1   5 use vars qw/$VERSION/;
  1         2  
  1         43  
7             $VERSION = '1.22';
8              
9 1     1   4 use constant debug => 0;
  1         2  
  1         1177  
10              
11             =head1 NAME
12              
13             Mail::Thread::Chronological - rearrange Mail::Thread::Containers into a Chronological structure
14              
15             =head1 SYNOPSIS
16              
17             use Mail::Thread;
18             use Mail::Thread::Chronological;
19              
20             my $threader = Mail::Thread->new( @messages );
21             my $lurker = Mail::Thread::Chronological->new;
22              
23             $threader->thread;
24              
25             for my $thread ($threader->rootset) {
26             for my $row ( $lurker->arrange( $thread ) ) {
27             my $container = grep { ref $_ } @$row;
28             print join('', map { ref $_ ? '*' : $_ } @$row),
29             " ", $container->messageid, "\n";
30             }
31             }
32              
33             =head1 DESCRIPTION
34              
35             Given a Mail::Thread::Container, Mail::Thread::Chronological transforms the
36             tree structure into a 2-dimensional array representing the history of
37             a given thread in time.
38              
39             The output is similar to that of the Lurker mail archiving system,
40             with a couple of small exceptions:
41              
42             =over
43              
44             =item Characters used
45              
46             The grid is populated with the characters ' ' (space), '-', '+', '|',
47             '{', or Mail::Thread::Container objects. Lurker uses [a-g], and
48             differentiates T-junctions from corners for you, this module assumes
49             you will do that for yourself.
50              
51             The characters mean:
52              
53             =over
54              
55             =item space
56              
57             empty cell
58              
59             =item -
60              
61             horizontal line
62              
63             =item +
64              
65             T junction or corner
66              
67             =item |
68              
69             vertical line
70              
71             =item {
72              
73             vertical line crossing over a horizontal line
74              
75             =back
76              
77             =item Vertical stream crossing is permitted
78              
79             In the original lurker crossing a path vertically is not allowed, this
80             results in a lot of horizontal space being used.
81              
82             =back
83              
84             =head1 METHODS
85              
86             =head2 new
87              
88             your common or garden constructor
89              
90             =cut
91              
92 0     0 1   sub new { bless {}, $_[0] }
93              
94             =head2 arrange
95              
96             Returns an array of arrays representing the thread tree.
97              
98             =cut
99              
100              
101             # identify the co-ordinates of something
102             sub _cell {
103 0     0     my $cells = shift;
104 0           my $find = shift;
105 0           for (my $y = 0; $y < @$cells; ++$y) {
106 0           for (my $x = 0; $x < @{ $cells->[$y] }; ++$x) {
  0            
107 0           my $here = $cells->[$y][$x];
108 0 0 0       return [$y, $x] if ref $here && $here == $find;
109             }
110             }
111 0           return;
112             }
113              
114             sub _draw_cells {
115 0     0     my $cells = shift;
116             # and again in their new state
117 0           print map { $_ % 10 } 0..20;
  0            
118 0           print "\n";
119 0           for my $row (@$cells) {
120 0           my $this;
121 0           for (@$row) {
122 0 0         $this = $_ if ref $_;
123 0 0         print ref $_ ? '*' : $_ ? $_ : ' ';
    0          
124             }
125 0           print "\t", $this->messageid, "\n";
126             }
127 0           print "\n";
128             }
129              
130             sub arrange {
131 0     0 1   my $self = shift;
132 0           my $thread = shift;
133              
134             # show them in the old order, and take a copy of the containers
135             # with messages on while we're at it
136 0           my @messages;
137             $thread->iterate_down(
138             sub {
139 0     0     my ($c, $d) = @_;
140 0           print ' ' x $d, $c->messageid, "\n" if debug;
141 0 0         push @messages, $c if $c->message;
142 0           } );
143              
144             # cells is the 2-d representation, row, col. the first
145             # message will be at [0][0], it's first reply, [0][1]
146 0           my @cells;
147              
148             # okay, wander them in date order
149 0           @messages = sort { $self->extract_time( $a ) <=>
  0            
150             $self->extract_time( $b ) } @messages;
151 0           for (my $row = 0; $row < @messages; ++$row) {
152 0           my $c = $messages[$row];
153             # and place them in cells
154              
155             # the first one - [0][0]
156 0 0         unless (@cells) {
157 0           $cells[$row][0] = $c;
158 0           next;
159             }
160              
161             # look up our parent
162 0           my $first_parent = $c->parent;
163 0   0       while ($first_parent && !$first_parent->message) {
164 0           $first_parent = $first_parent->parent;
165             }
166              
167 0 0 0       unless ($first_parent && $first_parent->message &&
      0        
168             _cell(\@cells, $first_parent) ) {
169             # just drop it randomly to one side, since it doesn't
170             # have a clearly identifiable parent
171 0           my $col = (max map { scalar @$_ } @cells );
  0            
172 0           $cells[$row][$col] = $c;
173 0           next;
174             }
175 0           my $col;
176 0           my ($parent_row, $parent_col) = @{ _cell( \@cells, $first_parent ) };
  0            
177 0 0         if ($first_parent->child == $c) {
178             # if we're the first child, then we directly beneath
179             # them
180 0           $col = $parent_col;
181             }
182             else {
183             # otherwise, we have to shuffle accross into the first
184             # free column
185              
186             # okay, figure out what the max col is
187 0           $col = my $max_col = (max map { scalar @$_ } @cells );
  0            
188              
189             # would drawing the simple horizontal line cross the streams?
190 0 0 0       if (grep {
  0            
191             ($cells[$parent_row][$_] || '') eq '|'
192             } $parent_col+1..$max_col) {
193             # we must not cross the streams (that would be bad).
194             # if given this tree:
195             # a + +
196             # b | |
197             # c |
198             # d
199             #
200             # e arrives, and is a reply to b, we can't just do this:
201             # a + +
202             # b - - +
203             # c | |
204             # d |
205             # e
206             #
207             # it's messy and confusing. instead we have to do
208             # extra work so we end up at
209             # a - + +
210             # b + | |
211             # | c |
212             # | d
213             # e
214              
215 0           print "Crossing the streams, horizontally\n" if debug;
216             # we want to end up in $parent_col + 1 and
217             # everything in that column needs to get shuffled
218             # over one
219 0           $col = $parent_col + 1;
220 0           for my $r (@cells[0 .. $row - 1]) {
221 0 0         next if @$r < $col;
222 0   0       my $here = $r->[$col] || '';
223             # what to splice in
224 0 0         my $splice = $here =~/[+\-]/ ? '-' : ' ';
225 0           splice(@$r, $col, 0, $splice);
226             }
227 0           $col = $parent_col + 1;
228             }
229              
230             # the path is now clear, add the line in
231 0           for ($parent_col..$col) {
232 0   0       $cells[$parent_row][$_] ||= '-';
233             }
234 0           $cells[$parent_row][$col] = '+';
235             }
236              
237             # place the message
238 0           $cells[$row][$col] = $c;
239             # link with vertical dashes
240 0           for ($parent_row+1..$row-1) {
241 0 0 0       $cells[$_][$col] = ($cells[$_][$col] || '') eq '-' ? '{' : '|';
242             }
243 0           _draw_cells(\@cells) if debug;
244             }
245              
246             # pad the rows with spaces
247 0           my $maxcol = max map { scalar @$_ } @cells;
  0            
248 0           for my $row (@cells) {
249 0   0       $row->[$_] ||= ' ' for (0..$maxcol-1);
250             }
251              
252 0           return @cells;
253             }
254              
255             =head2 extract_time( $container )
256              
257             Extracts the time from a Mail::Thread::Container, returned as epoch
258             seconds used to decide the order of adding messages to the rows.
259              
260             =cut
261              
262             sub extract_time {
263 0     0 1   my $self = shift;
264 0           my $container = shift;
265              
266 0           my $date = Mail::Thread->_get_hdr( $container->message, 'date' );
267 0           return str2time( $date );
268             }
269              
270             1;
271             __END__