File Coverage

blib/lib/Dancer2/Plugin/Menu.pm
Criterion Covered Total %
statement 75 75 100.0
branch 14 16 87.5
condition 2 6 33.3
subroutine 13 13 100.0
pod 1 2 50.0
total 105 112 93.7


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