Back to documentation
package Statocles::Document;
our $VERSION = '0.094';
# ABSTRACT: Base class for all Statocles documents
use Statocles::Base 'Class';
with 'Statocles::Role::PageAttrs';
use Statocles::Image;
use Statocles::Util qw( derp );
use YAML ();
use JSON::PP qw( decode_json );
=attr path
The path to this document. This is not settable from the frontmatter.
=cut
has path => (
is => 'rw',
isa => PagePath,
coerce => PagePath->coercion,
);
=attr store
The Store this document comes from. This is not settable from the
frontmatter.
=cut
has store => (
is => 'ro',
isa => StoreType,
coerce => StoreType->coercion,
);
=attr title
---
title: My First Post
---
The title of this document. Used in the template and the main page
title. Any unsafe characters in the title (C<E<lt>>, C<E<gt>>, C<">, and
C<&>) will be escaped by the template, so no HTML allowed.
=cut
=attr author
---
author: preaction <doug@example.com>
---
The author of this document. Optional. Either a simple string containing
the author's name and optionally, in E<gt>E<lt>, the author's e-mail address,
or a hashref of L<Statocles::Person attributes|Statocles::Person/ATTRIBUTES>.
---
# Using Statocles::Person attributes
author:
name: Doug Bell
email: doug@example.com
---
=cut
sub _build_author { }
=attr status
The publishing status of this document. Optional. Statocles apps can
examine this to determine whether to turn a document into a page. The
default value is C<published>; other reasonable values could include
C<draft> or C<private>.
=cut
has status => (
is => 'rw',
isa => Str,
default => 'published',
);
=attr content
The raw content of this document, in markdown. This is everything below
the ending C<---> of the frontmatter.
=cut
has content => (
is => 'rw',
isa => Str,
);
=attr tags
---
tags: recipe, beef, cheese
tags:
- recipe
- beef
- cheese
---
The tags for this document. Tags are used to categorize documents.
Tags may be specified as an array or as a comma-separated string of
tags.
=cut
has tags => (
is => 'rw',
isa => ArrayRef,
default => sub { [] },
coerce => sub {
return [] unless $_[0];
if ( !ref $_[0] ) {
return [ split /\s*,\s*/, $_[0] ];
}
return $_[0];
},
);
=attr links
---
links:
stylesheet:
- href: /theme/css/extra.css
alternate:
- href: http://example.com/blog/alternate
title: A contributed blog
---
Related links for this document. Links are used to build relationships
to other web addresses. Link categories are named based on their
relationship. Some possible categories are:
=over 4
=item stylesheet
Additional stylesheets for the content of this document.
=item script
Additional scripts for the content of this document.
=item alternate
A link to the same document in another format or posted to another web site
=back
Each category contains an arrayref of hashrefs of L<link objects|Statocles::Link>.
See the L<Statocles::Link|Statocles::Link> documentation for a full list of
supported attributes. The most common attributes are:
=over 4
=item href
The URL for the link.
=item text
The text of the link. Not needed for stylesheet or script links.
=back
=cut
=attr images
---
images:
title:
src: title.jpg
alt: A title image for this post
banner: banner.jpg
---
Related images for this document. These are used by themes to display
images in appropriate templates. Each image has a category, like C<title>,
C<banner>, or C<thumbnail>, mapped to an L<image object|Statocles::Image>.
See the L<Statocles::Image|Statocles::Image> documentation for a full
list of supported attributes. The most common attributes are:
=over 4
=item src
The source path of the image. Relative paths will be resolved relative
to this document.
=item alt
The alternative text to display if the image cannot be downloaded or
rendered. Also the text to use for non-visual media.
=back
=cut
=attr date
---
date: 2015-03-27
date: 2015-03-27 12:04:00
---
The date/time this document is for. For pages, this is the last modified date.
For blog posts, this is the post's date.
Should be in C<YYYY-MM-DD> or C<YYYY-MM-DD HH:MM:SS> format.
=cut
has date => (
is => 'rw',
isa => DateTimeObj,
coerce => DateTimeObj->coercion,
predicate => 'has_date',
);
=attr template
---
template: /blog/recipe.html
---
The path to a template override for this document. If set, the L<document
page|Statocles::Page::Document> will use this instead of the template provided
by the application.
The template path should not have the final extention (by default C<.ep>).
Different template parsers will have different extentions.
=cut
has template => (
is => 'rw',
isa => Maybe[ArrayRef[Str]],
coerce => sub {
return $_[0] if ref $_[0];
return [ grep { $_ ne '' } split m{/}, $_[0] ];
},
predicate => 'has_template',
);
=attr layout
---
layout: /site/layout-dark.html
---
The path to a layout template override for this document. If set, the L<document
page|Statocles::Page::Document> will use this instead of the layout provided
by the application.
The template path should not have the final extention (by default C<.ep>).
Different template parsers will have different extentions.
=cut
has layout => (
is => 'rw',
isa => Maybe[ArrayRef[Str]],
coerce => sub {
return $_[0] if ref $_[0];
return [ grep { $_ ne '' } split m{/}, $_[0] ];
},
predicate => 'has_layout',
);
=attr data
---
data:
ingredients:
- Eggs
- Milk
- Cheese
---
% for my $item ( @{ $self->data->{ingredients} } ) {
<%= $item %>
% }
A hash of extra data to attach to this document. This is available
immediately in the document content, and later in the page template.
Every document's content is parsed as a template. The C<data> attribute can be
used in the template to allow for some structured data that would be cumbersome
to have to mark up time and again.
=cut
has data => (
is => 'rw',
);
=attr disable_content_template
---
disable_content_template: true
---
This disables processing the content as a template. This can speed up processing
when the content is not using template directives.
This can be also set in the application
(L<Statocles::App/disable_content_template>), or for the entire site
(L<Statocles::Site/disable_content_template>).
=cut
has disable_content_template => (
is => 'ro',
isa => Bool,
lazy => 1,
default => 0,
predicate => 'has_disable_content_template',
);
around BUILDARGS => sub {
my ( $orig, $self, @args ) = @_;
my $args = $self->$orig( @args );
if ( defined $args->{data} && ref $args->{data} ne 'HASH' ) {
derp qq{Invalid data attribute in document "%s". Data attributes that are not hashes are deprecated and will be removed in v2.0. Please use a hash instead.},
$args->{path};
}
return $args;
};
=method parse_content
my $doc = $class->parse_content(
path => $path,
store => $store,
content => $content,
);
Construct a document the given content, with the given additional
attributes. Returns a new C<Statocles::Document> object.
This parses the YAML or JSON frontmatter into the document's attributes,
putting the rest of the file after the YAML or JSON frontmatter in the
C<content> attribute.
Custom document classes can override this method to change how file content is
parsed.
=cut
sub parse_content {
my ( $class, %args ) = @_;
my %doc;
my $content = delete $args{content} or die "Content is required";
my @lines = split /\n/, $content;
# YAML frontmatter
if ( @lines && $lines[0] =~ /^---/ ) {
shift @lines;
# The next --- is the end of the YAML frontmatter
my ( $i ) = grep { $lines[ $_ ] =~ /^---/ } 0..$#lines;
# If we did not find the marker between YAML and Markdown
if ( !defined $i ) {
die qq{Could not find end of YAML front matter (---)\n};
}
# Before the marker is YAML
eval {
%doc = %{ YAML::Load( join "\n", splice( @lines, 0, $i ), "" ) };
};
if ( $@ ) {
die qq{Error parsing YAML in "$args{path}"\n$@};
}
# Remove the last '---' mark
shift @lines;
}
# JSON frontmatter
elsif ( @lines && $lines[0] =~ /^{/ ) {
my $json;
if ( $lines[0] =~ /\}$/ ) {
# The JSON is all on a single line
$json = shift @lines;
}
else {
# The } on a line by itself is the last line of JSON
my ( $i ) = grep { $lines[ $_ ] =~ /^}$/ } 0..$#lines;
# If we did not find the marker between YAML and Markdown
if ( !defined $i ) {
die qq{Could not find end of JSON front matter (\})\n};
}
$json = join "\n", splice( @lines, 0, $i+1 );
}
eval {
%doc = %{ decode_json( $json ) };
};
if ( $@ ) {
die qq{Error parsing JSON: $@\n};
}
}
# The remaining lines are content
$doc{content} = join "\n", @lines, "";
delete $doc{path};
delete $doc{store};
return $class->new( %doc, %args );
}
=method deparse_content
my $content = $doc->deparse_content;
Deparse the document into a string ready to be stored in a file. This will
serialize the document attributes into YAML frontmatter, and place the content
after.
=cut
sub deparse_content {
my ( $self ) = @_;
my %data = %$self;
delete $data{ store };
delete $data{ path };
my $content = delete $data{content};
# Serialize date correctly
if ( exists $data{date} ) {
$data{date} = $data{date}->strftime('%Y-%m-%d %H:%M:%S');
}
# Don't save empty references
for my $hash_type ( qw( links images ) ) {
if ( exists $data{ $hash_type } && !keys %{ $data{ $hash_type } } ) {
delete $data{ $hash_type };
}
}
for my $array_type ( qw( tags ) ) {
if ( exists $data{ $array_type } && !@{ $data{ $array_type } } ) {
delete $data{ $array_type };
}
}
return YAML::Dump( \%data ) . "---\n". $content;
}
1;
__END__
=head1 DESCRIPTION
A Statocles::Document is the base unit of content in Statocles.
L<Applications|Statocles::App> take documents to build
L<pages|Statocles::Page>.
Documents are usually written as files, with the L<content|/content> in Markdown,
and the other attributes as frontmatter, a block of YAML at the top of the file.
An example file with frontmatter looks like:
---
title: My Blog Post
author: preaction
links:
stylesheet:
- href: /theme/css/extra.css
---
In my younger and more vulnerable years, my father gave me some
=head1 SEE ALSO
=over 4
=item L<Statocles::Help::Content>
The content guide describes how to edit content in Statocles sites, which are
represented by Document objects.
=back