├── Changes ├── .gitignore ├── t ├── 00-load.t ├── pod.t ├── manifest.t ├── pod-coverage.t └── boilerplate.t ├── MANIFEST ├── Makefile ├── test-dependencies.pl ├── lib ├── Minerl │ ├── BaseObject.pm │ ├── Formatter │ │ ├── Textile.pm │ │ ├── Perl.pm │ │ └── Markdown.pm │ ├── Template.pm │ ├── Util.pm │ ├── TemplateManager.pm │ ├── Page.pm │ └── PageManager.pm └── minerl.pm ├── minerl.pl └── README.md /Changes: -------------------------------------------------------------------------------- 1 | Revision history for minerl 2 | 3 | 0.01 2013-06-26/11:20 4 | First version, released on github. 5 | 0.02 2013-07-30/01:25 6 | Added support for textile in this release. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile.old 2 | Build 3 | Build.bat 4 | META.* 5 | MYMETA.* 6 | .build/ 7 | _build/ 8 | cover_db/ 9 | inc/ 10 | .lwpcookies 11 | .last_cover_stats 12 | nytprof.out 13 | pod2htm*.tmp 14 | pm_to_blib 15 | minerl-* 16 | minerl-*.tar.gz 17 | **.swp 18 | out/ 19 | minerl 20 | -------------------------------------------------------------------------------- /t/00-load.t: -------------------------------------------------------------------------------- 1 | #!perl -T 2 | use 5.006; 3 | use strict; 4 | use warnings FATAL => 'all'; 5 | use Test::More; 6 | 7 | plan tests => 1; 8 | 9 | BEGIN { 10 | use_ok( 'minerl' ) || print "Bail out!\n"; 11 | } 12 | 13 | diag( "Testing minerl $minerl::VERSION, Perl $], $^X" ); 14 | -------------------------------------------------------------------------------- /t/pod.t: -------------------------------------------------------------------------------- 1 | #!perl -T 2 | use 5.006; 3 | use strict; 4 | use warnings FATAL => 'all'; 5 | use Test::More; 6 | 7 | # Ensure a recent version of Test::Pod 8 | my $min_tp = 1.22; 9 | eval "use Test::Pod $min_tp"; 10 | plan skip_all => "Test::Pod $min_tp required for testing POD" if $@; 11 | 12 | all_pod_files_ok(); 13 | -------------------------------------------------------------------------------- /t/manifest.t: -------------------------------------------------------------------------------- 1 | #!perl -T 2 | use 5.006; 3 | use strict; 4 | use warnings FATAL => 'all'; 5 | use Test::More; 6 | 7 | unless ( $ENV{RELEASE_TESTING} ) { 8 | plan( skip_all => "Author tests not required for installation" ); 9 | } 10 | 11 | my $min_tcm = 0.9; 12 | eval "use Test::CheckManifest $min_tcm"; 13 | plan skip_all => "Test::CheckManifest $min_tcm required" if $@; 14 | 15 | ok_manifest(); 16 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | Changes 2 | lib/minerl.pm 3 | lib/Minerl/BaseObject.pm 4 | lib/Minerl/Formatter/Markdown.pm 5 | lib/Minerl/Formatter/Perl.pm 6 | lib/Minerl/Formatter/Textile.pm 7 | lib/Minerl/Page.pm 8 | lib/Minerl/PageManager.pm 9 | lib/Minerl/Template.pm 10 | lib/Minerl/TemplateManager.pm 11 | lib/Minerl/Util.pm 12 | minerl.pl 13 | test-dependencies.pl 14 | Makefile 15 | MANIFEST This list of files 16 | README 17 | t/00-load.t 18 | t/manifest.t 19 | t/pod-coverage.t 20 | t/pod.t 21 | -------------------------------------------------------------------------------- /t/pod-coverage.t: -------------------------------------------------------------------------------- 1 | #!perl -T 2 | use 5.006; 3 | use strict; 4 | use warnings FATAL => 'all'; 5 | use Test::More; 6 | 7 | # Ensure a recent version of Test::Pod::Coverage 8 | my $min_tpc = 1.08; 9 | eval "use Test::Pod::Coverage $min_tpc"; 10 | plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage" 11 | if $@; 12 | 13 | # Test::Pod::Coverage doesn't require a minimum Pod::Coverage version, 14 | # but older versions don't recognize some common documentation styles 15 | my $min_pc = 0.18; 16 | eval "use Pod::Coverage $min_pc"; 17 | plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage" 18 | if $@; 19 | 20 | all_pod_coverage_ok(); 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = minerl 2 | INSTALLBIN = /usr/local/bin 3 | 4 | TO_INST_PM = lib/Minerl/BaseObject.pm \ 5 | lib/Minerl/Formatter/Markdown.pm \ 6 | lib/Minerl/Formatter/Perl.pm \ 7 | lib/Minerl/Formatter/Textile.pm \ 8 | lib/Minerl/Page.pm \ 9 | lib/Minerl/PageManager.pm \ 10 | lib/Minerl/Template.pm \ 11 | lib/Minerl/TemplateManager.pm \ 12 | lib/Minerl/Util.pm \ 13 | lib/minerl.pm \ 14 | minerl.pl 15 | 16 | default: test-dependencies ${TO_INST_PM} 17 | echo "#!/usr/bin/perl -w" > ${NAME} 18 | echo "use strict;" >> ${NAME}; 19 | echo "use warnings;" >> ${NAME}; 20 | cat ${TO_INST_PM} >> ${NAME} 21 | chmod +x ${NAME} 22 | 23 | install: 24 | cp ${NAME} ${INSTALLBIN} 25 | 26 | test: 27 | prove -Ilib t/ 28 | 29 | clean: 30 | rm ${NAME} 31 | 32 | test-dependencies: 33 | test -f test-dependencies.pl && perl test-dependencies.pl 34 | -------------------------------------------------------------------------------- /test-dependencies.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | sub testDependencies { 5 | my @dependencies = qw( 6 | Config::IniFiles 7 | HTML::Template 8 | Text::Template 9 | Text::MultiMarkdown 10 | Text::Textile 11 | Getopt::Compact::WithCmd 12 | HTTP::Server::Brick 13 | ); 14 | 15 | my @modules_not_installed; 16 | for my $module (@dependencies) { 17 | eval("use $module;"); 18 | push @modules_not_installed, $module if $@; 19 | } 20 | 21 | my $install_cmd = "cpanm "; 22 | for my $module (@modules_not_installed) { 23 | print "warning: required module '$module' is not installed.\n"; 24 | $install_cmd .= ($module . " "); 25 | } 26 | if (@modules_not_installed) { 27 | print "You may use cpanm to install the modules:\n'$install_cmd'\n\n"; 28 | } else { 29 | print "All required modules are installed!\n"; 30 | } 31 | } 32 | 33 | testDependencies; 34 | -------------------------------------------------------------------------------- /lib/Minerl/BaseObject.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::BaseObject - the base object to be inherited from in the C project 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::BaseObject; 8 | our @ISA = qw(Minerl::BaseObject); 9 | 10 | =head1 DESCRIPTION 11 | 12 | This class implements a contructor that takes a C as parameter, so 13 | any other class that takes parameter as a C can inherit from it, which 14 | avoids duplicating the code 15 | 16 | =head1 AUTHOR 17 | 18 | neevek, C<< >> 19 | 20 | =head1 LICENSE AND COPYRIGHT 21 | 22 | Copyright 2013 neevek. 23 | 24 | This program is free software; you can redistribute it and/or modify it 25 | under the terms of the the Artistic License (2.0). You may obtain a 26 | copy of the full license at: 27 | 28 | L 29 | 30 | =cut 31 | 32 | package Minerl::BaseObject; 33 | 34 | sub new { 35 | my ($class, %args) = @_; 36 | 37 | my $self = bless {}, ref($class) || $class; 38 | 39 | $self->_init(%args); 40 | 41 | return $self; 42 | } 43 | 44 | sub _init { 45 | my ($self, %args) = @_; 46 | while (my ($key, $value) = each (%args)) { 47 | $self->{$key} = $value; 48 | } 49 | } 50 | 51 | 1; 52 | -------------------------------------------------------------------------------- /t/boilerplate.t: -------------------------------------------------------------------------------- 1 | #!perl -T 2 | use 5.006; 3 | use strict; 4 | use warnings FATAL => 'all'; 5 | use Test::More; 6 | 7 | plan tests => 3; 8 | 9 | sub not_in_file_ok { 10 | my ($filename, %regex) = @_; 11 | open( my $fh, '<:utf8', $filename ) 12 | or die "couldn't open $filename for reading: $!"; 13 | 14 | my %violated; 15 | 16 | while (my $line = <$fh>) { 17 | while (my ($desc, $regex) = each %regex) { 18 | if ($line =~ $regex) { 19 | push @{$violated{$desc}||=[]}, $.; 20 | } 21 | } 22 | } 23 | 24 | if (%violated) { 25 | fail("$filename contains boilerplate text"); 26 | diag "$_ appears on lines @{$violated{$_}}" for keys %violated; 27 | } else { 28 | pass("$filename contains no boilerplate text"); 29 | } 30 | } 31 | 32 | sub module_boilerplate_ok { 33 | my ($module) = @_; 34 | not_in_file_ok($module => 35 | 'the great new $MODULENAME' => qr/ - The great new /, 36 | 'boilerplate description' => qr/Quick summary of what the module/, 37 | 'stub function definition' => qr/function[12]/, 38 | ); 39 | } 40 | 41 | TODO: { 42 | local $TODO = "Need to replace the boilerplate text"; 43 | 44 | not_in_file_ok(README => 45 | "The README is used..." => qr/The README is used/, 46 | "'version information here'" => qr/to provide version information/, 47 | ); 48 | 49 | not_in_file_ok(Changes => 50 | "placeholder date/time" => qr(Date/time) 51 | ); 52 | 53 | module_boilerplate_ok('lib/minerl.pm'); 54 | 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /lib/Minerl/Formatter/Textile.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Formatter::Textile - Encapsulates C 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Formatter::Textile; 8 | my $formatter = new Minerl::Formatter::Textile(); 9 | $content = $formatter->format($$content); 10 | 11 | =head1 DESCRIPTION 12 | 13 | This class uses C to process content of pages 14 | 15 | =head1 AUTHOR 16 | 17 | neevek, C<< >> 18 | 19 | =head1 LICENSE AND COPYRIGHT 20 | 21 | Copyright 2013 neevek. 22 | 23 | This program is free software; you can redistribute it and/or modify it 24 | under the terms of the the Artistic License (2.0). You may obtain a 25 | copy of the full license at: 26 | 27 | L 28 | 29 | =head1 SUBROUTINES/METHODS 30 | 31 | =cut 32 | 33 | package Minerl::Formatter::Textile; 34 | { 35 | my $instance; 36 | my $textileInstance; 37 | 38 | =head2 39 | 40 | Constructor, which instantiates singleton of C 41 | 42 | =cut 43 | 44 | sub new { 45 | my $class = shift; 46 | 47 | if (!$instance) { 48 | $instance = bless {}, $class; 49 | 50 | my $useStr = "use Text::Textile;"; 51 | eval($useStr); 52 | $instance->{available} = !$@; 53 | 54 | $textileInstance = new Text::Textile if !$@; 55 | } 56 | 57 | warn "Warning: Text::Textile is not installed, Textile text will not be parsed." if !$instance->{available}; 58 | 59 | return $instance; 60 | } 61 | 62 | =head2 63 | 64 | Note: the C<$content> arguement is a B 65 | 66 | =cut 67 | 68 | sub format { 69 | my ($self, $content) = @_; 70 | return $textileInstance ? $textileInstance->process($$content) : $$content; 71 | } 72 | 73 | 1; 74 | } 75 | -------------------------------------------------------------------------------- /lib/Minerl/Formatter/Perl.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Formatter::Perl - Encapsulates C 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Formatter::Perl; 8 | my $formatter = new Minerl::Formatter::Perl(); 9 | $content = $formatter->format($$content); 10 | 11 | =head1 DESCRIPTION 12 | 13 | This class uses C to process content of pages 14 | 15 | =head1 AUTHOR 16 | 17 | neevek, C<< >> 18 | 19 | =head1 LICENSE AND COPYRIGHT 20 | 21 | Copyright 2013 neevek. 22 | 23 | This program is free software; you can redistribute it and/or modify it 24 | under the terms of the the Artistic License (2.0). You may obtain a 25 | copy of the full license at: 26 | 27 | L 28 | 29 | =head1 SUBROUTINES/METHODS 30 | 31 | =cut 32 | 33 | package Minerl::Formatter::Perl; 34 | { 35 | 36 | my $instance; 37 | 38 | =head2 39 | 40 | Constructor, which instantiates singleton of C 41 | 42 | =cut 43 | 44 | sub new { 45 | my $class = shift; 46 | 47 | if (!$instance) { 48 | $instance = bless {}, $class; 49 | 50 | my $useStr = "use Text::Template;"; 51 | eval($useStr); 52 | $instance->{available} = !$@; 53 | } 54 | 55 | return $instance; 56 | } 57 | 58 | =head2 59 | 60 | Note: the C<$content> arguement is a B 61 | 62 | =cut 63 | 64 | sub format { 65 | my ($self, $content, $data) = @_; 66 | 67 | if ($self->{available}) { 68 | my $txtTmpl = Text::Template->new( 69 | TYPE => "STRING", 70 | SOURCE => $$content, 71 | DELIMITERS => [ '{{', '}}' ] 72 | ); 73 | 74 | return $txtTmpl->fill_in( HASH => $data ); 75 | } 76 | return $$content; 77 | } 78 | 79 | 1; 80 | } 81 | -------------------------------------------------------------------------------- /lib/Minerl/Template.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Template - A class that encapsulates C 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Template; 8 | my $tmpl = new Minerl::Template( filename => $filename, name => $name ); 9 | $content = $tmpl->apply($content, $options); 10 | ... 11 | 12 | =head1 DESCRIPTION 13 | 14 | This class encapsulates C, and uses C to expand variables set 15 | in the templates and pages. 16 | 17 | =head1 AUTHOR 18 | 19 | neevek, C<< >> 20 | 21 | =head1 LICENSE AND COPYRIGHT 22 | 23 | Copyright 2013 neevek. 24 | 25 | This program is free software; you can redistribute it and/or modify it 26 | under the terms of the the Artistic License (2.0). You may obtain a 27 | copy of the full license at: 28 | 29 | L 30 | 31 | =cut 32 | 33 | package Minerl::Template; 34 | 35 | use HTML::Template; 36 | our @ISA = qw(Minerl::Page); 37 | 38 | sub build { 39 | my ($self) = @_; 40 | $self->{template} = HTML::Template->new_scalar_ref($self->content, die_on_bad_params => 0, loop_context_vars => 1); 41 | } 42 | 43 | sub apply { 44 | my ($self, $content, $options) = @_; 45 | 46 | my $tmpl = $self->{template}; 47 | 48 | $tmpl or die "Template '" . $self->{name} . "' not prepared, call build() first."; 49 | 50 | $tmpl->clear_params(); 51 | 52 | if (ref($options) eq "HASH") { 53 | $tmpl->param($options); 54 | } elsif (ref($options) eq "ARRAY") { 55 | foreach my $option (@$options) { 56 | if (ref($option) eq "HASH") { 57 | $tmpl->param($option); 58 | } 59 | } 60 | } 61 | $tmpl->param( content => $$content ); 62 | 63 | 64 | return \$tmpl->output(); 65 | } 66 | 67 | sub built { 68 | my ($self) = @_; 69 | return $self->{template} ? 1 : undef; 70 | } 71 | 72 | 1; 73 | -------------------------------------------------------------------------------- /lib/Minerl/Formatter/Markdown.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Formatter::Markdown - Encapsulates C 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Formatter::Markdown; 8 | my $formatter = new Minerl::Formatter::Markdown(); 9 | $content = $formatter->format($$content); 10 | 11 | =head1 DESCRIPTION 12 | 13 | This class uses C to process content of pages 14 | 15 | =head1 AUTHOR 16 | 17 | neevek, C<< >> 18 | 19 | =head1 LICENSE AND COPYRIGHT 20 | 21 | Copyright 2013 neevek. 22 | 23 | This program is free software; you can redistribute it and/or modify it 24 | under the terms of the the Artistic License (2.0). You may obtain a 25 | copy of the full license at: 26 | 27 | L 28 | 29 | =head1 SUBROUTINES/METHODS 30 | 31 | =cut 32 | 33 | package Minerl::Formatter::Markdown; 34 | { 35 | my $instance; 36 | my $markdownInstance; 37 | 38 | =head2 39 | 40 | Constructor, which instantiates singleton of C 41 | 42 | =cut 43 | 44 | sub new { 45 | my $class = shift; 46 | 47 | if (!$instance) { 48 | $instance = bless {}, $class; 49 | 50 | my $useStr = "use Text::MultiMarkdown;"; 51 | eval($useStr); 52 | $instance->{available} = !$@; 53 | 54 | $markdownInstance = Text::MultiMarkdown->new( 55 | empty_element_suffix => '>', 56 | tab_width => 4, 57 | use_wikilinks => 0, 58 | ) if !$@; 59 | } 60 | 61 | warn "Warning: Text::MultiMarkdown is not installed, Markdown text will not be parsed." if !$instance->{available}; 62 | 63 | return $instance; 64 | } 65 | 66 | =head2 67 | 68 | Note: the C<$content> arguement is a B 69 | 70 | =cut 71 | 72 | sub format { 73 | my ($self, $content) = @_; 74 | return $markdownInstance ? $markdownInstance->markdown($$content) : $$content; 75 | } 76 | 77 | 1; 78 | } 79 | -------------------------------------------------------------------------------- /lib/Minerl/Util.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Util - Utility class 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Util; 8 | my %hash; 9 | Minerl::Util::parsePageFile($filename, \%hash); 10 | ... 11 | 12 | =head1 DESCRIPTION 13 | 14 | This class includes some utility routines 15 | 16 | =head1 AUTHOR 17 | 18 | neevek, C<< >> 19 | 20 | =head1 LICENSE AND COPYRIGHT 21 | 22 | Copyright 2013 neevek. 23 | 24 | This program is free software; you can redistribute it and/or modify it 25 | under the terms of the the Artistic License (2.0). You may obtain a 26 | copy of the full license at: 27 | 28 | L 29 | 30 | =head1 SUBROUTINES/METHODS 31 | 32 | =cut 33 | package Minerl::Util; 34 | 35 | require Exporter; 36 | our @ISA = qw(Exporter); 37 | our @EXPORT = qw(parsePageFile); 38 | our @EXPORT_OK = qw(parsePageFile); 39 | 40 | use File::Basename; 41 | 42 | use constant { 43 | PAGE_PREREAD => 1, 44 | PAGE_READ_HEADER => 2, 45 | PAGE_READ_CONTENT => 3 46 | }; 47 | 48 | =head2 49 | 50 | this subroutine parses file that is composed of a header section and content section 51 | 52 | =cut 53 | 54 | sub parsePageFile { 55 | my ($filename, $hash) = @_; 56 | 57 | $hash = $hash || {}; 58 | my $content = ""; 59 | my $state = PAGE_PREREAD; 60 | open FILE, "<:utf8", $filename; 61 | while (my $line = ) { 62 | 63 | if ($line =~ /^-{3,}$/) { 64 | if ($state == PAGE_PREREAD) { 65 | $state = PAGE_READ_HEADER; 66 | next; # ignore the dashed line 67 | } 68 | if ($state == PAGE_READ_HEADER) { 69 | $state = PAGE_READ_CONTENT; 70 | next; # ignore the dashed line 71 | } 72 | } elsif ($state == PAGE_PREREAD && $line !~ /^-{3,}$/) { 73 | $state = PAGE_READ_CONTENT; 74 | } 75 | 76 | if ($state == PAGE_READ_HEADER) { 77 | # strip leading white spaces 78 | $line =~ s/^[ \t]+//g; 79 | 80 | # skip comments 81 | next if $line =~ /^#/; 82 | 83 | # strip trailing white spaces 84 | $line =~ s/[ \t\n]+$//g; 85 | my ($key, $value) = $line =~ '^([^:]+):[ \t]*(.*)$'; 86 | $hash->{headers}->{$key} = $value; 87 | } elsif ($state == PAGE_READ_CONTENT) { 88 | $content .= $line; 89 | } 90 | } 91 | 92 | $hash->{content} = $content; 93 | 94 | die "$filename: Header section is not closed." if $state == PAGE_READ_HEADER; 95 | close(FILE); 96 | 97 | return $hash; 98 | } 99 | 100 | 1; 101 | -------------------------------------------------------------------------------- /lib/Minerl/TemplateManager.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::TemplateManager - Manages all templates of the site 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Template; 8 | my $tm = new Minerl::TemplateManager(template_dir => $templateDir, template_suffix => $templateSuffix); 9 | my $html = $tm->applyTemplate("layout_name", "...content..." , [ {options1}, {options2}, ... ] ); 10 | ... 11 | 12 | =head1 DESCRIPTION 13 | 14 | This class reads all files with C under C as templates, 15 | these templates can be applied on the pages to generate rendered HTML pages. 16 | 17 | =head1 AUTHOR 18 | 19 | neevek, C<< >> 20 | 21 | =head1 LICENSE AND COPYRIGHT 22 | 23 | Copyright 2013 neevek. 24 | 25 | This program is free software; you can redistribute it and/or modify it 26 | under the terms of the the Artistic License (2.0). You may obtain a 27 | copy of the full license at: 28 | 29 | L 30 | 31 | =head1 SUBROUTINES/METHODS 32 | 33 | =cut 34 | package Minerl::TemplateManager; 35 | 36 | our @ISA = qw(Minerl::BaseObject); 37 | 38 | use File::Basename; 39 | 40 | sub new { 41 | my ($class, @args) = @_; 42 | my $self = $class->SUPER::new(@args); 43 | 44 | my $templateDir = $self->{template_dir}; 45 | my $templateSuffix = $self->{template_suffix}; 46 | 47 | $self->_initTemplates($templateDir, $templateSuffix); 48 | 49 | return $self; 50 | } 51 | 52 | sub _initTemplates { 53 | my ($self, $templateDir, $templateSuffix) = @_; 54 | 55 | -d $templateDir or die "$templateDir: $!"; 56 | my @files = glob($templateDir . "/*" . $templateSuffix); 57 | 58 | my $tmplHashes = $self->{templates} = {}; 59 | 60 | foreach my $filename (@files) { 61 | #print "found template file: $filename\n" if $self->{DEBUG}; 62 | 63 | # basename without suffix 64 | my ($name) = basename($filename) =~ /([^.]+)/; 65 | $tmplHashes->{$name} = new Minerl::Template( filename => $filename, name => $name ); 66 | } 67 | 68 | while (my ($tmplName, $tmpl) = each %$tmplHashes) { 69 | $tmpl->build(); 70 | } 71 | } 72 | 73 | =head2 74 | 75 | Applies the templates recursively on the content. that we need recursion is because 76 | templates(or layouts) can be inherited/extended. 77 | 78 | =cut 79 | 80 | sub applyTemplate { 81 | my ($self, $tmplName, $content, $options) = @_; 82 | 83 | $content = $self->_applyTemplateRecursively($tmplName, $content, $options); 84 | 85 | return $self->_prettyPrintAvailable ? $self->_prettyPrint($content) : $content; 86 | } 87 | 88 | sub _applyTemplateRecursively { 89 | my ($self, $tmplName, $content, $options) = @_; 90 | 91 | my $tmpl = $self->{templates}->{$tmplName}; 92 | $tmpl or die "Template not found: $tmplName"; 93 | 94 | $content = $tmpl->apply($content, $options); 95 | my $baseTmplName = $tmpl->header("layout"); 96 | if ($baseTmplName) { 97 | return $self->_applyTemplateRecursively($baseTmplName, $content, $options); 98 | } 99 | return $content; 100 | } 101 | 102 | sub _prettyPrintAvailable { 103 | my ($self) = @_; 104 | return 0; # pretty print is not used currently, because it is slow 105 | 106 | my $useStr = " 107 | use HTML::HTML5::Parser qw(); 108 | use HTML::HTML5::Writer qw(); 109 | use XML::LibXML::PrettyPrint qw(); 110 | "; 111 | 112 | eval($useStr); 113 | 114 | return $@ ? undef : 1; 115 | } 116 | 117 | sub _prettyPrint { 118 | my ($self, $content) = @_; 119 | 120 | return \(HTML::HTML5::Writer->new( 121 | start_tags => 'force', 122 | end_tags => 'force', 123 | )->document( 124 | XML::LibXML::PrettyPrint->new_for_html( 125 | indent_string => "\t" 126 | )->pretty_print( 127 | HTML::HTML5::Parser->new->parse_string( $content ) 128 | ) 129 | )); 130 | } 131 | 132 | 1; 133 | -------------------------------------------------------------------------------- /lib/Minerl/Page.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::Page - A Page encapsulates a file, either a page or a template 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::Page; 8 | my $page = new Minerl::Page( filename => "test.md", name => "test" ); 9 | $page->headers(); 10 | $page->content(); 11 | $page->outputFilename(); 12 | ... 13 | 14 | =head1 DESCRIPTION 15 | 16 | This class encapsulates a file, which contains a header section and a 17 | content section, the header section is surrounded with 3 or more dashes. 18 | 19 | =head1 AUTHOR 20 | 21 | neevek, C<< >> 22 | 23 | =head1 LICENSE AND COPYRIGHT 24 | 25 | Copyright 2013 neevek. 26 | 27 | This program is free software; you can redistribute it and/or modify it 28 | under the terms of the the Artistic License (2.0). You may obtain a 29 | copy of the full license at: 30 | 31 | L 32 | 33 | =cut 34 | 35 | package Minerl::Page; 36 | 37 | our @ISA = qw(Minerl::BaseObject); 38 | 39 | use File::Basename; 40 | 41 | =head1 SUBROUTINES/METHODS 42 | 43 | =head2 new 44 | 45 | The contstructor takes a filename as parameter, and then it uses 46 | C to parse the file, the headers and content 47 | will be correctly set in the C<$self> HASH when the method returns. 48 | 49 | =cut 50 | 51 | sub new { 52 | my ($class, @args) = @_; 53 | my $self = $class->SUPER::new(@args); 54 | 55 | my $filename = $self->{filename}; 56 | 57 | $filename or die "Must pass in filename of the page."; 58 | Minerl::Util::parsePageFile($filename, $self); 59 | 60 | return $self; 61 | } 62 | 63 | sub header { 64 | my ($self, $key) = @_; 65 | return $self->{headers}->{$key}; 66 | } 67 | 68 | sub headers { 69 | my ($self) = @_; 70 | return $self->{headers}; 71 | } 72 | 73 | =head2 content 74 | 75 | Returns the content of the page, when C<$limit> is not empty, it is used 76 | as a restriction to limit the number of characters to te returned 77 | 78 | =cut 79 | 80 | sub content { 81 | my ($self, $limit) = @_; 82 | if (!$limit) { 83 | return \$self->{content}; 84 | } else { 85 | my $content = \$self->{content}; 86 | if (length $$content > $limit) { 87 | return \substr($$content, 0, $limit); 88 | } 89 | return $content; 90 | } 91 | } 92 | 93 | =head2 applyFormatter 94 | 95 | Applies the supplied formatter on the content, makes variables in the headers 96 | available to the formatter. 97 | 98 | =cut 99 | 100 | sub applyFormatter { 101 | my ($self, $formatter) = @_; 102 | $self->{content} = $formatter->format( \$self->{content}, $self->headers() ); 103 | } 104 | 105 | =head2 formats 106 | 107 | Gets all the formats that are specified by the C field in the header 108 | 109 | =cut 110 | 111 | sub formats { 112 | my ($self) = @_; 113 | my $formatHeader = $self->header("format"); 114 | 115 | return $formatHeader ? [split "[ \t]*,[ \t]*", $formatHeader] : undef; 116 | } 117 | 118 | =head2 ctxVars 119 | 120 | C is a HASH that stores some context variables of the page, such as 121 | C<__post_title>, C<__post_createdate> etc. these values are set in C 122 | 123 | =cut 124 | 125 | sub ctxVars { 126 | my ($self, $ctxVars) = @_; 127 | $self->{ctx_vars} = $ctxVars if $ctxVars; 128 | return $self->{ctx_vars}; 129 | } 130 | 131 | sub ctxVar { 132 | my ($self, $key) = @_; 133 | return $self->ctxVar($key); 134 | } 135 | 136 | =head2 outputFilename 137 | 138 | Content of this page, after being applied a template, will be output to C 139 | with this filename. 140 | 141 | =cut 142 | 143 | sub outputFilename { 144 | my ($self, $designatedName) = @_; 145 | 146 | my $outputFilename = $self->{output_filename}; 147 | return $outputFilename if $outputFilename; 148 | 149 | $outputFilename = $self->{filename}; 150 | 151 | # strip the first dirname, which is the root directory(raw_dir) of the page 152 | $outputFilename =~ s|^[^/]*/||g; 153 | 154 | my $dir = dirname($outputFilename); 155 | 156 | if ($designatedName) { 157 | if ($dir) { 158 | return "$dir/$designatedName"; 159 | } else { 160 | return $designatedName; 161 | } 162 | } else { 163 | my $slug = $self->header("slug"); 164 | if ($slug && $dir ne ".") { 165 | $slug = "$dir/$slug"; 166 | } 167 | return $slug if $slug; 168 | 169 | $outputFilename = lc $self->header("title"); 170 | $outputFilename or die "Post does not contain a title header: " . $self->{filename}; 171 | 172 | $outputFilename =~ s/[^a-z]/ /ig; # replace all non-A-to-Z characters with whitespace 173 | $outputFilename =~ s/^[ \t]+//g; # trim left 174 | $outputFilename =~ s/[ \t]+$//g; # trim right 175 | $outputFilename =~ s/[ \t]+/-/g; # replace all whitespaces with dashes 176 | 177 | $outputFilename = $outputFilename . ".html"; 178 | $outputFilename = "$dir/$outputFilename" if $dir ne "."; 179 | } 180 | 181 | return $self->{output_filename} = $outputFilename; 182 | } 183 | 184 | 1; 185 | -------------------------------------------------------------------------------- /minerl.pl: -------------------------------------------------------------------------------- 1 | package main; 2 | use strict; 3 | use warnings; 4 | 5 | our $VERSION = 0.03; 6 | 7 | use File::Path qw(make_path); 8 | use Getopt::Compact::WithCmd; 9 | 10 | my $go = Getopt::Compact::WithCmd->new( 11 | command_struct => { 12 | "build" => { 13 | options => [[[qw(r rebuild)], qq(Rebuild all the pages), "!", undef, { default => 0}], 14 | [[qw(v verbose)], qq(Print details), "!", undef, { default => 0 }] 15 | ], 16 | args => "[-r] [-v] [-h]", 17 | desc => "- Applies the templates on the pages, generates the final HTML pages", 18 | other_usage => "" 19 | }, 20 | "serve" => { 21 | options => [[[qw(p port)], qq(The port which the HTTP server listens on), "=i", undef, { default => 8888 }]], 22 | args => "[-p port]", 23 | desc => "- Starts an HTTP server to serve the directory specified by the 'output_dir' property in minerl.cfg", 24 | other_usage => "" 25 | }, 26 | "createpost" => { 27 | options => [[[qw(f filename)], qq(File name of the page), "=s", undef, { required => 1 }], 28 | [[qw(l layout)], qq(Layout on which the newly created page is to be applied), "=s", undef, { required => 1 }], 29 | [[qw(m format)], qq(Format of the page, currently supports 'html, markdown, textile, perl'), ":s", undef, { default => "html" }], 30 | [[qw(g tags)], qq(Tags for the post, separated by commas), ":s", undef, { default => "uncategorized" }], 31 | [[qw(t title)], qq(Title of the post), ":s", undef, { default => "untitled" }], 32 | [[qw(d subdir)], qq(The subdir to put the newly created post), ":s", undef, { default => "" }], 33 | ], 34 | args => "<-f filename> <-l layout> [-m format] [-g tags] [-t title]", 35 | desc => "- Creates the skeleton of a new post", 36 | other_usage => "Example:\n\tminerl createpost -f my-first-post-of-the-day.md -l post -m markdown -g \"perl, minerl\" -d posts -t \"Hello World\"\n\n\n\tIf the -d or --subdir option is absent, the newly created post is put directly under 'page_dir' set in minerl.cfg", 37 | }, 38 | "generate" => { 39 | options => [[[qw(d dirname)], qq(The directory name to the site to be created), "=s", undef, { required => 1 }]], 40 | args => "<-d dirname>", 41 | desc => "- Creates a brand new Minerl site", 42 | other_usage => "Example:\n\tminerl generate -d mysite" 43 | }, 44 | }, 45 | name => "minerl" 46 | ); 47 | 48 | 49 | my $command = $go->command; 50 | my $opts = $go->opts; 51 | $go->show_usage if !$command; 52 | 53 | if ($command eq 'generate') { 54 | my $dirname = $opts->{dirname}; 55 | !-d $dirname or print "$dirname already exists\n" and exit 0; 56 | make_path($dirname, { mode => 0755 }); 57 | 58 | make_path("$dirname/_templates", { mode => 0755 }); 59 | make_path("$dirname/_pages", { mode => 0755 }); 60 | make_path("$dirname/_raw", { mode => 0755 }); 61 | 62 | createDefaultConfigurationFile("$dirname/minerl.cfg"); 63 | createDefaultLayout("$dirname/_templates/default.html"); 64 | createDefaultPage('default', "$dirname/_pages/index.html"); 65 | 66 | exit 0; 67 | } 68 | 69 | my $minerl = new minerl( cfg_file => "minerl.cfg" ); 70 | 71 | if ($command eq 'build') { 72 | $minerl->build($opts->{verbose}); 73 | } elsif ($command eq 'serve') { 74 | use HTTP::Server::Brick; 75 | my $server = HTTP::Server::Brick->new( host => "localhost", port => $opts->{port}); 76 | $server->mount("/" => {"path" => $minerl->{cfg}->{system}->{output_dir}}); 77 | $server->start() 78 | } elsif ($command eq 'createpost') { 79 | use POSIX qw(strftime); 80 | my $timestamp = time; 81 | my ($date) = strftime("%F %T", localtime $timestamp) =~ /([^ ]+) (.+)$/; 82 | 83 | my $filename = $opts->{filename}; 84 | my $layout = $opts->{layout}; 85 | my $format = $opts->{format}; 86 | my $tags = $opts->{tags}; 87 | my $title = $opts->{title}; 88 | my $subdir = $opts->{subdir}; 89 | 90 | my $headers = "---\n" 91 | . "title: $title\n" 92 | . "layout: $layout\n" 93 | . "format: $format\n" 94 | . "type: post\n" 95 | . "tags: $tags\n" 96 | . "timestamp: $timestamp\n" 97 | . "---\n\n"; 98 | 99 | my $pageDir = $minerl->{cfg}->{system}->{page_dir} . "/$subdir/$date"; 100 | $pageDir =~ s,-|/+,/,g; 101 | make_path($pageDir, { mode => 0755 }); 102 | 103 | my $finalFilePath = "$pageDir/$filename"; 104 | 105 | if (-f $finalFilePath) { 106 | print "$finalFilePath exists, override it? \n"; 107 | my $answer = ; 108 | chomp $answer; 109 | if (lc $answer ne "y") { 110 | exit 0; 111 | } 112 | } 113 | 114 | open my $fh, ">:utf8", $finalFilePath or die "Failed to write to '$finalFilePath' - $!"; 115 | binmode($fh, ":utf8"); 116 | print $fh $headers; 117 | close $fh; 118 | 119 | print "Created: $finalFilePath\n"; 120 | } 121 | 122 | sub createDefaultConfigurationFile { 123 | my ($cfgFile) = @_; 124 | 125 | my $defaultConfigurations = <:utf8", $cfgFile; 167 | print $cfgFh $defaultConfigurations; 168 | close $cfgFh; 169 | } 170 | 171 | sub createDefaultLayout{ 172 | my ($layoutFilename) = @_; 173 | 174 | my $content = < 176 | 177 | 178 | 179 | <TMPL_VAR title> 180 | 198 | 199 | 200 |
201 | 202 |
203 | 204 | 205 | HTML 206 | # END HTML 207 | 208 | open my $fh, ">:utf8", $layoutFilename; 209 | print $fh $content; 210 | close $fh; 211 | } 212 | 213 | sub createDefaultPage{ 214 | my ($layout, $pageFilename) = @_; 215 | 216 | my $content = <:utf8", $pageFilename; 229 | print $fh $content; 230 | close $fh; 231 | } 232 | 233 | 1; 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | minerl 2 | ====== 3 | 4 | **minerl** is a blog-aware static site generator written in Perl, it supports *tagging*, *automatic archiving*, *post*, *page* and *layout inheritance*. 5 | 6 | *You can read the introduction in the following or read it in the [first site generated by minerl](http://neevek.net/posts/2013/06/27/introduction-to-minerl.html).* 7 | 8 | Installation 9 | ============ 10 | 11 | Before installation, make sure you have installed all modules required by **minerl**, which depends on the following modules: 12 | 13 | Config::IniFiles 14 | HTML::Template 15 | Text::Template 16 | Text::MultiMarkdown 17 | Text::Textile 18 | Getopt::Compact::WithCmd 19 | HTTP::Server::Brick 20 | 21 | I recommend [cpanm](https://metacpan.org/dist/App-cpanminus/view/lib/App/cpanminus/fatscript.pm) for installing modules. 22 | 23 | curl -L https://cpanmin.us | perl - --sudo App::cpanminus 24 | 25 | After cpanm is installed, use the following command to install all required modules, note you may need root permission if the modules are to be installed in system directory: 26 | 27 | cpanm Config::IniFiles HTML::Template Text::Template Text::MultiMarkdown \ 28 | Text::Textile Getopt::Compact::WithCmd HTTP::Server::Brick 29 | 30 | Okay, all prerequisites are met, we are ready to install and try out **minerl**. It is that straightforward, simply clone the code from github, change direcotry to the root of the project, `make && make install` installs the **minerl** script under `/usr/local/bin`: 31 | 32 | git clone https://github.com/neevek/minerl.git 33 | cd minerl 34 | make && make install 35 | 36 | Now generate your first minerl site: 37 | 38 | minerl genearte -d mysite 39 | cd mysite 40 | minerl build -v 41 | minerl serve 42 | 43 | You may have already seen the output of the commands, navigate your browser to `http://127.0.0.1:8888`. Cool! You have just created the first page of your minerl site. 44 | 45 | Structure 46 | ========= 47 | 48 | mysite/ 49 | ├── _pages 50 | │   └── index.html 51 | ├── _raw 52 | ├── _templates 53 | │   └── default.html 54 | └── minerl.cfg 55 | 56 | Page files are put in the `_pages` directory, layout files are put in the `_templates` directory, resource files(images/js/css) are put in the `_raw` directory, these directories are all specified in the `minerl.cfg` configuration file. 57 | 58 | Implemented Commands 59 | ==================== 60 | 61 | minerl v0.02 62 | usage: minerl [options] COMMAND 63 | 64 | options: 65 | -h, --help This help message 66 | 67 | Implemented commands are: 68 | build - Applies the templates on the pages, generates the final HTML pages 69 | createpost - Creates the skeleton of a new post 70 | generate - Creates a brand new minerl site 71 | serve - Starts an HTTP server to serve the directory specified by the 'output_dir' property in minerl.cfg 72 | 73 | See 'minerl help COMMAND' for more information on a specific command. 74 | 75 | Currently 4 commands are implemented, for more information, run `minerl help COMMAND`. 76 | 77 | What can minerl do for you 78 | ========================== 79 | 80 | Now that you have installed **minerl**, you may want to know more about it, and see what it can do for you. In the begining I mentioned that **minerl** supports *tagging* and *automatic archiving*, which sounds unclear, what is that? 81 | 82 | **tagging** per se is not interesting at all, the fun part is that after you tag your blog posts, **minerl** will organize all your posts and group them by tags, so that you can create index pages listing posts for each tag, which is cool for a pure static site. **automatic archiving** works in the same way, it organizes all your posts and group them by months, you can create index pages for all months as you would for tags. **automatic archiving** requires the *timestamp* header in every post to work. I recommend you always use `minerl createpost` to create the skeleton of a new post, which sets the *timestamp* as well as a few other headers for you. 83 | 84 | Note: when I am talking about **posts**, I mean pages of type **post**, which is set in the header section. 85 | 86 | Templates 87 | ========= 88 | 89 | A template file is composed of a header section and a content/body section, header section starts and ends with 3 or more dashes. minerl uses `HTML::Template` to expand variables in template files, template files can be inherited, which makes HTML structure design a lot easier even if you have many pages. A template inherits another by specifying the `layout` header and name the inherited template without suffix, like this: 90 | 91 | --- 92 | layout: default 93 | --- 94 | 95 | ### Caveats 96 | 97 | 98 | - When a template is designed to be inherited(like `default.html` in the demo created by `minerl generate`), it MUST contain a variable called `content`, like this: ``. 99 | 100 | Pages 101 | ===== 102 | 103 | Format of pages are the same as that of template files, a header section and a content/body section, the only difference is that you may always need to set more headers for pages. Let's take an example of this post, which contains the following headers: 104 | 105 | - title: Introduction to minerl 106 | - layout: post 107 | - format: markdown 108 | - type: post 109 | - tags: minerl, perl 110 | - slug: introduction-to-minerl.html 111 | - timestamp: 1372332934 112 | 113 | For a normal page, the `type`, `tags` and `timestamp` headers are not needed. `slug` is used as the final output file name of the page, if it is absent, `title` will be used instead with whitespaces replaced with dahses. 114 | 115 | Builtin variables 116 | ================= 117 | 118 | **minerl** offers quite a few builtin variables that can be used to generate index pages for tags and archives. Builtin variables can be referenced in templates with [HTML::Template](http://search.cpan.org/~wonko/HTML-Template-2.94/lib/HTML/Template.pm) syntax. 119 | 120 | **minerl** offers the following builtin variables: 121 | 122 | - `__minerl_all_posts` - ARRAY, used in LOOP, available in all templates 123 | - `__minerl_recent_posts` - ARRAY, used in LOOP, available in all templates 124 | - `__minerl_archived_months` - ARRAY, used in LOOP, available in all templates 125 | - `__minerl_archived_posts` - ARRAY, used in LOOP, available in templates that are designed to be applied on pages of type `archive` 126 | - `__minerl_cur_month` - string SCALAR, available in templates that are designed to be applied on pages of type `archive` 127 | - `__minerl_all_tags` - ARRAY, used in LOOP, available in all templates 128 | - `__minerl_tagged_posts` - ARRAY, used in LOOP, available in templates that are designed to be applied on pages of type `taglist` 129 | - `__minerl_cur_tag` - string SCALAR, available in templates that are designed to be applied on pages of type `taglist` 130 | 131 | The following builtin variables(string SCALAR) are only available in templates that are designed to be applied on pages of type `post`: 132 | 133 | - `__post_timestamp` 134 | - `__post_title` 135 | - `__post_link` 136 | - `__post_createdate` 137 | - `__post_createtime` 138 | - `__post_tags` 139 | - `__post_content` 140 | - `__post_excerpt` 141 | 142 | Besides the above variables, all user defined variables in page headers are available in all templates. 143 | 144 | ### Examples 145 | 146 | The following code uses the `__minerl_all_posts` variable to list all posts of the site: 147 | 148 |
    149 | 150 |
  • 151 |
    152 | ... 153 |
    154 |
    155 |
156 | 157 | The following code uses the `__minerl_tagged_posts` variable to list all posts of a certain tag: 158 | 159 |
    160 | 161 |
  • 162 |
    163 | ... 164 |
    165 |
    166 |
167 | 168 | Formats 169 | ======= 170 | 171 | Currently **minerl** supports [markdown](http://search.cpan.org/~bobtfish/Text-MultiMarkdown-1.000034/lib/Text/MultiMarkdown.pm), [textile](http://search.cpan.org/~bchoate/Text-Textile-2.12/lib/Text/Textile.pm) and Perl script, and of course, plain text/HTML. 172 | 173 | LICENSE AND COPYRIGHT 174 | ===================== 175 | 176 | Copyright 2013 neevek. 177 | 178 | This program is free software; you can redistribute it and/or modify it 179 | under the terms of the the Artistic License (2.0). You may obtain a 180 | copy of the full license at: 181 | 182 | [http://www.perlfoundation.org/artistic_license_2_0](http://www.perlfoundation.org/artistic_license_2_0) 183 | 184 | Any use, modification, and distribution of the Standard or Modified 185 | Versions is governed by this Artistic License. By using, modifying or 186 | distributing the Package, you accept this license. Do not use, modify, 187 | or distribute the Package, if you do not accept this license. 188 | 189 | If your Modified Version has been derived from a Modified Version made 190 | by someone other than you, you are nevertheless required to ensure that 191 | your Modified Version complies with the requirements of this license. 192 | 193 | This license does not grant you the right to use any trademark, service 194 | mark, tradename, or logo of the Copyright Holder. 195 | 196 | This license includes the non-exclusive, worldwide, free-of-charge 197 | patent license to make, have made, use, offer to sell, sell, import and 198 | otherwise transfer the Package with respect to any patent claims 199 | licensable by the Copyright Holder that are necessarily infringed by the 200 | Package. If you institute patent litigation (including a cross-claim or 201 | counterclaim) against any party alleging that the Package constitutes 202 | direct or contributory patent infringement, then this Artistic License 203 | to you shall terminate on the date that such litigation is filed. 204 | 205 | Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER 206 | AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. 207 | THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 208 | PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY 209 | YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR 210 | CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR 211 | CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, 212 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 213 | -------------------------------------------------------------------------------- /lib/Minerl/PageManager.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Minerl::PageManager - Manages all pages under C 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Minerl::PageManager; 8 | my $pm = new Minerl::PageManager( page_dir => $pageDir, page_suffix_regex => $pageSuffixRegex); 9 | $pm->pages(); 10 | $pm->posts(); 11 | ... 12 | 13 | =head1 DESCRIPTION 14 | 15 | This class manages all the pages in C, it processes all 16 | the pages and prepares some of the builiin variables for template 17 | files, such as C<__post_title>, C<__post_link>, etc. 18 | 19 | =head1 AUTHOR 20 | 21 | neevek, C<< >> 22 | 23 | =head1 LICENSE AND COPYRIGHT 24 | 25 | Copyright 2013 neevek. 26 | 27 | This program is free software; you can redistribute it and/or modify it 28 | under the terms of the the Artistic License (2.0). You may obtain a 29 | copy of the full license at: 30 | 31 | L 32 | 33 | =head1 SUBROUTINES/METHODS 34 | 35 | =cut 36 | 37 | package Minerl::PageManager; 38 | 39 | our @ISA = qw(Minerl::BaseObject); 40 | 41 | use File::Basename; 42 | use File::Find qw(find); 43 | use File::stat; 44 | use POSIX; 45 | 46 | sub new { 47 | my ($class, @args) = @_; 48 | my $self = $class->SUPER::new(@args); 49 | 50 | my $pageDir = $self->{page_dir}; 51 | my $pageSuffixRegex = $self->{page_suffix_regex}; 52 | 53 | $self->_initPages($pageDir, $pageSuffixRegex); 54 | 55 | return $self; 56 | } 57 | 58 | =head2 _initPages 59 | 60 | Searches C for files that match C, appplies 61 | formatters on the content of the pages, prepares some builtin variables, 62 | which will be made available to template files. 63 | 64 | =cut 65 | 66 | sub _initPages { 67 | my ($self, $pageDir, $pageSuffixRegex) = @_; 68 | 69 | my $pageArr = $self->{pages} = []; 70 | 71 | my @postArr; 72 | 73 | -d $pageDir or die "$pageDir: Directory does not exist."; 74 | 75 | my $taggedPosts = $self->{tagged_posts} = {}; 76 | my $archivedPosts = $self->{archived_posts} = {}; 77 | 78 | # this hash is used to sort the date 79 | my $archivedMonths = $self->{archived_months} = {}; 80 | 81 | find( { wanted => sub { 82 | if ( -f $_ ) { 83 | return if $_ !~ /$pageSuffixRegex/; 84 | 85 | #print "found page file: $pageDir/$filename\n" if $self->{DEBUG}; 86 | 87 | # basename without suffix 88 | my ($name) = basename($_) =~ /([^.]+)/; 89 | my $page = new Minerl::Page( filename => $_, name => $name ); 90 | 91 | $page->header("layout") or die "$_: 'layout' header is not specified."; 92 | 93 | # applies formatters on all pages 94 | my $formats = $page->formats(); 95 | map { 96 | my $formatter = $self->_obtainFormatter($_); 97 | $page->applyFormatter($formatter) if $formatter 98 | } @$formats if $formats; 99 | 100 | push @$pageArr, $page; 101 | 102 | my $pageType = $page->header("type"); 103 | 104 | # only for pages of 'post' type do we need to extract some properties 105 | if ($pageType && $pageType eq "post") { 106 | my @postTags; 107 | 108 | my @tags; 109 | # if any tags were specified, extract them and put them in the array 110 | if ($page->header("tags")) { 111 | @tags = split /[ \t]*,[ \t]*/, $page->header("tags"); 112 | @tags = grep { $_ } @tags; 113 | 114 | foreach my $t (@tags) { 115 | push @postTags, { __minerl_tag_name => $t, __minerl_tag_link => "/tags/$t.html" }; 116 | } 117 | } 118 | 119 | my $excerpt = ${$page->content(300)}; 120 | $excerpt =~ s/<[^>]+>//g; 121 | $excerpt =~ s/<.*//g; 122 | 123 | # generates the create timestamp for the page if it is absent in the header 124 | $page->{headers}->{timestamp} = stat($_)->ctime if !$page->header("timestamp"); 125 | 126 | 127 | 128 | # setup builtin variables 129 | my $post= { 130 | __post_timestamp => $page->header("timestamp"), # this is for sorting 131 | __post_title => $page->header("title"), 132 | __post_link => "/" . $page->outputFilename(), 133 | __post_createdate => POSIX::strftime("%b %d, %Y", localtime($page->header("timestamp"))), 134 | __post_createtime => POSIX::strftime("%I:%M %p", localtime($page->header("timestamp"))), 135 | __post_tags => \@postTags, 136 | __post_content => ${$page->content()}, 137 | __post_excerpt => $excerpt, 138 | }; 139 | 140 | # save these builtin variables in the context of the page 141 | $page->ctxVars($post); 142 | 143 | push @postArr, $post; 144 | 145 | # categorize the posts by tags 146 | foreach my $t (@tags) { 147 | $t = lc $t; 148 | 149 | my $postsByTag = $taggedPosts->{$t}; 150 | if (!$postsByTag) { 151 | push @$postsByTag, $post; 152 | $taggedPosts->{$t} = $postsByTag; 153 | } else { 154 | push @$postsByTag, $post; 155 | } 156 | } 157 | 158 | # sort in-pace the posts 159 | while (my ($tag, $postArrRef) = each %$taggedPosts) { 160 | @$postArrRef = sort { $b->{__post_timestamp} <=> $a->{__post_timestamp} } @$postArrRef; 161 | } 162 | 163 | # group the posts by month 164 | my $monthAsKey = POSIX::strftime("%b, %Y", localtime($page->header("timestamp"))); 165 | $archivedMonths->{$monthAsKey} = POSIX::strftime("%Y/%m", localtime($page->header("timestamp"))); 166 | my $postsByMonth = $archivedPosts->{$monthAsKey}; 167 | if (!$postsByMonth) { 168 | push @$postsByMonth, $post; 169 | $archivedPosts->{$monthAsKey} = $postsByMonth; 170 | } else { 171 | push @$postsByMonth, $post; 172 | } 173 | 174 | # sort in-pace the posts 175 | while (my ($month, $postArrRef) = each %$archivedPosts) { 176 | @$postArrRef = sort { $b->{__post_timestamp} <=> $a->{__post_timestamp} } @$postArrRef; 177 | } 178 | } 179 | } 180 | }, no_chdir => 1 }, ($pageDir) ); 181 | 182 | # sort the posts by createtime 183 | @postArr = sort { $b->{__post_timestamp} <=> $a->{__post_timestamp} } @postArr; 184 | $self->{posts} = \@postArr; 185 | } 186 | 187 | =head2 _obtainFormatter 188 | 189 | Obtains a formatter with a name 190 | 191 | =cut 192 | 193 | sub _obtainFormatter { 194 | my ($self, $name) = @_; 195 | my $formatterHash = $self->{formatters}; 196 | if (!$formatterHash) { 197 | $formatterHash->{markdown} = new Minerl::Formatter::Markdown(); 198 | $formatterHash->{perl} = new Minerl::Formatter::Perl(); 199 | $formatterHash->{textile} = new Minerl::Formatter::Textile(); 200 | } 201 | 202 | if (defined $formatterHash->{$name}) { 203 | return $formatterHash->{$name}; 204 | } else { 205 | warn "formatter not supported: $name" unless $name eq 'html'; 206 | } 207 | } 208 | 209 | =head2 pages 210 | 211 | Gets all pages of any type(specified in the header section of the page) 212 | 213 | =cut 214 | 215 | sub pages { 216 | my ($self) = @_; 217 | return $self->{pages}; 218 | } 219 | 220 | =head2 posts 221 | 222 | Gets an ARRAY of all or C<$limit> count of posts, each post is 223 | a HASH, the HASH contains builtin variables of the post, which 224 | will be made available to template files. 225 | 226 | =cut 227 | 228 | sub posts { 229 | my ($self, $limit) = @_; 230 | if (!$limit) { 231 | return $self->{posts}; 232 | } else { 233 | my $posts = $self->{posts}; 234 | if (scalar @$posts > $limit) { 235 | my @slice = @$posts[0..$limit-1]; 236 | return \@slice; 237 | } 238 | return $posts; 239 | } 240 | } 241 | 242 | =head2 tags 243 | 244 | Gets an ARRAY of tags from all posts, the ARRAY contains builtin 245 | variables of the page 246 | 247 | =cut 248 | 249 | sub tags { 250 | my ($self) = @_; 251 | my $taggedPosts = $self->{tagged_posts}; 252 | my @keys = keys %$taggedPosts if $taggedPosts; 253 | return \@keys; 254 | } 255 | 256 | =head2 postsByTag 257 | 258 | Gets all posts of the specified tag 259 | 260 | =cut 261 | 262 | sub postsByTag { 263 | my ($self, $tag) = @_; 264 | my $taggedPosts = $self->{tagged_posts}; 265 | return $taggedPosts ? $taggedPosts->{$tag} : undef; 266 | } 267 | 268 | =head2 postTgas 269 | 270 | Gets all tags of the posts 271 | 272 | =cut 273 | 274 | sub postTags { 275 | my ($self) = @_; 276 | my $taggedPosts = $self->{tagged_posts}; 277 | 278 | my @postTags; 279 | while (my ($tag, $posts) = each %$taggedPosts) { 280 | my $count = @$posts; 281 | push @postTags, { __minerl_tag => $tag, __minerl_post_count => $count }; 282 | } 283 | 284 | @postTags = sort { $a->{__minerl_tag} cmp $b->{__minerl_tag} } @postTags; 285 | 286 | return \@postTags; 287 | } 288 | 289 | =head2 months 290 | 291 | months during which some posts were created 292 | 293 | =cut 294 | 295 | sub months { 296 | my ($self) = @_; 297 | my $archivedPosts = $self->{archived_posts}; 298 | my @keys = keys %$archivedPosts if $archivedPosts; 299 | return \@keys; 300 | } 301 | 302 | =head2 monthLink 303 | 304 | my $month = $self->monthLink("2013/06"); 305 | $month eq "Jun, 2013"; 306 | 307 | =cut 308 | 309 | sub monthLink { 310 | my ($self, $month) = @_; 311 | return $self->{archived_months}->{$month}; 312 | } 313 | 314 | =head2 postsByMonth 315 | 316 | Gets all posts created on the specified month 317 | 318 | =cut 319 | 320 | sub postsByMonth { 321 | my ($self, $month) = @_; 322 | my $archivedPosts = $self->{archived_posts}; 323 | return $archivedPosts ? $archivedPosts->{$month} : undef; 324 | } 325 | 326 | =head2 postsMonths 327 | 328 | months during which some posts were created 329 | 330 | =cut 331 | 332 | sub postMonths { 333 | my ($self) = @_; 334 | my $archivedPosts = $self->{archived_posts}; 335 | 336 | my $archivedMonths = $self->{archived_months}; 337 | 338 | my @months; 339 | while (my ($month, $posts) = each %$archivedPosts) { 340 | my $count = @$posts; 341 | # format: "June, 2013", "2013/06", "12" 342 | push @months, { __minerl_month_display => $month, __minerl_month_link => $archivedMonths->{$month}, __minerl_post_count => $count }; 343 | } 344 | 345 | @months = sort { $a->{__minerl_month_link} cmp $b->{__minerl_month_link} } @months; 346 | 347 | return \@months; 348 | } 349 | 350 | 1; 351 | -------------------------------------------------------------------------------- /lib/minerl.pm: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | minerl - A static site generator written in Perl 4 | 5 | =head1 VERSION 6 | 7 | our $VERSION = '0.01'; 8 | 9 | =head1 SYNOPSIS 10 | 11 | use minerl; 12 | my $minerl = new minerl( cfg_file => "minerl.cfg" ); 13 | minerl->build(); 14 | ... 15 | 16 | =head1 DESCRIPTION 17 | 18 | This class exposes only one public method - build(), which is used to generate 19 | pages of the site and copy all raw resources to the output directory. 20 | 21 | =head1 AUTHOR 22 | 23 | neevek, C<< >> 24 | 25 | =head1 LICENSE AND COPYRIGHT 26 | 27 | Copyright 2013 neevek. 28 | 29 | This program is free software; you can redistribute it and/or modify it 30 | under the terms of the the Artistic License (2.0). You may obtain a 31 | copy of the full license at: 32 | 33 | L 34 | 35 | Any use, modification, and distribution of the Standard or Modified 36 | Versions is governed by this Artistic License. By using, modifying or 37 | distributing the Package, you accept this license. Do not use, modify, 38 | or distribute the Package, if you do not accept this license. 39 | 40 | If your Modified Version has been derived from a Modified Version made 41 | by someone other than you, you are nevertheless required to ensure that 42 | your Modified Version complies with the requirements of this license. 43 | 44 | This license does not grant you the right to use any trademark, service 45 | mark, tradename, or logo of the Copyright Holder. 46 | 47 | This license includes the non-exclusive, worldwide, free-of-charge 48 | patent license to make, have made, use, offer to sell, sell, import and 49 | otherwise transfer the Package with respect to any patent claims 50 | licensable by the Copyright Holder that are necessarily infringed by the 51 | Package. If you institute patent litigation (including a cross-claim or 52 | counterclaim) against any party alleging that the Package constitutes 53 | direct or contributory patent infringement, then this Artistic License 54 | to you shall terminate on the date that such litigation is filed. 55 | 56 | Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER 57 | AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. 58 | THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 59 | PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY 60 | YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR 61 | CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR 62 | CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, 63 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 | 65 | =cut 66 | 67 | package minerl; 68 | 69 | our $VERSION = '0.01'; 70 | 71 | our @ISA = qw(Minerl::BaseObject); 72 | 73 | use Config::IniFiles; 74 | use File::Path qw(make_path); 75 | use File::Find qw(find); 76 | use File::Copy qw(copy); 77 | use File::Basename qw(dirname); 78 | 79 | =head1 SUBROUTINES/METHODS 80 | 81 | =head2 new 82 | 83 | The constructor, which calls C<$self->_initConfigFile()> to read configurations 84 | from the cfg file, usually C 85 | 86 | =cut 87 | 88 | sub new { 89 | my ($class, @args) = @_; 90 | my $self = $class->SUPER::new(@args); 91 | 92 | $self->_initConfigFile(); 93 | 94 | return $self; 95 | } 96 | 97 | =head2 _initConfigFile 98 | 99 | For internal use only, it uses C to read the configurations 100 | and setup defaults for properties that are absent in the configuration file 101 | 102 | =cut 103 | 104 | sub _initConfigFile { 105 | my ($self) = @_; 106 | 107 | my $cfgFile = $self->{cfg_file}; 108 | -f $cfgFile or die "$cfgFile not found."; 109 | 110 | tie my %cfg, 'Config::IniFiles', ( -file => $cfgFile ); 111 | 112 | $cfg{system} = {} if !$cfg{system}; 113 | foreach my $k (keys %{$cfg{system}}) { 114 | $cfg{system}->{$k} =~ s/[\t\s]+$//; 115 | } 116 | 117 | $cfg{system}->{output_dir} = "site" if !$cfg{system}->{output_dir}; 118 | $cfg{system}->{raw_dir} = "_raw" if !$cfg{system}->{raw_dir}; 119 | $cfg{system}->{page_dir} = "_pages" if !$cfg{system}->{page_dir}; 120 | $cfg{system}->{page_suffix_regex} = "\\.(?:md|markdown|html)\$" if !$cfg{system}->{page_suffix_regex}; 121 | $cfg{system}->{template_dir} = "_templates" if !$cfg{system}->{template_dir}; 122 | $cfg{system}->{template_suffix} = ".html" if !$cfg{system}->{template_suffix}; 123 | $cfg{system}->{recent_posts_limit} = 5 if !$cfg{system}->{recent_posts_limit}; 124 | 125 | $cfg{template} = {} if !$cfg{template}; 126 | foreach my $k (keys %{$cfg{template}}) { 127 | $cfg{template}->{$k} =~ s/[\t\s]+$//; 128 | } 129 | 130 | $self->{cfg} = \%cfg; 131 | } 132 | 133 | =head2 _generatePages 134 | 135 | The main routine that generates all the HTML pages of the site, it reads all 136 | files that match the specified suffix regex in C, applies the specified 137 | templates on the pages, writes the final files to C. 138 | 139 | =cut 140 | 141 | sub _generatePages { 142 | my ($self, $verbose) = @_; 143 | 144 | my $cfg = $self->{cfg}; 145 | my $pageDir = $cfg->{system}->{page_dir}; 146 | -d $pageDir or die "$pageDir does not exist."; 147 | 148 | my $templateDir = $cfg->{system}->{template_dir}; 149 | -d $templateDir or die "$templateDir does not exist."; 150 | 151 | # ensures the output_dir exists 152 | my $outputDir = $cfg->{system}->{output_dir}; 153 | $outputDir =~ s/^[ \t]+//; 154 | $outputDir =~ s/[ \t]+$//; 155 | make_path($outputDir, { mode => 0755 }); 156 | 157 | my $templateSuffix = $cfg->{system}->{template_suffix}; 158 | my $pageSuffixRegex = $cfg->{system}->{page_suffix_regex}; 159 | 160 | my $tm = new Minerl::TemplateManager(template_dir => $templateDir, template_suffix => $templateSuffix); 161 | my $pm = new Minerl::PageManager( page_dir => $pageDir, page_suffix_regex => $pageSuffixRegex); 162 | 163 | # gets all the tags 164 | my $postTags = $pm->postTags(); 165 | # gets all the archive months 166 | my $postMonths = $pm->postMonths(); 167 | 168 | # gets all the pages 169 | my $pages = $pm->pages(); 170 | foreach my $page (@$pages) { 171 | print "processing page: $page->{filename}\n" if $verbose; 172 | 173 | my $type = $page->header("type"); 174 | if ($type && $type eq "taglist") { # if the page type is 'taglist', we loop through all the tags to generate page for each tag 175 | my $tags = $pm->tags(); 176 | foreach my $tag (@$tags) { 177 | 178 | # gets all posts with the specified tag 179 | my $postsByTag = $pm->postsByTag($tag); 180 | 181 | my $html = $tm->applyTemplate($page->header("layout"), $page->content 182 | , [$cfg->{template}, $page->headers, { __minerl_all_posts => $pm->posts() 183 | , __minerl_recent_posts => $pm->posts($cfg->{system}->{recent_posts_limit}) 184 | , __minerl_tagged_posts => $postsByTag , __minerl_cur_tag => $tag, "__minerl_all_tags" => $postTags, "__minerl_archived_months" => $postMonths } ]); 185 | 186 | # Pages of 'taglist' type are restricted to be output to "$output_dir/tags/" 187 | my $destFile = "$outputDir/tags/$tag.html"; 188 | $self->_writePageFile($outputDir, $destFile, $html); 189 | 190 | print " generated tag page: $destFile\n" if $verbose; 191 | } 192 | } elsif ($type && $type eq "archive") { # if the page type is 'archive', we loop through all the archive months to generate page for each archive month 193 | my $months = $pm->months(); 194 | foreach my $month (@$months) { 195 | 196 | # gets all posts posted on the specified month 197 | my $postsByMonth = $pm->postsByMonth($month); 198 | 199 | my $html = $tm->applyTemplate($page->header("layout"), $page->content 200 | , [$cfg->{template}, $page->headers, { __minerl_all_posts => $pm->posts() 201 | , __minerl_recent_posts => $pm->posts($cfg->{system}->{recent_posts_limit}) 202 | , __minerl_archived_posts => $postsByMonth, __minerl_cur_month => $month, "__minerl_all_tags" => $postTags, "__minerl_archived_months" => $postMonths } ]); 203 | 204 | # Pages of 'archive' type are restricted to be output to "$output_dir/archives/" 205 | my $destFile = "$outputDir/archives/" . $pm->monthLink($month) . ".html"; 206 | $self->_writePageFile($outputDir, $destFile, $html); 207 | 208 | print " generated archive page: $destFile\n" if $verbose; 209 | } 210 | } else { # when the 'type' is not specified or is 'post', we treat it as normal page 211 | my $html = $tm->applyTemplate($page->header("layout"), $page->content, [$cfg->{template}, $page->headers, $page->ctxVars, 212 | , { __minerl_all_posts => $pm->posts(), __minerl_recent_posts => $pm->posts($cfg->{system}->{recent_posts_limit}) 213 | , "__minerl_all_tags" => $postTags, "__minerl_archived_months" => $postMonths } ]); 214 | 215 | # outputFilename is affected by the title or the slug of the page specified at the header section 216 | # it has nothing to do with what filename is used for the source page 217 | my $destFile = "$outputDir/" . $page->outputFilename(); 218 | $self->_writePageFile($outputDir, $destFile, $html); 219 | 220 | print " generated normal page: $destFile\n" if $verbose; 221 | } 222 | } 223 | } 224 | 225 | =head2 _writePageFile 226 | 227 | Reusable method that outputs a file to C 228 | 229 | =cut 230 | 231 | sub _writePageFile { 232 | my ($self, $outputDir, $destFile, $html) = @_; 233 | 234 | my $outputSubDir = dirname($destFile); 235 | make_path($outputSubDir, { mode => 0755 }) if !-d $outputSubDir; 236 | 237 | open my $fh, ">:utf8", $destFile or die "Failed to write to '$destFile' - $!"; 238 | binmode($fh, ":utf8"); 239 | print $fh $$html; 240 | close $fh; 241 | } 242 | 243 | =head2 _copyRawResources 244 | 245 | Copies verbatim all files under C to C. 246 | 247 | =cut 248 | 249 | sub _copyRawResources { 250 | my ($self, $verbose) = @_; 251 | 252 | my $cfg = $self->{cfg}; 253 | my $rawDir = $cfg->{system}->{raw_dir}; 254 | 255 | # if there's nothing to copy 256 | return if !-d $rawDir; 257 | 258 | my $outputDir = $cfg->{system}->{output_dir}; 259 | 260 | find( { wanted => sub { 261 | if ( -d $_ ) { 262 | s|$rawDir||; # strip the first directory 263 | make_path ("$outputDir/" . $_, { mode => 0755 }); 264 | } elsif ( -f $_ ) { 265 | my $srcFile = $_; 266 | s|$rawDir|$outputDir|; # replace 'raw_dir' with 'output_dir' 267 | copy ($srcFile, $_); 268 | 269 | print " copied $srcFile to $_\n" if $verbose; 270 | } 271 | }, no_chdir => 1 }, ($rawDir) ); 272 | } 273 | 274 | =head2 build 275 | 276 | Public method that builds the site, it simply does two things: 277 | generates HTML pages and copies static resources to C 278 | 279 | =cut 280 | 281 | sub build { 282 | my ($self, $verbose) = @_; 283 | 284 | $self->_generatePages($verbose); 285 | $self->_copyRawResources($verbose); 286 | } 287 | 288 | 1; 289 | --------------------------------------------------------------------------------