File Coverage

blib/lib/Dancer2/Plugin/Menu.pm
Criterion Covered Total %
statement 82 82 100.0
branch 19 20 95.0
condition 4 6 66.6
subroutine 13 13 100.0
pod 1 2 50.0
total 119 123 96.7


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Menu;
2             $Dancer2::Plugin::Menu::VERSION = '0.009';
3 2     2   962833 use 5.010; use strict; use warnings;
  2     2   17  
  2     2   10  
  2         5  
  2         40  
  2         18  
  2         6  
  2         87  
4              
5             # ABSTRACT: Automatically generate an HTML menu for your Dancer2 app
6              
7 2     2   643 use Storable 'dclone';
  2         3240  
  2         178  
8 2     2   600 use Data::Dumper 'Dumper';
  2         6731  
  2         105  
9 2     2   1845 use HTML::Element;
  2         41045  
  2         12  
10 2     2   1231 use Dancer2::Plugin;
  2         231132  
  2         21  
11 2     2   46770 use MooX::HandlesVia;
  2         1524  
  2         20  
12 2     2   220 use Dancer2::Core::Hook;
  2         6  
  2         1635  
13              
14             plugin_keywords qw ( menu_item );
15              
16             # _tree has nodes generated by menu_item function; used to generate menu items
17             # _html_cache contains generated HTML for each route
18             has '_tree' => ( is => 'rw', default => sub { { '/' => { children => {} } } } );
19             has '_html_cache' => ( is => 'rw', default => sub { {} }, handles_via => 'Hash',
20             handles => { get_html_cache => 'get', set_html_cache => 'set' } );
21              
22             # sets up before_template hook to generate HTML from a modified copy of the tree
23             sub BUILD {
24 2     2 0 178 my $s = shift;
25              
26             $s->app->add_hook (Dancer2::Core::Hook->new (
27             name => 'before_template',
28             code => sub {
29 4     4   55511 my $tokens = shift;
30 4         24 my $route = $tokens->{request}->route->spec_route;
31              
32             # send cached html to template if it exists
33 4 100       98 if (my $html = $s->get_html_cache($route)) {
34 1         117 $tokens->{menu} = $html;
35 1         4 return;
36             }
37              
38             # set active menu items on a copy of the tree
39 3         691 my $new_tree = dclone $s->_tree->{'/'};
40 3         11 my $tree = $new_tree;
41 3         21 my @nodes = split /\//, $route;
42              
43 3         21 foreach my $node (@nodes[1 .. @nodes-1]) {
44 7         19 $tree->{children}{$node}{active} = 1;
45 7         17 $tree = $tree->{children}{$node};
46             }
47              
48             # generate html, send to template, save to cache
49 3         35 my $html_elem = _get_html($new_tree, HTML::Element->new('ul'));
50 3         17 my $html = $html_elem->as_HTML('', "\t", {});
51 3         3162 $tokens->{menu} = $html;
52 3         85 $s->set_html_cache($route, $html);
53             }
54 2         68 ));
55             }
56              
57             # Builds tree and associates menu item data with each node in the tree. Called
58             # once for each route wrapped in the "menu_item" keyword.
59             sub menu_item {
60 6     6 1 12279 my $s = shift;
61 6         25 my $mi_data = shift; # menu item data
62 6         14 my $route = shift;
63 6         21 my $tree = $s->_tree;
64 6         32 my @nodes = split /\//, $route->spec_route;
65 6         16 $nodes[0] = '/'; # replace blank node with root node
66              
67             # add node for each segment of the path and associate data with it
68 6         21 while (my $node = shift @nodes) {
69             # set menu item data
70 19         38 my $title = ucfirst($node);
71 19         27 my $weight = 5;
72 19 100       40 if (!@nodes) {
73 6   66     21 $mi_data->{title} //= $title;
74 6   66     23 $mi_data->{weight} //= $weight;
75             }
76              
77             # more nodes after this one so extend tree if next node doesn't already exist
78 19 100       52 if (@nodes) {
    100          
79 13 100       37 if (!$tree->{$node}{children}) {
80 4         10 $tree->{$node}{children} = {};
81             }
82             # add mi_data if we are at the end of a path and therefore route
83             } elsif (!$tree->{$node}{children}) {
84 4         10 $tree->{$node} = $mi_data;
85 4         9 $tree->{$node}{protected} = 1; # don't let new routes clobber node
86 4         19 next; # we can bail early on iteration and save 2 zillionths of a second
87             }
88              
89             # add data to a node that is not at end of existing path
90 15 100       36 if (!$tree->{$node}{protected}) {
91             # if at node at end of route, add mi_data; otherwise defaults are used
92 12 100       26 if (!@nodes) {
93 2         6 ($title, $weight) = ($mi_data->{title}, $mi_data->{weight});
94 2         4 $tree->{$node}{protected} = 1;
95             }
96 12         24 $tree->{$node}{title} = $title;
97 12         21 $tree->{$node}{weight} = $weight;
98             }
99 15         43 $tree = $tree->{$node}{children};
100             }
101             }
102              
103             # generate HTML menu from tree
104             sub _get_html {
105 11     11   143 my ($tree, $element) = @_;
106              
107             # sort sibling menu items by weight and then by name
108 11         15 foreach my $child (
109             sort { ( $tree->{children}{$a}{weight} <=> $tree->{children}{$b}{weight} )
110             || ( $tree->{children}{$a}{title} cmp $tree->{children}{$b}{title} )
111 5 50       23 } keys %{$tree->{children}} ) {
  11         50  
112              
113             # create menu item list element with classes for css styling
114 15         58 my $li_this = HTML::Element->new('li');
115 15 100       326 $li_this->attr(class => $tree->{children}{$child}{active} ? 'active' : '');
116              
117             # add HTML elements for each menu item; recurse if menu item has children
118 15         251 $li_this->push_content($tree->{children}{$child}{title});
119 15 100       225 if ($tree->{children}{$child}{children}) {
120 8         21 my $ul = HTML::Element->new('ul');
121 8         164 $li_this->push_content($ul);
122 8         126 $element->push_content($li_this);
123 8         125 _get_html($tree->{children}{$child}, $ul)
124             } else {
125 7         17 $element->push_content($li_this);
126             }
127             }
128 11         96 return $element;
129             }
130              
131             1; # Magic true value
132              
133             __END__