# 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::Page; our $VERSION = 2014101201; use namespace::autoclean; use Moose '-meta_name' => '_moose_meta'; use MooseX::Aliases; use MooseX::StrictConstructor; use Path::Class (); use Newcomen::Data; use Newcomen::Renderer::Specification; use Newcomen::Util::Traits; use Newcomen::Writer::Specification; with 'Newcomen::Role::Attribute::Core'; with 'Newcomen::Role::Attribute::Formatter'; with 'Newcomen::Role::Attribute::Plugins'; with 'Newcomen::Role::Attribute::Renderer'; with 'Newcomen::Role::Attribute::Writer'; has '_content' => ( 'is' => 'ro', 'isa' => 'ArrayRef[Newcomen::Content]', 'init_arg' => 'content', 'default' => sub {[]}, 'traits' => ['Array'], 'handles' => Newcomen::Util::Traits::std_array_handles (), ); has 'target' => ( 'is' => 'ro', 'isa' => 'Str', 'writer' => '_set_target', 'required' => 1, ); has 'creator' => ( 'is' => 'ro', 'isa' => 'Str', 'required' => 1, ); has 'collection' => ( 'is' => 'ro', 'isa' => 'Maybe[Newcomen::Collection]', 'default' => undef, ); has 'rendered' => ( 'is' => 'rw', 'isa' => 'Maybe[Str]', 'init_arg' => undef, 'default' => undef, ); has '_page_meta' => ( 'is' => 'ro', 'isa' => 'Newcomen::Data', 'init_arg' => undef, 'default' => sub { Newcomen::Data -> new () }, 'handles' => [qw( delete set )], ); has 'meta' => ( 'is' => 'rw', 'isa' => 'Newcomen::Data', 'init_arg' => undef, 'writer' => '_set_meta', 'default' => sub { Newcomen::Data -> new () }, 'handles' => [qw( exists get )], ); for (qw( Renderer Writer )) { has lc $_ => ( 'is' => 'rw', 'isa' => 'Maybe[Newcomen::' . $_ . '::Specification]', 'default' => undef, 'init_arg' => undef, ); } alias 'content' => 'item'; alias 'contents' => 'items'; sub BUILD { my $self = shift; my $target = $self -> target (); for my $plugin ($self -> _plugins () -> sort ('hook_rewrite_target')) { $target = $self -> _plugins () -> plugin ($plugin) -> hook_rewrite_target ( $target, $self -> creator () ); } confess 'A page must have a target' unless $target; $self -> _set_target ($target); } # Using an alias 'page_meta()' allows an after method modifier to be set for 'page_meta()' while # using '_page_meta()' for merging, avoiding infinite recursion. alias 'page_meta' => '_page_meta'; sub _merge_meta { my $self = shift; $self -> _set_meta ( Newcomen::Data -> new () -> merge ( $self -> collection () ? $self -> collection () -> meta () : {}, $self -> _page_meta () ) ); } before [qw( exists get meta )] => sub { shift -> _merge_meta (); }; after [qw( delete set page_meta )] => sub { shift -> _merge_meta (); }; sub _set_backend { my $self = shift; my $specification = shift; my $creator = shift; my $accessor = shift; my $spec_class = shift; unless ($creator) { my $package = substr (scalar caller (1), length 'Newcomen::Plugin::'); $creator = qr/^\Q$package\E$/; } return if not $specification or $self -> $accessor () or $self -> creator () !~ $creator; my $name = ref $specification eq '' ? $specification : $specification -> {'name' }; my $opts = ref $specification eq '' ? {} : $specification -> {'options'}; $self -> $accessor ($spec_class -> new ('name' => $name)); $self -> $accessor () -> options () -> merge ($opts) if $opts; } sub set_renderer { my $self = shift; my $spec = shift; my $creator = shift; $self -> _set_backend ($spec, $creator, 'renderer', 'Newcomen::Renderer::Specification'); } sub set_writer { my $self = shift; my $spec = shift; my $creator = shift; $self -> _set_backend ($spec, $creator, 'writer', 'Newcomen::Writer::Specification'); } sub rel { my $self = shift; my $path = shift; return $path if $path =~ /^[a-z]+:/ai; my $tgt_path = Path::Class::file ('.', $self -> target ()) -> parent (); my $rel_path = Path::Class::file ('.', $path ) -> relative ($tgt_path); return $rel_path -> as_foreign ('Unix') -> stringify (); } sub dir { my $self = shift; my $path = Path::Class::file ('.', $self -> target ()) -> parent () -> as_foreign ('Unix'); if ($path eq '.') { $path = ''; } else { $path =~ s!^/*!!; $path .= '/' unless $path =~ m!/$!; } return $path; } sub format { my $self = shift; return unless $self -> count (); for my $index (0 .. $self -> count () - 1) { my $content = $self -> content ($index); $self -> _formatter () -> format_content ($content, $self, $index); } } sub render { $_ [0] -> _renderer () -> render_page ($_ [0]) } sub write { $_ [0] -> _writer () -> write_page ($_ [0]) } __PACKAGE__ -> _moose_meta () -> make_immutable (); 1; __END__ #################################################################################################### =head1 NAME Newcomen::Page - Represents a single output page of the project. =head1 SYNOPSIS use Newcomen::Page; my $page = Newcomen::Page -> new ( 'creator' => $creator, 'target' => $target, 'content' => $content, 'collection' => $collection, ); # Add a piece of content: $page -> push ($content); # Set some meta data: $page -> set (['some', 'data'], 'value'); =head1 DESCRIPTION An I instance combines zero or more L instances and meta data to a page. See the developer section in L for a description of how B works and how pages fit in. B The handling of meta data of an I instance differs from most other classes. Please see the L section below for details. =head1 CLASS METHODS =head2 new my $page = Newcomen::Page -> new ( 'creator' => $creator, 'target' => $target, 'content' => $content, 'collection' => $collection, ); Constructor. The I parameter identifies the plugin that creates the page. It is suggested, though not required, to use the module name or module basename (i.e. the module name without the C<'Newcomen::Plugin::'> prefix). The I parameter identifies the page. Usually this is the relative target path of the file to be generated. This is not required, though some writers (see L) may expect it to be. If the page is added to the site (see L), every page must have a unique target. The target will be rewritten when an instance is created, by calling the I hook of all plugins implementing it. This can not be disabled on a per-page basis (though a rewrite plugin may be disabled globally). After that, the target can not be changed anymore. The final (rewritten) target must not be empty. The I and I parameters are required. The optional I parameter may be used to set the initial list of content items. If provided, it must be an arrayref containing L instances (or an empty arrayref). The optional I parameter may be used to reference the collection from which a page was created. If provided, it must be an L instance (or C). This can not be changed later on. =head1 INSTANCE METHODS =head2 General =head3 creator, target, collection my $creator = $page -> creator (); my $target = $page -> target (); my $collection = $page -> collection (); Returns the creator ID, the page target or the source collection from which the page was created, respectively. The values are set via the constructor (see L above for a description), and can not be changed later on. Keep in mind that the page's target is subject to the rewrite hooks, thus I may not return the same value that was passed to the constructor when creating the instance. The return value of the I method may be C. =head3 rel my $rel_path = $page -> rel ($path); Returns the path specified as the only (and required) parameter relative to the page's parent directory, as determined by the page target. Both the page target and the supplied parameter have to be file names relative to a common parent directory (usually the output directory). For example, if the page target is C<'foo/bar/page'> and the path C<'foo/baz/something'> is specified as parameter to the I method, the return value will be C<'../baz/something'>. Note that the two paths will always be interpreted as relative paths, even if they start with C<'/'>. The I method will use L to determine the relative path to return. It will always be returned in Unix format (i.e. using slashes as separators). B There are is a built-in exception to the above rule. If the parameter looks like an URL, i.e. if it matches the pattern C, it will be returned as is. This exception allows using the I method in HTML templates without checking for absolute URLs or, for example, C or C etc. links in the template itself. =head3 dir my $dir = $page -> dir (); Returns the parent directory of the page's target, in Unix format. This obviously only works if the page target is a path (or something path-like). No assumption is made about the format of the page target, it may not work if it is not specified in the platform's native path format. L is used to get the parent directory. If the target does not have any parent directory (i.e. if it is set to only a file name), this method will return an empty string. Otherwise it will be a relative path in Unix format: it will never begin with a slash (i.e. even if the page target starts with a slash, it will be removed from the result of this method), but it will always end with a slash. =head3 rendered # Get the rendered content: my $rendered = $page -> rendered (); # Set the rendered content: $page -> rendered ($rendered); Getter/setter for the page's rendered content. This will be set by L during the rendering stage. In general, plugins should not set it directly. Some writers may expect this to be the content that will be written to disk, see the individual writer backends for details. The parameter for the setter may be any string, or C. =head2 Content I uses an arrayref to store the content instances as an ordered list. It uses the L of L, with the following additions: =over =item * The method I is an alias for the L method. =item * The method I is an alias for the L method. =back Please see L for a complete list of methods and more details. =head2 Meta Data =head3 get, set, exists, delete # Set meta data: $page -> set (['some', 'data'], 'value'); # Retrieve and delete it, if it exists: my $value = 'some default value'; if ($page -> exists (['some', 'data'])) { $value = $page -> get (['some', 'data']); $page -> delete (['some', 'data']); } Meta data of an I instance consists of two parts: The meta data of the collection item referenced by the page (if there is one), and the page's own meta data. The methods I and I will operate on the page's meta data if it exists for a given key. If it does not exist, they will operate on the collection's meta data for that key. Note that the current implementation uses the merged data (see below), which will always be cloned. So any references will not point to the original data! The methods I and I on the other hand will only operate on the page's meta data. =over The methods I, I, I and I are mapped to the L methods of the same name. Please see there for details on the parameters. =back =head3 meta my $merged = $page -> meta (); The L instance returned by the I method contains both items' meta data merged (with page meta data overriding collection meta data if necessary) at the time of the call. Merging will be done before or after (as appropriate) every call to I, I, I, I, I or L. Once retrieved, the instance will not be updated (a new instance is created on every merge)! Any changes to the L instance returned by I will be lost on the next call to one of the aforementioned methods. It is recommended to only use I to read the meta data (mostly used in the templates). The I method exists only to provide an interface consistent with the other classes. =head3 page_meta my $page_meta = $page -> page_meta (); The L instance returned by I contains the page's own meta data, without any collection meta data. If access to page meta data only is required, without falling back to the collection meta data, this method may be used to access the L instance. After every call to I meta data merging occurs, so changes in the page's meta data will be visible when using L. =head2 Rendering And Writing Backends One rendering and one writing backend may be assigned to a page using the following methods: =head3 renderer, writer # Get current renderer backend settings: my $renderer_spec = $page -> renderer (); # Set new writer backend: $page -> writer ($writer_spec); Get or set the renderer or writer specification for the page. The return value of the getter as well as the parameter for the setter is either an L instance or an L instance, respectively. Both may also be set to C if required. =head3 set_renderer, set_writer # Set renderer by name: $page -> set_renderer ($name, qr/^Some::Plugin::/); # Set renderer (name and options): $page -> set_renderer ( {'name' => $name, 'options' => $options}, $creator ); These methods will set the renderer (or writer), but only if the following conditions are both true: =over =item * No renderer/writer is currently set for the page (both renderer/writer are C). =item * The page's creator matches the second parameter (a regular expression, see below). =back Both methods will do nothing if one of these conditions is not met. The first parameter must be either C, a string, or a hashref. If it is C (or another value that evaluates to false), these methods will do nothing. If it is a string, the renderer (or writer) of the page will be set to a backend specification using this name, with empty options. If it is a hashref, it must contain the key C<'name'> (the value will be used as the backend name), and it may contain the key C<'options'> (the value must be a hashref itself and will be used as backend options). Other keys will be ignored. See above for an example. If no second parameter is supplied, the pages creator must match the caller's basename (i.e. the caller's module name without the C<'Newcomen::Plugin::'> prefix). In this case, you must not use these methods from within modules that are not in the I namespace! If the second parameter is supplied, it must be a regular expression (C, see the example above), and the page's creator will be matched against it. =head2 Formatting, Rendering, Writing =head3 format $page -> format (); This method will format all the content items on the page, according to their formatter settings. It will simply iterate over the L instances and call the L method of the main L instance for each content item. =head3 render, write $page -> render (); $page -> write (); These two methods will render and write the page, respectively. They are simple wrappers for the respective methods of the main renderer and writer instances: L (of L) and L (of L). The two method calls above are effectively the same as $renderer -> render_page ($page); $writer -> write_page ($page); With C<$renderer> and C<$writer> being the renderer and writer instances provided by the core (see L). =head1 SEE ALSO L, L, L, L, L, L, L, L, L, L, L, L, L =head1 VERSION This is version C<2014101201>. =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 as published by the L, 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 for more details. You should have received a copy of the L along with Newcomen. If not, see >. =cut #################################################################################################### # :indentSize=3:tabSize=3:noTabs=true:mode=perl:maxLineLen=100: