# Copyright 2013-2014 Stefan Goebel. # # This file is part of Newcomen. # # Newcomen is free software: you can redistribute it and/or modify it under the terms of the GNU # General Public License as published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # Newcomen is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even # the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. # # You should have received a copy of the GNU General Public License along with Newcomen. If not, see # . package Newcomen::Plugin::Blog::Menu; our $VERSION = 2014052501; use namespace::autoclean; use Moose '-meta_name' => '_moose_meta'; use MooseX::StrictConstructor; use Newcomen::Plugin::Blog::Menu::Item; use Newcomen::Util::String; extends 'Newcomen::Plugin::Base'; with 'Newcomen::Role::Attribute::Config'; override '_build_default_config' => sub {{ 'blog' => { 'menu' => {}, } }}; has 'menu' => ( 'is' => 'ro', 'isa' => 'Newcomen::Plugin::Blog::Menu::Item', 'init_arg' => undef, 'default' => sub { Newcomen::Plugin::Blog::Menu::Item -> new ('id' => 'root') }, ); sub _process_path { my $self = shift; my $path = shift; return $path =~ /^\d+_(.+)/a ? ($path, $1) : (undef, $path); } sub _process_item { my $self = shift; my $path = shift; my $data = shift; my @parts = Newcomen::Util::String::split_ssv ($path); my $last = pop @parts; my $root = $self -> menu (); for my $part (@parts) { my ($order, $id) = $self -> _process_path ($part); my $child = $root -> child ($id); unless ($child) { $child = Newcomen::Plugin::Blog::Menu::Item -> new ( 'id' => $id, 'order' => $order, 'parent' => $root, ); $child -> set (['text' ], $id); $child -> set (['title'], $id); $child -> set (['auto' ], 1 ); } $root = $child; } my ($order, $id) = $self -> _process_path ($last); my $child = $root -> child ($data -> {'id'} // $id); if (not $child or $child -> get (['auto'])) { $child = Newcomen::Plugin::Blog::Menu::Item -> new ( 'id' => $data -> {'id'} // $id, 'parent' => $root, ) unless $child; $child -> order ($data -> {'order'} // $order); $child -> url ($data -> {'url' } // undef ); $child -> set (['text' ], $data -> {'text' } // $child -> id ()); $child -> set (['title'], $data -> {'title'} // $child -> id ()); $child -> set (['anchor'], $data -> {'anchor'}) if $data -> {'anchor'}; $child -> set (['source'], $data -> {'source'}) if $data -> {'source'}; $child -> delete (['auto']); while (my ($key, $value) = each %$data) { next if $key =~ /^(?:id|order|url|text|title|anchor|source)$/; $child -> set ($key, $value); } } } sub hook_init { my $self = shift; my $src = shift; my $menu = $self -> _config () -> get (['blog', 'menu']) // {}; $self -> _core () -> set (['blog', 'menu'], $self -> menu ()); while (my ($path, $data) = each %$menu) { $self -> _process_item ($path, $data); } } sub hook_process_source { my $self = shift; my $src = shift; my $menu = $src -> get (['menu']); return 1 unless $menu; $menu = [$menu] if ref $menu eq ''; if (ref $menu eq 'HASH') { while (my ($path, $data) = each %$menu) { $self -> _process_item ($path, { %$data, 'source' => $src }); } } elsif (ref $menu eq 'ARRAY') { for my $item (@$menu) { my ($anchor, $path) = $item =~ /^\+(\w+)\s+(.+)$/a ? ('#' . $1, $2) : (undef, $item); $self -> _process_item ($path, { 'anchor' => $anchor, 'source' => $src }); } } return 1; } sub hook_post_build_pages { my $self = shift; $self -> _set_urls ($self -> menu ()); } sub _set_urls { my $self = shift; my $root = shift; unless ($root -> url ()) { if (my $src = $root -> get (['source'])) { if (my $url = $src -> get (['single_url'])) { $root -> url ($url); } } } for my $child ($root -> children ()) { $self -> _set_urls ($child); } } sub hook_renderer { my $self = shift; my $page = shift; $self -> menu () -> activate ($page -> target ()); } __PACKAGE__ -> _moose_meta () -> make_immutable (); 1; __END__ #################################################################################################### =head1 NAME Newcomen::Plugin::Blog::Menu - Manages a site menu. =head1 DESCRIPTION This plugin allows the user to define a menu structure, which can then be used in the templates to display a menu. Menu items can be defined in the configuration file, as well as in the article sources' meta data. See L and L for details. The menu items defined in the user configuration will be processed first, menu items defined in article sources will be processed after that. The menu structure is stored using L instances, please see the documentation of this module for more details on the instance attributes, methods etc. Before a page is rendered, the L method of the root menu item will be called with the page target as parameter, so for a page that is included in the menu the I attributes of the items will be set appropriately. =head1 OPTIONS { 'blog' => { 'menu' => {}, }, } These are the default options set by this plugin. They may be overridden by user configuration. The I option must be a hashref. The keys specify menu item paths, the values must themselves be hashrefs, containing the data for the menu item to be created. =head2 Path Processing The path of a menu item specifies the item's position in the menu hierarchy. The path consists of one or more menu IDs, separated by slashes (C<'/'>). The menu IDs uniquely identify a menu item at a specific level in the menu hierarchy. Intermediate items in the hierarchy will automatically be created if required. The properties of these intermediate items will be derived from the path. The path is split at the separator, resulting in one or more path components, each one relating to one menu item in the menu hierarchy. In general, a path component specifies the menu ID for the item. If this string starts with at least one digit, followed by an underscore, followed by at least one other character, the original value will be used as the I property of the menu item, and the ID will be the original value with the leading digit(s) and underscore removed. If a path component does not start with the aforementioned pattern, the order value of the item will be set to C. In any case, the menu item's meta data I and I<text> will be set to the same value as the ID for intermediate items (see below on details how to override these for the leafs). For automatically created intermediate items, the meta data will also include the key I<auto>, with its value set to C<1>. =head2 Menu Item Data The final element in the path (the leaf) specifies the actual menu item for which the hashref contains the menu data. The hashref may include any of the following keys: =over =item * I<id>, to specify the ID. If this is not set, the value from the path (as described above) will be used. =item * I<order>, to specify an order value. If this is not set, the value will be derived from the path as described above. =item * I<text> and I<title> will be copied to the item's meta data (under the same keys). If these are not set, they will be set to the same value as the item's ID. =item * I<url> and I<anchor> may be used to specify the URL and an anchor on the target page. If the anchor is set, it should include the leading C<'#'> character. Note that the anchor is stored in the item's meta data, while the URL is stored in its own attribute of the L<Newcomen::Plugin::Blog::Menu::Item> instance. Both are optional. =item * Any other key not mentioned above will simply be copied to the menu item's meta data. =back =head2 Overriding Menu Items Once a menu item has been created, it can not be overridden, with the exception of automatically created (intermediate) items. More precisely, any item with the I<auto> meta data set to a true value can be overridden. Any such item will only be overridden by explicitly creating it, i.e. it will not be changed if it appears again as an intermediate item. If any such item is overridden, the I<auto> flag will be deleted from the meta data (before copying the new meta data, so if this new meta data should contain another I<auto> value, this new value will be preserved). =head2 Example The following is an example of a valid menu configuration. Note that not all data provided in the example is required, and some data may be derived from the path as described above. { 'blog' => { 'menu' => { '999_Meta/About' => { 'id' => 'about', 'url' => '/about.html', 'text' => 'About This Site', 'title' => 'About', 'anchor' => '#top', 'order' => '500_About', 'color' => 'red', } }, }, } In this example the path is set to C<'999_Meta/About'>. One intermediate menu item is created for the C<'999_Meta'> path automatically as described above, unless it already exists. Its ID (as well as its I<text> and I<title> meta data) will be set to C<'Meta'>, and its I<order> attribute to C<'999_Meta'>. The actual leaf menu item will be created as a child of the C<'Meta'> item. Its properties would be derived from the path, if they were not specified in the item's hashref. See the section L<Menu Item Data|/Menu Item Data> for a description. =head1 META DATA =head2 Sources { 'menu' => <menu specification>, 'single_url' => <used as URL unless overridden>, } Menu entries may be specified in a source's meta data, the key to specify one or more entries is I<menu>. The I<single_url> meta data will be used as URL value for a menu item, unless another value is set explicitly. See L<Newcomen::Plugin::Blog::Single> for details on I<single_url>. The I<< <menu specification> >> may be: =over =item * A single string. In that case, this string is processed as the path of the menu entry to be created (see L<Path Processing|/Path Processing>). =item * An arrayref, containing multiple strings as described above. =item * A hashref. In that case the I<menu> meta data works basically like the I<blog/menu> configuration option, see L<OPTIONS|/OPTIONS>. =back For a menu entry created from a source's meta data, the menu item's meta data will contain a key I<source>, referencing the L<Newcomen::Source> instance in which the menu entry was defined (only for the leaf, it will not be set for the automatically created intermediate items). This will later be used to automatically set the menu item's URL: once the pages have been created, for menu items with the I<source> meta data set, the value of I<single_url> in the source's meta data will be used as URL for the menu item. If an URL is set explicitly (only possible for the hashref method, not for single strings), it will not be overridden, though. The I<single_url> meta data is usually set by the L<Newcomen::Plugin::Blog::Single> plugin for the source items. For the single string method (either a single value or an arrayref containing multiple strings), the strings are processed as menu item paths, as described above. However, there is one addition: If the string starts with a plus sign (C<'+'>), followed by at least one word character (C</\w/>), followed by at least one space character (C</\s/>), followed by at least one other character, the first part of the string up to the space will be used as anchor value, with the plus sign replaced by a C<'#'>, and the last part of the string (everything after the first space(s)) will be used as path. The anchor value will be set in the meta data of the menu item. For example, the string C<'+chapter2 Some/Menu/Entry'> would create a menu structure with the two intermediate items for C<'Some'> and C<'Menu'> (as described under L<OPTIONS|/OPTIONS>), and for the last menu item, C<'Entry'>, the anchor value will be set to C<'#chapter2'>. Note: When using the hashref method to specify menu items, the hashref's keys are used as paths without further processing (the same as for the configuration). An anchor has to be set explicitly in the menu data in this case. =head1 GLOBAL DATA { 'blog' => { 'menu' => <menu root element>, }, } This is the global data (see L<Newcomen::Core>) set by this plugin. I<blog/menu> is the L<Newcomen::Plugin::Blog::Menu::Item> instance representing the root element of the menu. Its ID is C<'root'>, other properties are not set for the root item. =head1 INSTANCE METHODS =head2 menu my $menu = $plugin -> menu (); Returns the L<Newcomen::Plugin::Blog::Menu::Item> instance representing the root element of the menu. Basically the same as accessing the I<blog/menu> global value (see above). =head1 HOOKS This plugin implements the following hooks (with default priority unless mentioned otherwise): I<hook_init()>, I<hook_process_source()>, I<hook_post_build_pages()>, I<hook_renderer()>. =head1 SEE ALSO L<Newcomen::Core>, L<Newcomen::Plugin::Blog::Menu::Item>, L<Newcomen::Plugin::Blog::Single>, L<Newcomen::Source> =head1 VERSION This is version C<2014052501>. =head1 AUTHOR Stefan Goebel - newcomen {at} subtype {dot} de =head1 COPYRIGHT AND LICENSE Copyright 2013-2014 Stefan Goebel. This file is part of Newcomen. Newcomen is free software: you can redistribute it and/or modify it under the terms of the L<GNU General Public License|http://www.gnu.org/licenses/gpl.html> as published by the L<Free Software Foundation|http://www.fsf.org/>, either version 3 of the license, or (at your option) any later version. Newcomen is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the L<GNU General Public License|http://www.gnu.org/licenses/gpl.html> for more details. You should have received a copy of the L<GNU General Public License|http://www.gnu.org/licenses/gpl.html> along with Newcomen. If not, see <L<http://www.gnu.org/licenses/>>. =cut #################################################################################################### # :indentSize=3:tabSize=3:noTabs=true:mode=perl:maxLineLen=100: