File Coverage

blib/lib/PFT/Map.pm
Criterion Covered Total %
statement 163 178 91.5
branch 45 54 83.3
condition 20 27 74.0
subroutine 28 36 77.7
pod 13 14 92.8
total 269 309 87.0


line stmt bran cond sub pod time code
1             # Copyright 2014-2016 - Giovanni Simoni
2             #
3             # This file is part of PFT.
4             #
5             # PFT is free software: you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free
7             # Software Foundation, either version 3 of the License, or (at your
8             # option) any later version.
9             #
10             # PFT is distributed in the hope that it will be useful, but WITHOUT ANY
11             # WARRANTY; without even the implied warranty of MERCHANTABILITY or
12             # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with PFT. If not, see .
17             #
18             package PFT::Map v1.4.0;
19              
20             =encoding utf8
21              
22             =head1 NAME
23              
24             PFT::Map - Map of a PFT site
25              
26             =head1 SYNOPSIS
27              
28             my $tree = PFT::Content->new($basedir);
29             PFT::Map->new($tree);
30              
31             =head1 DESCRIPTION
32              
33             The map of a I site allows to navigate the site content.
34              
35             This can be useful for compiling the site or determining properties of the
36             site (e.g. dangling links, important pages…).
37              
38             =cut
39              
40 3     3   512 use strict;
  3         6  
  3         90  
41 3     3   14 use warnings;
  3         5  
  3         98  
42 3     3   20 use utf8;
  3         6  
  3         14  
43 3     3   115 use v5.16;
  3         13  
44              
45 3     3   17 use Carp;
  3         10  
  3         208  
46 3     3   22 use File::Spec;
  3         5  
  3         89  
47 3     3   21 use Encode::Locale qw/$ENCODING_LOCALE/;
  3         6  
  3         417  
48              
49 3     3   1428 use PFT::Map::Node;
  3         10  
  3         87  
50 3     3   1380 use PFT::Map::Index;
  3         9  
  3         160  
51 3     3   25 use PFT::Text;
  3         5  
  3         64  
52 3     3   15 use PFT::Header;
  3         6  
  3         50  
53 3     3   13 use PFT::Date;
  3         5  
  3         5983  
54              
55             sub new {
56 2     2 0 7 my $cls = shift;
57 2         4 my $tree = shift;
58 2 50       24 confess 'want a PFT::Content, got ', ref($tree)
59             unless $tree->isa('PFT::Content');
60              
61 2         14 my $self = bless {
62             tree => $tree,
63             idx => {},
64             next => 0,
65             toresolve => [],
66             }, $cls;
67              
68 2         10 $self->_scan_pages;
69 2         14 $self->_scan_blog;
70 2         21 $self->_scan_tags;
71 2         9 $self->_scan_attach;
72 2         12 $self->_scan_pics;
73 2         11 $self->_resolve;
74 2         7 $self;
75             }
76              
77             sub _resolve {
78             # Resolving items in $self->{toresolve}. They are inserted in _mknod.
79 2     2   5 my $self = shift;
80 2         7 my $index = $self->index;
81              
82 2         3 for my $node (@{$self->{toresolve}}) {
  2         8  
83 17         59 for my $symbol ($node->symbols) {
84 15         30 my $resolved = eval {
85 15         49 my @rs = $index->resolve($node, $symbol);
86 14 100       51 if (@rs != 1) {
87 3         7 local $" = ', ';
88 3 100       445 croak @rs ? "Ambiguous resolution { @rs }"
89             : "No matching item";
90             }
91 11         25 $rs[0]
92             };
93              
94 15 100       336 if (defined $resolved) {
95 11 50 33     68 if (!ref($resolved) || $resolved->isa('PFT::Map::Node')) {
96             # scalar or other node
97 11         32 $node->add_outlink($resolved);
98             } else {
99 0         0 confess "Buggy resolver: searching $symbol",
100             ', expected node, got ', $resolved
101             }
102             }
103             else {
104 4         19 $node->add_outlink(undef);
105 4 50       50 my $reason = ($@ ? $@ =~ s/ at .*$//rs : undef);
106 4         16 $node->add_symbol_unres($symbol => $reason);
107             }
108             }
109             }
110             delete $self->{toresolve}
111 2         9 };
112              
113             sub _mknod {
114 25     25   54 my $self = shift;
115 25         47 my($cntnt, $hdr) = @_;
116              
117             my $node = PFT::Map::Node->new(
118 25         73 $self->{next} ++,
119             (my $id = $self->index->content_id(@_)),
120             @_,
121             );
122              
123 25 100 66     121 if ($cntnt and $cntnt->isa('PFT::Content::Entry') and $cntnt->exists) {
      100        
124 17         38 push @{$self->{toresolve}}, $node
  17         44  
125             }
126 25 50       86 die if exists $self->{idx}{$id};
127 25         125 $self->{idx}{$id} = $node;
128             }
129              
130             sub _scan_pages {
131 2     2   3 my $self = shift;
132 2         16 $self->_mknod($_) foreach $self->{tree}->pages_ls;
133             }
134              
135             sub _scan_blog {
136 2     2   4 my $self = shift;
137 2         5 my $tree = $self->{tree};
138 2         10 my @blog = map $self->_mknod($_),
139             grep !$_->isa('PFT::Content::Month'), $tree->blog_ls;
140              
141 2         17 my($prev, $prev_month, $last_month);
142 2         14 foreach (sort { $a->date <=> $b->date } @blog) {
  16         44  
143 10 100       40 $_->prev($prev) if defined $prev;
144              
145 10         16 $_->month(do {
146 10         21 my $m_date = $_->date->derive(d => undef);
147              
148 10 100 100     39 if (!defined($prev_month) or $prev_month->date <=> $m_date) {
149 3         12 my $m_hdr = PFT::Header->new(date => $m_date);
150 3         14 my $m_page = $tree->entry($m_hdr);
151 3 100       17 my $n = $self->_mknod($m_page,
152             $m_page->exists ? $m_page->header : $m_hdr
153             );
154 3 100       14 $n->prev($prev_month) if defined $prev_month;
155 3         8 $last_month = $prev_month = $n;
156             }
157             $prev_month
158 10         41 });
159              
160 10         21 $prev = $_;
161             }
162              
163 2         7 @{$self}{'last', 'last_month'} = ($prev, $last_month);
  2         10  
164             }
165              
166             sub _scan_tags {
167 2     2   5 my $self = shift;
168 2         5 my $tree = $self->{tree};
169 2         4 my %tags;
170              
171 2         5 for my $node (sort { $a <=> $b } values %{$self->{idx}}) {
  41         70  
  2         12  
172 18         45 foreach (sort $node->header->tags_slug) {
173 8 100       24 my $t_node = exists $tags{$_} ? $tags{$_} : do {
174 3         11 my $t_hdr = PFT::Header->new(title => $_);
175 3         20 my $t_page = $tree->tag($t_hdr);
176 3 100       18 $tags{$_} = $self->_mknod($t_page,
177             $t_page->exists ? $t_page->header : $t_hdr
178             );
179             };
180 8         23 $node->add_tag($t_node);
181             }
182             }
183             }
184              
185             sub _scan_attach {
186 2     2   5 my $self = shift;
187 2         11 $self->_mknod($_) for $self->{tree}->attachments_ls;
188             }
189              
190             sub _scan_pics {
191 2     2   6 my $self = shift;
192 2         10 $self->_mknod($_) for $self->{tree}->pics_ls;
193             }
194              
195             =head2 Properties
196              
197             =over
198              
199             =item nodes
200              
201             List of the nodes
202              
203             =cut
204              
205             sub nodes {
206 3     3 1 12 my $self = shift;
207 3 100       11 if (@_) {
208 2         4 @{$self->{idx}}{@_}
  2         12  
209             } else {
210 1         3 values %{$self->{idx}}
  1         7  
211             }
212             }
213              
214             =item ids
215              
216             List of the mnemonic ids.
217              
218             map $m->id_to_node($_), $m->ids
219              
220             is equivalent to
221              
222             $m->nodes
223              
224             =cut
225              
226 3     3 1 4 sub ids { keys %{shift->{idx}} }
  3         84  
227              
228             =item tree
229              
230             The associated content tree
231              
232             =cut
233              
234 0     0 1 0 sub tree { shift->{tree} }
235              
236             =item pages
237              
238             List of page nodes
239              
240             =cut
241              
242             sub _grep_content {
243 0     0   0 my($self, $type) = @_;
244              
245 0         0 sort{ $a <=> $b }
246 0         0 grep{ $_->content_type eq $type }
  0         0  
247             $self->nodes
248             }
249              
250 0     0 1 0 sub pages { shift->_grep_content('PFT::Content::Page') }
251              
252             =item months
253              
254             List of month nodes
255              
256             =cut
257              
258 0     0 1 0 sub months { shift->_grep_content('PFT::Content::Month') }
259              
260             =item tags
261              
262             List of tag nodes
263              
264             =cut
265              
266 0     0 1 0 sub tags { shift->_grep_content('PFT::Content::Tag') }
267              
268             =item index
269              
270             The PFT::Map::Index object associated to this map.
271              
272             It handles the unique identifiers of content items and can be used to
273             query the map.
274              
275             =cut
276              
277 27     27 1 141 sub index { PFT::Map::Index->new(shift) }
278              
279             =item dump
280              
281             # TODO: move forward this description, as method
282              
283             Dump of the nodes in a easy-to-display form, that is a list of
284             dictionaries.
285              
286             This method is used mainly or solely for testing.
287              
288             =cut
289              
290             sub dump {
291             my $node_dump = sub {
292 13     13   15 my $node = shift;
293             my %out = (
294             id => $node->seqnr,
295 13   66     28 tt => $node->title || do {
296             do {
297             my $cnt = $node->content;
298             $cnt->isa('PFT::Content::Month') ? '' :
299             $cnt->isa('PFT::Content::Attachment') ? '' :
300             $cnt->isa('PFT::Content::Picture') ? '' :
301             confess "what is $node?";
302             }
303             },
304             );
305              
306 13 100       29 if (defined(my $prev = $node->prev)) { $out{'<'} = $prev->seqnr }
  5         11  
307 13 100       28 if (defined(my $next = $node->next)) { $out{'>'} = $next->seqnr }
  5         10  
308 13 100       25 if (defined(my $month = $node->month)) { $out{'^'} = $month->seqnr }
  5         10  
309 13 100 100     25 if (defined($node->header)
310             and defined(my $date = $node->header->date)) {
311 7         20 $out{d} = "$date"
312             }
313 13 100       34 if (my @l = $node->days) {
314 2         12 $out{v} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  4         10  
  5         13  
315             }
316 13 100       29 if (my @l = $node->tags) {
317 4         8 $out{t} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  1         4  
  5         12  
318             }
319 13 100       29 if (my @l = $node->tagged) {
320 2         3 $out{'.'} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  4         8  
  5         9  
321             }
322              
323 13         42 \%out
324 1     1 1 9 };
325              
326 1         3 my $self = shift;
327 13         21 map{ $node_dump->($_) }
328 32         56 sort{ $a <=> $b }
329 1         3 values %{$self->{idx}}
  1         6  
330             }
331              
332             =back
333              
334             =head2 Methods
335              
336             =over
337              
338             =cut
339              
340             =item node_of
341              
342             Given a PFT::Content::Base (or any subclass) object, returns the
343             associated node, or undef if such node does not exist.
344              
345             =cut
346              
347             sub node_of {
348 0     0 1 0 my $self = shift;
349 0         0 my $id = $self->index->content_id(@_);
350 0 0       0 exists $self->{idx}{$id} ? $self->{idx}{$id} : undef
351             }
352              
353             =item id_to_node
354              
355             Given a unique mnemonic id (as in C) returns the
356             associated node, or C if there is no such node.
357              
358             =cut
359              
360             sub id_to_node {
361 16     16 1 3928 my $idx = shift->{idx};
362 16         24 my $id = shift;
363 16 100       61 exists $idx->{$id} ? $idx->{$id} : undef
364             }
365              
366             =item blog_recent
367              
368             Getter for the most I recent blog nodes. Accepts I as strictly
369             positive integer parameter.
370              
371             In list context returns the I most recent blog nodes, ordered by date,
372             from most to least recent. Less than I nodes will be returned if I
373             is greater than the number of available entries.
374              
375             In scalar context returns the I-th to last entry. For I equal to
376             zero the most recent entry is returned.
377              
378             =cut
379              
380             sub blog_recent {
381 7     7 1 4740 shift->_recent(last => shift)
382             }
383              
384             =item months_recent
385              
386             Getter for the most I recent month nodes. Accepts I as strictly
387             positive integer parameter.
388              
389             In list context returns the I most recent month nodes, ordered by date,
390             from most to least recent. Less than I nodes will be returned if I
391             is greater than the number of available entries.
392              
393             In scalar context returns the I-th to last entry. For I equal to
394             zero the most recent entry is returned. The C value is returned
395             if there are less then I available.
396              
397             =cut
398              
399             sub months_recent {
400 0     0 1 0 shift->_recent(last_month => shift)
401             }
402              
403             =item blog_exists
404              
405             Checker for blog history.
406              
407             Returns a boolean value telling if the site contains at least one blog entry.
408              
409             =cut
410              
411             sub blog_exists {
412 0     0 1 0 defined shift->_recent(last => 1);
413             }
414              
415             sub _recent {
416 7     7   16 my($self, $key, $n) = @_;
417 7         15 my $cursor = $self->{$key};
418              
419 7 50 33     34 if (!defined $n or $n <= 0) {
420 0 0       0 croak "Invalid N, expected positive integer, got: ",
421             defined $n ? $n : 'undef'
422             }
423              
424             wantarray ? do {
425 2         4 my @out;
426 2   100     9 while ($n-- && defined $cursor) {
427 8         13 push @out, $cursor;
428 8         19 $cursor = $cursor->prev;
429             }
430 2         9 @out;
431 7 100       21 } : do {
432 5   66     19 while (--$n && defined $cursor) {
433 10         25 $cursor = $cursor->prev;
434             }
435 5         79 $cursor;
436             }
437             }
438              
439             =back
440              
441             =cut
442              
443             1;