# 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^[a-z]+:/ai>, 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: