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.1;
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   552 use strict;
  3         7  
  3         88  
41 3     3   16 use warnings;
  3         6  
  3         87  
42 3     3   15 use utf8;
  3         5  
  3         13  
43 3     3   101 use v5.16;
  3         11  
44              
45 3     3   17 use Carp;
  3         11  
  3         181  
46 3     3   17 use File::Spec;
  3         4  
  3         92  
47 3     3   17 use Encode::Locale qw/$ENCODING_LOCALE/;
  3         6  
  3         399  
48              
49 3     3   1342 use PFT::Map::Node;
  3         10  
  3         97  
50 3     3   1469 use PFT::Map::Index;
  3         8  
  3         188  
51 3     3   24 use PFT::Text;
  3         7  
  3         66  
52 3     3   17 use PFT::Header;
  3         6  
  3         52  
53 3     3   14 use PFT::Date;
  3         7  
  3         5966  
54              
55             sub new {
56 2     2 0 9 my $cls = shift;
57 2         4 my $tree = shift;
58 2 50       23 confess 'want a PFT::Content, got ', ref($tree)
59             unless $tree->isa('PFT::Content');
60              
61 2         18 my $self = bless {
62             tree => $tree,
63             idx => {},
64             next => 0,
65             toresolve => [],
66             }, $cls;
67              
68 2         10 $self->_scan_pages;
69 2         13 $self->_scan_blog;
70 2         18 $self->_scan_tags;
71 2         18 $self->_scan_attach;
72 2         20 $self->_scan_pics;
73 2         11 $self->_resolve;
74 2         9 $self;
75             }
76              
77             sub _resolve {
78             # Resolving items in $self->{toresolve}. They are inserted in _mknod.
79 2     2   4 my $self = shift;
80 2         6 my $index = $self->index;
81              
82 2         33 for my $node (@{$self->{toresolve}}) {
  2         10  
83 17         52 for my $symbol ($node->symbols) {
84 15         30 my $resolved = eval {
85 15         44 my @rs = $index->resolve($node, $symbol);
86 14 100       52 if (@rs != 1) {
87 3         9 local $" = ', ';
88 3 100       452 croak @rs ? "Ambiguous resolution { @rs }"
89             : "No matching item";
90             }
91 11         25 $rs[0]
92             };
93              
94 15 100       316 if (defined $resolved) {
95 11 50 33     68 if (!ref($resolved) || $resolved->isa('PFT::Map::Node')) {
96             # scalar or other node
97 11         31 $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         17 $node->add_symbol_unres($symbol => $reason);
107             }
108             }
109             }
110             delete $self->{toresolve}
111 2         15 };
112              
113             sub _mknod {
114 25     25   46 my $self = shift;
115 25         53 my($cntnt, $hdr) = @_;
116              
117             my $node = PFT::Map::Node->new(
118 25         70 $self->{next} ++,
119             (my $id = $self->index->content_id(@_)),
120             @_,
121             );
122              
123 25 100 66     126 if ($cntnt and $cntnt->isa('PFT::Content::Entry') and $cntnt->exists) {
      100        
124 17         43 push @{$self->{toresolve}}, $node
  17         47  
125             }
126 25 50       86 die if exists $self->{idx}{$id};
127 25         117 $self->{idx}{$id} = $node;
128             }
129              
130             sub _scan_pages {
131 2     2   4 my $self = shift;
132 2         14 $self->_mknod($_) foreach $self->{tree}->pages_ls;
133             }
134              
135             sub _scan_blog {
136 2     2   7 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         14 my($prev, $prev_month, $last_month);
142 2         10 foreach (sort { $a->date <=> $b->date } @blog) {
  16         38  
143 10 100       36 $_->prev($prev) if defined $prev;
144              
145 10         15 $_->month(do {
146 10         21 my $m_date = $_->date->derive(d => undef);
147              
148 10 100 100     38 if (!defined($prev_month) or $prev_month->date <=> $m_date) {
149 3         9 my $m_hdr = PFT::Header->new(date => $m_date);
150 3         11 my $m_page = $tree->entry($m_hdr);
151 3 100       16 my $n = $self->_mknod($m_page,
152             $m_page->exists ? $m_page->header : $m_hdr
153             );
154 3 100       15 $n->prev($prev_month) if defined $prev_month;
155 3         9 $last_month = $prev_month = $n;
156             }
157             $prev_month
158 10         41 });
159              
160 10         20 $prev = $_;
161             }
162              
163 2         7 @{$self}{'last', 'last_month'} = ($prev, $last_month);
  2         9  
164             }
165              
166             sub _scan_tags {
167 2     2   5 my $self = shift;
168 2         5 my $tree = $self->{tree};
169 2         17 my %tags;
170              
171 2         5 for my $node (sort { $a <=> $b } values %{$self->{idx}}) {
  42         73  
  2         11  
172 18         38 foreach (sort $node->header->tags_slug) {
173 8 100       25 my $t_node = exists $tags{$_} ? $tags{$_} : do {
174 3         12 my $t_hdr = PFT::Header->new(title => $_);
175 3         16 my $t_page = $tree->tag($t_hdr);
176 3 100       17 $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   4 my $self = shift;
187 2         10 $self->_mknod($_) for $self->{tree}->attachments_ls;
188             }
189              
190             sub _scan_pics {
191 2     2   4 my $self = shift;
192 2         8 $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 44 my $self = shift;
207 3 100       12 if (@_) {
208 2         5 @{$self->{idx}}{@_}
  2         12  
209             } else {
210 1         2 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         85  
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 139 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   17 my $node = shift;
293             my %out = (
294             id => $node->seqnr,
295 13   66     24 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       30 if (defined(my $prev = $node->prev)) { $out{'<'} = $prev->seqnr }
  5         13  
307 13 100       29 if (defined(my $next = $node->next)) { $out{'>'} = $next->seqnr }
  5         11  
308 13 100       25 if (defined(my $month = $node->month)) { $out{'^'} = $month->seqnr }
  5         10  
309 13 100 100     26 if (defined($node->header)
310             and defined(my $date = $node->header->date)) {
311 7         21 $out{d} = "$date"
312             }
313 13 100       37 if (my @l = $node->days) {
314 2         13 $out{v} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  4         13  
  5         12  
315             }
316 13 100       26 if (my @l = $node->tags) {
317 4         8 $out{t} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  1         4  
  5         9  
318             }
319 13 100       26 if (my @l = $node->tagged) {
320 2         3 $out{'.'} = [sort { $a <=> $b } map{ $_->seqnr } @l]
  4         9  
  5         9  
321             }
322              
323 13         42 \%out
324 1     1 1 8 };
325              
326 1         3 my $self = shift;
327 13         22 map{ $node_dump->($_) }
328 34         55 sort{ $a <=> $b }
329 1         2 values %{$self->{idx}}
  1         7  
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 4779 my $idx = shift->{idx};
362 16         26 my $id = shift;
363 16 100       71 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 4709 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         12 my $cursor = $self->{$key};
418              
419 7 50 33     38 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         3 my @out;
426 2   100     12 while ($n-- && defined $cursor) {
427 8         12 push @out, $cursor;
428 8         18 $cursor = $cursor->prev;
429             }
430 2         9 @out;
431 7 100       20 } : do {
432 5   66     20 while (--$n && defined $cursor) {
433 10         21 $cursor = $cursor->prev;
434             }
435 5         77 $cursor;
436             }
437             }
438              
439             =back
440              
441             =cut
442              
443             1;