Advertisement
Guest User

blaze-make

a guest
Jan 28th, 2015
244
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 55.74 KB | None | 0 0
  1. #!/usr/bin/env perl
  2.  
  3. # blaze-make - generates a blog from the BlazeBlogger repository
  4. # Copyright (C) 2009-2011 Jaromir Hradilek
  5.  
  6. # This program is  free software:  you can redistribute it and/or modify it
  7. # under  the terms  of the  GNU General Public License  as published by the
  8. # Free Software Foundation, version 3 of the License.
  9. #
  10. # This program  is  distributed  in the hope  that it will  be useful,  but
  11. # WITHOUT  ANY WARRANTY;  without  even the implied  warranty of MERCHANTA-
  12. # BILITY  or  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
  13. # License for more details.
  14. #
  15. # You should have received a copy of the  GNU General Public License  along
  16. # with this program. If not, see <http://www.gnu.org/licenses/>.
  17.  
  18. use strict;
  19. use warnings;
  20. use Digest::MD5;
  21. use File::Basename;
  22. use File::Compare;
  23. use File::Copy;
  24. use File::Path;
  25. use File::Spec::Functions;
  26. use Getopt::Long;
  27. use Time::Local 'timelocal_nocheck';
  28.  
  29. use utf8;
  30. use charnames ':full';
  31. use Unicode::Normalize qw (decompose);
  32. use Encode qw/encode decode/;
  33.  
  34.  
  35. # General script information:
  36. use constant NAME    => basename($0, '.pl');        # Script name.
  37. use constant VERSION => '1.2.0';                    # Script version.
  38.  
  39. # General script settings:
  40. our $blogdir     = '.';                             # Repository location.
  41. our $destdir     = '.';                             # HTML pages location.
  42. our $verbose     = 1;                               # Verbosity level.
  43. our $with_index  = 1;                               # Generate index page?
  44. our $with_posts  = 1;                               # Generate posts?
  45. our $with_pages  = 1;                               # Generate pages?
  46. our $with_tags   = 1;                               # Generate tags?
  47. our $with_rss    = 1;                               # Generate RSS feed?
  48. our $with_css    = 1;                               # Generate stylesheet?
  49. our $full_paths  = 0;                               # Generate full paths?
  50.  
  51. # Global variables:
  52. our $conf        = {};                              # Configuration.
  53. our $locale      = {};                              # Localization.
  54. our $cache_theme = '';                              # Cached template.
  55.  
  56. # Set up the __WARN__ signal handler:
  57. $SIG{__WARN__}  = sub {
  58.   print STDERR NAME . ": " . (shift);
  59. };
  60.  
  61. # Display an error message, and terminate the script:
  62. sub exit_with_error {
  63.   my $message      = shift || 'An error has occurred.';
  64.   my $return_value = shift || 1;
  65.  
  66.   # Display the error message:
  67.   print STDERR NAME . ": $message\n";
  68.  
  69.   # Terminate the script:
  70.   exit $return_value;
  71. }
  72.  
  73. # Display a warning message:
  74. sub display_warning {
  75.   my $message = shift || 'A warning was requested.';
  76.  
  77.   # Display the warning message:
  78.   print STDERR "$message\n";
  79.  
  80.   # Return success:
  81.   return 1;
  82. }
  83.  
  84. # Display usage information:
  85. sub display_help {
  86.   my $NAME = NAME;
  87.  
  88.   # Display the usage:
  89.   print << "END_HELP";
  90. Usage: $NAME [-cpqrIFPTV] [-b DIRECTORY] [-d DIRECTORY]
  91.        $NAME -h|-v
  92.  
  93.   -b, --blogdir DIRECTORY     specify a directory in which the BlazeBlogger
  94.                               repository is placed
  95.   -d, --destdir DIRECTORY     specify a directory in which the generated
  96.                               blog is to be placed
  97.   -c, --no-css                disable creating a style sheet
  98.   -I, --no-index              disable creating the index page
  99.   -p, --no-posts              disable creating blog posts
  100.   -P, --no-pages              disable creating pages
  101.   -T, --no-tags               disable creating tags
  102.   -r, --no-rss                disable creating the RSS feed
  103.   -F, --full-paths            enable including page names in generated
  104.                               links
  105.   -q, --quiet                 do not display unnecessary messages
  106.   -V, --verbose               display all messages, including a list of
  107.                               created files
  108.   -h, --help                  display this help and exit
  109.   -v, --version               display version information and exit
  110. END_HELP
  111.  
  112.   # Return success:
  113.   return 1;
  114. }
  115.  
  116. # Display version information:
  117. sub display_version {
  118.   my ($NAME, $VERSION) = (NAME, VERSION);
  119.  
  120.   # Display the version:
  121.   print << "END_VERSION";
  122. $NAME $VERSION
  123.  
  124. Copyright (C) 2009-2011 Jaromir Hradilek
  125. This program is free software; see the source for copying conditions. It is
  126. distributed in the hope  that it will be useful,  but WITHOUT ANY WARRANTY;
  127. without even the implied warranty of  MERCHANTABILITY or FITNESS FOR A PAR-
  128. TICULAR PURPOSE.
  129. END_VERSION
  130.  
  131.   # Return success:
  132.   return 1;
  133. }
  134.  
  135. # Translate given date to YYYY-MM-DD string:
  136. sub date_to_string {
  137.   my @date = localtime(shift);
  138.   return sprintf("%d-%02d-%02d", ($date[5] + 1900), ++$date[4], $date[3]);
  139. }
  140.  
  141. # Translate a date to a string in the RFC 822 form:
  142. sub rfc_822_date {
  143.   my @date = localtime(shift);
  144.  
  145.   # Prepare aliases:
  146.   my @months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
  147.   my @days   = qw( Sun Mon Tue Wed Thu Fri Sat );
  148.  
  149.   # Return the result:
  150.   return sprintf("%s, %02d %s %d %02d:%02d:%02d GMT", $days[$date[6]],
  151.                  $date[3], $months[$date[4]], 1900 + $date[5],
  152.                  $date[2], $date[1], $date[0]);
  153. }
  154.  
  155. # Append proper index file name to the end of the link if requested:
  156. sub fix_link {
  157.   my $link = shift || '';
  158.  
  159.   # Check whether the full path is enabled:
  160.   if ($full_paths) {
  161.     # Append the slash if missing:
  162.     $link .= '/' if ($link && $link !~ /\/$/);
  163.  
  164.     # Append the index file name:
  165.     $link .= 'index.' . ($conf->{core}->{extension} || 'html');
  166.   }
  167.   else {
  168.     # Make sure the link is not empty:
  169.     $link = '.' unless $link;
  170.   }
  171.  
  172.   # Return the correct link:
  173.   return $link;
  174. }
  175.  
  176.  
  177. # Strip all HTML elements:
  178. sub strip_html {
  179.   my $string = shift || return '';
  180.  
  181.   # Substitute common HTML entities:
  182.   $string =~ s/&[mn]dash;/--/ig;
  183.   $string =~ s/&[lrb]dquo;/"/ig;
  184.   $string =~ s/&[lr]squo;/'/ig;
  185.   $string =~ s/&hellip;/.../ig;
  186.   $string =~ s/&nbsp;/ /ig;
  187.  
  188.   # Strip HTML elements and other forbidded characters:
  189.   $string =~ s/(<[^>]*>|&[^;]*;|<|>|&)//g;
  190.  
  191.   # Strip superfluous whitespaces:
  192.   $string =~ s/\s{2,}/ /g;
  193.  
  194.   # Return the result:
  195.   return $string;
  196. }
  197.  
  198. # Read data from the INI file:
  199. sub read_ini {
  200.   my $file    = shift || die 'Missing argument';
  201.  
  202.   # Initialize required variables:
  203.   my $hash    = {};
  204.   my $section = 'default';
  205.  
  206.   # Open the file for reading:
  207.   open(INI, "$file") or return 0;
  208.   binmode(INI, ':encoding(UTF-8)');
  209.  
  210.   # Process each line:
  211.   while (my $line = <INI>) {
  212.     # Parse the line:
  213.     if ($line =~ /^\s*\[([^\]]+)\]\s*$/) {
  214.       # Change the section:
  215.       $section = $1;
  216.     }
  217.     elsif ($line =~ /^\s*(\S+)\s*=\s*(\S.*)$/) {
  218.       # Add the option to the hash:
  219.       $hash->{$section}->{$1} = $2;
  220.     }
  221.   }
  222.  
  223.   # Close the file:
  224.   close(INI);
  225.  
  226.   # Return the result:
  227.   return $hash;
  228. }
  229.  
  230. # Read the content of the configuration file:
  231. sub read_conf {
  232.   # Prepare the file name:
  233.   my $file = catfile($blogdir, '.blaze', 'config');
  234.  
  235.   # Parse the file:
  236.   if (my $conf = read_ini($file)) {
  237.     # Return the result:
  238.     return $conf;
  239.   }
  240.   else {
  241.     # Report failure:
  242.     display_warning("Unable to read the configuration.");
  243.  
  244.     # Return an empty configuration:
  245.     return {};
  246.   }
  247. }
  248.  
  249. # Read the content of the localization file:
  250. sub read_lang {
  251.   my $name = shift || 'en_US';
  252.  
  253.   # Prepare the file name:
  254.   my $file = catfile($blogdir, '.blaze', 'lang', $name);
  255.  
  256.   # Parse the file:
  257.   if (my $lang = read_ini($file)) {
  258.     # Return the result:
  259.     return $lang;
  260.   }
  261.   else {
  262.     # Report failure:
  263.     display_warning("Unable to read the localization file.");
  264.  
  265.     # Return an empty language settings:
  266.     return {};
  267.   }
  268. }
  269.  
  270. # Make proper URL from a string, stripping all forbidden characters:
  271. sub make_url {
  272.   my $url = shift || return '';
  273.  
  274.   # Strip forbidden characters:
  275.   $url =~ s/[^\w\s\-]//g;
  276.  
  277.   # Strip trailing spaces:
  278.   $url =~ s/\s+$//;
  279.  
  280.   # Substitute spaces:
  281.   $url =~ s/\s+/-/g;
  282.  
  283.   # Return the result:
  284.   return $url;
  285. }
  286.  
  287. # Compose a blog post or a page record:
  288. sub make_record {
  289.   my $type = shift || die 'Missing argument';
  290.   my $id   = shift || die 'Missing argument';
  291.   my ($title, $author, $date, $keywords, $tags, $url) = @_;
  292.  
  293.   # Check whether the title is specified:
  294.   if ($title) {
  295.     # Strip trailing spaces:
  296.     $title =~ s/\s+$//;
  297.   }
  298.   else {
  299.     # Assign the default value:
  300.     $title = 'Untitled';
  301.  
  302.     # Display the appropriate warning:
  303.     display_warning("Missing title in the $type with ID $id. " .
  304.                     "Using `$title' instead.");
  305.   }
  306.  
  307.   # Check whether the author is specified:
  308.   unless ($author) {
  309.     # Assign the default value:
  310.     $author = $conf->{user}->{name} || 'admin';
  311.  
  312.     # Report the missing author:
  313.     display_warning("Missing author in the $type with ID $id. " .
  314.                     "Using `$author' instead.");
  315.   }
  316.  
  317.   # Check whether the date is specified:
  318.   if ($date) {
  319.     # Check whether the format is valid:
  320.     unless ($date =~ /\d{4}-[01]\d-[0-3]\d/) {
  321.       # Use current date instead:
  322.       $date = date_to_string(time);
  323.  
  324.       # Report the invalid date:
  325.       display_warning("Invalid date in the $type with ID $id. " .
  326.                       "Using `$date' instead.");
  327.     }
  328.   }
  329.   else {
  330.     # Use current date instead:
  331.     $date = date_to_string(time);
  332.  
  333.     # Report the missing date:
  334.     display_warning("Missing date in the $type with ID $id. " .
  335.                     "Using `$date' instead.");
  336.   }
  337.  
  338.   # Check whether the keywords are specified:
  339.   if ($keywords) {
  340.     # Strip quotation marks:
  341.     $keywords =~ s/"//g;
  342.   }
  343.  
  344.   # Check whether the tags are specified:
  345.   if ($tags) {
  346.     # Make all tags lower case:
  347.     $tags = lc($tags);
  348.  
  349.     # Strip superfluous spaces:
  350.     $tags =~ s/\s{2,}/ /g;
  351.     $tags =~ s/\s+$//;
  352.  
  353.     # Strip trailing commas:
  354.     $tags =~ s/^,+|,+$//g;
  355.  
  356.     # Remove duplicates:
  357.     my %temp = map { $_, 1 } split(/,+\s*/, $tags);
  358.     $tags = join(', ', sort(keys(%temp)));
  359.   }
  360.   else {
  361.     # Assign the default value:
  362.     $tags = '';
  363.   }
  364.  
  365.   # Check whether the URL is specified:
  366.   if ($url) {
  367.  
  368.  
  369.     # Check whether it contains forbidden characters:
  370.     if ($url =~ /[^\w\-]/) {
  371.       # Strip forbidden characters:
  372.       $url = make_url($url);
  373.  
  374.       # Report the invalid URL:
  375.       display_warning("Invalid URL in the $type with ID $id. " .
  376.                       ($url ? "Stripping to `$url'."
  377.                             : "Deriving from the title."));
  378.     }
  379.   }
  380.  
  381.   # Unless already created, derive the URL from the blog post or page
  382.   # title:
  383.   unless ($url) {
  384.     # Derive the URL from the blog post or page title:
  385.     $url = make_url(lc($title));
  386.   }
  387.  
  388.   # Finalize the URL:
  389.   if ($url) {
  390.     # Prepend the ID to the blog post URL:
  391.     $url = "$id-$url" if $type eq 'post';
  392.   }
  393.   else {
  394.     # Base the URL on the ID:
  395.     $url = ($type eq 'post') ? $id : "page$id";
  396.  
  397.     # Report missing URL:
  398.     display_warning("Empty URL in the $type with ID $id. " .
  399.                     "Using `$url' instead.");
  400.   }
  401.  
  402.   # Return the composed record:
  403.   return {
  404.     'id'       => $id,
  405.     'title'    => $title,
  406.     'author'   => $author,
  407.     'date'     => $date,
  408.     'keywords' => $keywords,
  409.     'tags'     => $tags,
  410.     'url'      => $url,
  411.   };
  412. }
  413.  
  414. # Return a list of blog post or page header records:
  415. sub collect_headers {
  416.   my $type    = shift || 'post';
  417.  
  418.   # Initialize required variables:
  419.   my @records = ();
  420.  
  421.   # Prepare the file name:
  422.   my $head    = catdir($blogdir, '.blaze', "${type}s", 'head');
  423.  
  424.   # Open the directory:
  425.   opendir(HEAD, $head) or return @records;
  426.  
  427.  
  428.   # Process each file:
  429.   while (my $id = readdir(HEAD)) {
  430.     # Skip both . and ..:
  431.     next if $id =~ /^\.\.?$/;
  432.  
  433.     # Parse header data:
  434.     my $data     = read_ini(catfile($head, $id)) or next;
  435.     my $title    = $data->{header}->{title};
  436.     my $author   = $data->{header}->{author};
  437.     my $date     = $data->{header}->{date};
  438.     my $keywords = $data->{header}->{keywords};
  439.     my $tags     = $data->{header}->{tags};
  440.     my $url      = $data->{header}->{url};
  441.  
  442.     # Create the record:
  443.     my $record = make_record($type, $id, $title, $author, $date,
  444.                              $keywords, $tags, $url);
  445.  
  446.     # Add the record to the beginning of the list:
  447.     push(@records, $record);
  448.   }
  449.  
  450.   # Close the directory:
  451.   closedir(HEAD);
  452.  
  453.   # Return the result:
  454.   if ($type eq 'post') {
  455.     return sort {
  456.       sprintf("%s:%08d", $b->{date}, $b->{id}) cmp
  457.       sprintf("%s:%08d", $a->{date}, $a->{id})
  458.     } @records;
  459.   }
  460.   else {
  461.     return sort {
  462.       sprintf("%s:%08d", $a->{date}, $a->{id}) cmp
  463.       sprintf("%s:%08d", $b->{date}, $b->{id})
  464.     } @records;
  465.   }
  466. }
  467.  
  468. # Collect metadata:
  469. sub collect_metadata {
  470.   # Initialize required variables:
  471.   my $post_links  = {};
  472.   my $page_links  = {};
  473.   my $month_links = {};
  474.   my $tag_links   = {};
  475.  
  476.   # Prepare the list of month names:
  477.   my @month_name  = qw( january february march april may june july
  478.                         august september october november december );
  479.  
  480.   # Collect the page headers:
  481.   my @pages  = collect_headers('page');
  482.  
  483.   # Collect the blog post headers:
  484.   my @posts  = collect_headers('post');
  485.  
  486.   # Process each blog post header:
  487.   foreach my $record (@posts) {
  488.     # Decompose the record:
  489.     my ($year, $month) = split(/-/, $record->{date});
  490.     my @tags           = split(/,\s*/, $record->{tags});
  491.     my $temp           = $month_name[int($month) - 1];
  492.     my $name           = ($locale->{lang}->{$temp} || "\u$temp") ." $year";
  493.     my $url            = $record->{url};
  494.     my $id             = $record->{id};
  495.  
  496.     # Set up the blog post URL:
  497.     $post_links->{$id}->{url} = "$year/$month/$url";
  498.  
  499.     # Check whether the month is already present:
  500.     if ($month_links->{$name}) {
  501.       # Increase the counter:
  502.       $month_links->{$name}->{count}++;
  503.     }
  504.     else {
  505.       # Set up the URL:
  506.       $month_links->{$name}->{url}   = "$year/$month";
  507.  
  508.       # Set up the counter:
  509.       $month_links->{$name}->{count} = 1;
  510.     }
  511.  
  512.     # Process each tag separately:
  513.     foreach my $tag (@tags) {
  514.       # Check whether the tag is already present:
  515.       if ($tag_links->{$tag}) {
  516.         # Increase the counter:
  517.         $tag_links->{$tag}->{count}++;
  518.       }
  519.       else {
  520.         # Derive the URL from the tag name:
  521.  
  522.        my $characters = $tag;
  523.        my $tx = q{};
  524.        $characters = decompose($characters);
  525.        $characters =~ s/\p{NonspacingMark}//gxms;
  526.        for my $character ( split //xms, $tag ) {
  527.            my $name        = charnames::viacode(ord $character);
  528.            $name           =~ s/\s WITH \s .+ \z//xms;
  529.            $tx   .= chr charnames::vianame($name);
  530.        }
  531.  
  532.         my $tag_url = make_url($tx);
  533.  
  534.         # Make sure the URL string is not empty:
  535.         unless ($tag_url) {
  536.           # Use an MD5 checksum instead:
  537.           $tag_url = Digest::MD5->new->add($tag)->hexdigest;
  538.  
  539.           # Report the missing URL:
  540.           display_warning("Unable to derive the URL from tag `$tag'. " .
  541.                           "Using `$tag_url' instead.");
  542.         }
  543.  
  544.         # Set up the URL:
  545.         $tag_links->{$tag}->{url}   = $tag_url;
  546.  
  547.         # Set up the counter:
  548.         $tag_links->{$tag}->{count} = 1;
  549.       }
  550.     }
  551.   }
  552.  
  553.   # Process each page header:
  554.   foreach my $record (@pages) {
  555.     # Set up the page URL:
  556.     $page_links->{$record->{id}}->{url} = $record->{url};
  557.   }
  558.  
  559.   # Return the result:
  560.   return {
  561.     'headers' => {
  562.       'posts'   => \@posts,
  563.       'pages'   => \@pages,
  564.     },
  565.     'links'   => {
  566.       'posts'   => $post_links,
  567.       'pages'   => $page_links,
  568.       'months'  => $month_links,
  569.       'tags'    => $tag_links,
  570.     },
  571.   };
  572. }
  573.  
  574. # Return a list of tags:
  575. sub list_of_tags {
  576.   my $tags = shift || die 'Missing argument';
  577.  
  578.   # Check whether the tag creation is enabled:
  579.   return '' unless $with_tags;
  580.  
  581.   # Check whether the list is not empty:
  582.   if (my %tags = %$tags) {
  583.     # Return the list of tags:
  584.     return join("\n", map {
  585.       "<li><a href=\"" . fix_link("%root%tags/$tags{$_}->{url}") .
  586.       "\">$_ (" . $tags{$_}->{count} . ")</a></li>"
  587.     } sort(keys(%tags)));
  588.   }
  589.   else {
  590.     # Return an empty string:
  591.     return '';
  592.   }
  593. }
  594.  
  595. # Return a list of months:
  596. sub list_of_months {
  597.   my $months = shift || die 'Missing argument';
  598.   my $year   = shift || '';
  599.  
  600.   # Check whether the post creation is enabled:
  601.   return '' unless $with_posts;
  602.  
  603.   # Check whether the list is not empty:
  604.   if (my %months = %$months) {
  605.     # Return the list of months:
  606.     return join("\n", sort { $b cmp $a } (map {
  607.       "<li><a href=\"" . fix_link("%root%$months{$_}->{url}") .
  608.       "\">$_ (" . $months{$_}->{count} . ")</a></li>"
  609.     } grep(/$year$/, keys(%months))));
  610.   }
  611.   else {
  612.     # Return an empty string:
  613.     return '';
  614.   }
  615. }
  616.  
  617. # Return a list of pages:
  618. sub list_of_pages {
  619.   my $pages = shift || die 'Missing argument';
  620.  
  621.   # Initialize required variables:
  622.   my $list  = '';
  623.  
  624.   # Check whether the page creation is enabled:
  625.   return '' unless $with_pages;
  626.  
  627.   # Process each page separately:
  628.   foreach my $record (@$pages) {
  629.     # Decompose the record:
  630.     my $title = $record->{title};
  631.     my $url   = $record->{url};
  632.  
  633.     # Add the page link to the list:
  634.     $list .= "<li><a href=\"".fix_link("%root%$url")."\">$title</a></li>\n";
  635.   }
  636.  
  637.   # Strip trailing line break:
  638.   chomp($list);
  639.  
  640.   # Return the list of pages:
  641.   return $list;
  642. }
  643.  
  644. # Return a list of blog posts:
  645. sub list_of_posts {
  646.   my $posts = shift || die 'Missing argument';
  647.   my $max   = shift || 5;
  648.  
  649.   # Initialize required variables:
  650.   my $list  = '';
  651.  
  652.   # Check whether the blog post creation is enabled:
  653.   return '' unless $with_posts;
  654.  
  655.   # Initialize the counter:
  656.   my $count = 0;
  657.  
  658.   # Process each post separately:
  659.   foreach my $record (@$posts) {
  660.     # Stop when the post count reaches the limit:
  661.     last if $count == $max;
  662.  
  663.     # Decompose the record:
  664.     my $id    = $record->{id};
  665.     my $url   = $record->{url};
  666.     my $title = $record->{title};
  667.     my ($year, $month) = split(/-/, $record->{date});
  668.  
  669.     # Add the post link to the list:
  670.     $list .= "<li><a href=\"" . fix_link("%root%$year/$month/$url") .
  671.              "\" rel=\"permalink\">$title</a></li>\n";
  672.  
  673.     # Increase the counter:
  674.     $count++;
  675.   }
  676.  
  677.   # Strip trailing line break:
  678.   chomp($list);
  679.  
  680.   # Return the list of blog posts:
  681.   return $list;
  682. }
  683.  
  684. # Return the blog post or page body or synopsis:
  685. sub read_entry {
  686.   my $id      = shift || die 'Missing argument';
  687.   my $type    = shift || 'post';
  688.   my $link    = shift || '';
  689.   my $excerpt = shift || 0;
  690.  
  691.   # Prepare the file name:
  692.   my $file    = catfile($blogdir, '.blaze', "${type}s", 'body', $id);
  693.  
  694.   # Initialize required variables:
  695.   my $result  = '';
  696.  
  697.   # Open the file for reading:
  698.   open (FILE, $file) or return '';
  699.   binmode(FILE, ':encoding(UTF-8)');
  700.  
  701.   # Read the content of the file:
  702.   while (my $line = <FILE>) {
  703.     # When the synopsis is requested, look for a break mark:
  704.     if ($excerpt && $line =~ /<!--\s*break\s*-->/i) {
  705.       # Check whether the link is provided:
  706.       if ($link) {
  707.         # Read required data from the localization file:
  708.         my $more = $locale->{lang}->{more} || 'Read more &raquo;';
  709.  
  710.         # Add the `Read more' link to the end of the synopsis:
  711.         $result .= "<p><a href=\"$link\" class=\"more\">$more</a></p>\n";
  712.       }
  713.  
  714.       # Stop the parsing here:
  715.       last;
  716.     }
  717.  
  718.     # Add the line to the result:
  719.     $result .= $line;
  720.   }
  721.  
  722.   # Close the file:
  723.   close(FILE);
  724.  
  725.   # Return the result:
  726.   return $result;
  727. }
  728.  
  729. # Return a formatted blog post or page heading:
  730. sub format_heading {
  731.   my $title = shift || die 'Missing argument';
  732.   my $link  = shift || '';
  733.  
  734.   # Return the result:
  735.   return $link ? "<h2 class=\"post\"><a href=\"$link\"" .
  736.                  " rel=\"permalink\">$title</a></h2>\n"
  737.                : "<h2 class=\"post\">$title</h2>\n";
  738. }
  739.  
  740. # Return formatted blog post or page information:
  741. sub format_information {
  742.   my $record = shift || die 'Missing argument';
  743.   my $tags   = shift || die 'Missing argument';
  744.   my $type   = shift || 'top';
  745.  
  746.   # Initialize required variables:
  747.   my $class  = ($type eq 'top') ? 'information' : 'post-footer';
  748.   my ($date, $author, $taglist) = ('', '', '');
  749.  
  750.   # Read required data from the configuration:
  751.   my $author_location = $conf->{post}->{author} || 'top';
  752.   my $date_location   = $conf->{post}->{date}   || 'top';
  753.   my $tags_location   = $conf->{post}->{tags}   || 'top';
  754.  
  755.   # Read required data from the localization file:
  756.   my $posted_on = $locale->{lang}->{postedon}   || '';
  757.   my $posted_by = $locale->{lang}->{postedby}   || 'by';
  758.   my $tagged_as = $locale->{lang}->{taggedas}   || 'tagged as';
  759.  
  760.   # Check whether the date of publishing is to be included:
  761.   if ($date_location eq $type) {
  762.     # Format the date of publishing:
  763.     $date   = "$posted_on <span class=\"date\">$record->{date}</span>";
  764.   }
  765.  
  766.   # Check whether the author is to be included:
  767.   if ($author_location eq $type) {
  768.     # Format the author:
  769.     $author = "$posted_by <span class=\"author\">$record->{author}</span>";
  770.  
  771.     # Prepend a space if the date of publishing is included:
  772.     $author = " $author" if $date;
  773.   }
  774.  
  775.   # Check whether the tags are to be included (and if there are any):
  776.   if ($tags_location eq $type && $with_tags && $record->{tags}) {
  777.     # Convert tags to proper links:
  778.     $taglist = join(', ', map {
  779.       "<a href=\"". fix_link("%root%tags/$tags->{$_}->{url}") ."\">$_</a>"
  780.       } split(/,\s*/, $record->{tags}));
  781.  
  782.     # Format the tags:
  783.     $taglist = "$tagged_as <span class=\"tags\">$taglist</span>";
  784.  
  785.     # Prepend a comma if the date of publishing or the author are included:
  786.     $taglist = ", $taglist" if $date || $author;
  787.   }
  788.  
  789.   # Check if there is anything to return:
  790.   if ($date || $author || $taglist) {
  791.     # Return the result:
  792.     return "<div class=\"$class\">\n  \u$date$author$taglist\n</div>\n";
  793.   }
  794.   else {
  795.     # Return an empty string:
  796.     return '';
  797.   }
  798. }
  799.  
  800. # Return a formatted blog post or page entry:
  801. sub format_entry {
  802.   my $data    = shift || die 'Missing argument';
  803.   my $record  = shift || die 'Missing argument';
  804.   my $type    = shift || 'post';
  805.   my $excerpt = shift || 0;
  806.  
  807.   # Initialize required variables:
  808.   my $tags    = $data->{links}->{tags};
  809.   my $title   = $record->{title};
  810.   my $id      = $record->{id};
  811.   my ($link, $information, $post_footer) = ('', '', '');
  812.  
  813.   # If the synopsis is requested, prepare the entry link:
  814.   if ($excerpt) {
  815.     # Check whether the entry is a blog post, or a page:
  816.     if ($type eq 'post') {
  817.       # Decompose the record:
  818.       my ($year, $month) = split(/-/, $record->{date});
  819.  
  820.       # Compose the link:
  821.       $link = fix_link("%root%$year/$month/$record->{url}")
  822.     }
  823.     else {
  824.       # Compose the link:
  825.       $link = fix_link("%root%$record->{url}");
  826.     }
  827.   }
  828.  
  829.   # Prepare the blog post or page heading, and the body or the synopsis:
  830.   my $heading = format_heading($title, $link);
  831.   my $body    = read_entry($id, $type, $link, $excerpt);
  832.  
  833.   # For blog posts, prepare its additional information:
  834.   if ($type eq 'post') {
  835.     $information = format_information($record, $tags, 'top');
  836.     $post_footer = format_information($record, $tags, 'bottom');
  837.   }
  838.  
  839.   # Return the result:
  840.   return "\n$heading$information$body$post_footer";
  841. }
  842.  
  843. # Return a formatted section title:
  844. sub format_section {
  845.   my $title = shift || die 'Missing argument';
  846.  
  847.   # Return the result:
  848.   return "<div class=\"section\">$title</div>\n";
  849. }
  850.  
  851. # Return a formatted navigation links:
  852. sub format_navigation {
  853.   my $type  = shift || die 'Missing argument';
  854.   my $index = shift || '';
  855.  
  856.   # Read required data from the configuration:
  857.   my $conf_extension = $conf->{core}->{extension} || 'html';
  858.  
  859.   # Check the navigation type:
  860.   if ($type eq 'previous') {
  861.     # Read required data from the localization:
  862.     my $label = $locale->{lang}->{previous} || '&laquo; Previous';
  863.  
  864.     # Return the result:
  865.     return "<div class=\"previous\">" .
  866.            "<a href=\"index$index.$conf_extension\" rel=\"prev\">" .
  867.            "$label</a></div>\n";
  868.   }
  869.   else {
  870.     # Read required data from the localization:
  871.     my $label = $locale->{lang}->{next}     || 'Next &raquo;';
  872.  
  873.     # Return the result:
  874.     return "<div class=\"next\">" .
  875.            "<a href=\"index$index.$conf_extension\" rel=\"next\">" .
  876.            "$label</a></div>\n";
  877.     }
  878. }
  879.  
  880. # Prepare a template:
  881. sub format_template {
  882.   my $data          = shift || die 'Missing argument';
  883.   my $theme_file    = shift || $conf->{blog}->{theme} || 'default.html';
  884.   my $style_file    = shift || $conf->{blog}->{style} || 'default.css';
  885.  
  886.   # Restore the template from the cache if available:
  887.   return $cache_theme if $cache_theme;
  888.  
  889.   # Read required data from the documentation:
  890.   my $conf_doctype  = $conf->{core}->{doctype}     || 'html';
  891.   my $conf_encoding = $conf->{core}->{encoding}    || 'UTF-8';
  892.   my $conf_title    = $conf->{blog}->{title}       || 'Blog Title';
  893.   my $conf_subtitle = $conf->{blog}->{subtitle}    || 'blog subtitle';
  894.   my $conf_desc     = $conf->{blog}->{description} || 'blog description';
  895.   my $conf_name     = $conf->{user}->{name}        || 'admin';
  896.   my $conf_email    = $conf->{user}->{email}       || 'admin@localhost';
  897.   my $conf_nickname = $conf->{user}->{nickname}    || $conf_name;
  898.  
  899.   # Prepare a list of blog posts, pages, tags, and months:
  900.   my $list_pages    = list_of_pages($data->{headers}->{pages});
  901.   my $list_posts    = list_of_posts($data->{headers}->{posts});
  902.   my $list_months   = list_of_months($data->{links}->{months});
  903.   my $list_tags     = list_of_tags($data->{links}->{tags});
  904.  
  905.   # Determine the current year:
  906.   my $current_year  = substr(date_to_string(time), 0, 4);
  907.  
  908.   # Prepare the META tags:
  909.   my $meta_content_type = '<meta http-equiv="Content-Type" content="' .
  910.                           'txt/html; charset=' . $conf_encoding . '">';
  911.   my $meta_generator    = '<meta name="Generator" content="BlazeBlogger ' .
  912.                           VERSION . '">';
  913.   my $meta_copyright    = '<meta name="Copyright" content="&copy; ' .
  914.                           $current_year . ' ' . $conf_name . '">';
  915.   my $meta_date         = '<meta name="Date" content="'. localtime() .'">';
  916.   my $meta_description  = '<meta name="Description" content="' .
  917.                           $conf_desc . '">';
  918.   my $meta_keywords     = '<meta name="Keywords" content="%keywords%">';
  919.  
  920.   # Prepare the LINK tags:
  921.   my $link_stylesheet   = '<link rel="stylesheet" href="%root%' .
  922.                           $style_file . '" type="text/css">';
  923.   my $link_feed         = '<link rel="alternate" href="%root%rss.xml" ' .
  924.                           'title="RSS Feed" type="application/rss+xml">';
  925.  
  926.   # Prepare the document header and footer:
  927.   my $document_start;
  928.   my $document_end      = '</html>';
  929.  
  930.   # Decide which document type to use:
  931.   if ($conf_doctype ne 'xhtml') {
  932.     # Fix the document header:
  933.     $document_start = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  934.                      "http://www.w3.org/TR/html4/strict.dtd">
  935. <html>';
  936.   }
  937.   else {
  938.     # Fix the document header:
  939.     $document_start = '<?xml version="1.0" encoding="'.$conf_encoding.'"?>
  940. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  941.                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  942. <html xmlns="http://www.w3.org/1999/xhtml">';
  943.  
  944.     # Fix the tags:
  945.     $meta_content_type =~ s/>$/ \/>/;
  946.     $meta_generator    =~ s/>$/ \/>/;
  947.     $meta_copyright    =~ s/>$/ \/>/;
  948.     $meta_date         =~ s/>$/ \/>/;
  949.     $meta_description  =~ s/>$/ \/>/;
  950.     $meta_keywords     =~ s/>$/ \/>/;
  951.     $link_stylesheet   =~ s/>$/ \/>/;
  952.     $link_feed         =~ s/>$/ \/>/;
  953.   }
  954.  
  955.   # Open the theme file for reading:
  956.   open(THEME, catfile($blogdir, '.blaze', 'theme', $theme_file))
  957.     or return 0;
  958.   binmode(THEME, ':encoding(UTF-8)');
  959.  
  960.   # Read the theme file:
  961.   my $template = do { local $/; <THEME> };
  962.  
  963.   # Close the theme file:
  964.   close(THEME);
  965.  
  966.   # Substitute the header placeholders:
  967.   $template =~ s/<!--\s*start-document\s*-->/$document_start/ig;
  968.   $template =~ s/<!--\s*end-document\s*-->/$document_end/ig;
  969.   $template =~ s/<!--\s*content-type\s*-->/$meta_content_type/ig;
  970.   $template =~ s/<!--\s*generator\s*-->/$meta_generator/ig;
  971.   $template =~ s/<!--\s*copyright\s*-->/$meta_copyright/ig;
  972.   $template =~ s/<!--\s*date\s*-->/$meta_date/ig;
  973.   $template =~ s/<!--\s*description\s*-->/$meta_description/ig;
  974.   $template =~ s/<!--\s*keywords\s*-->/$meta_keywords/ig;
  975.   $template =~ s/<!--\s*stylesheet\s*-->/$link_stylesheet/ig;
  976.   $template =~ s/<!--\s*feed\s*-->/$link_feed/ig if $with_rss;
  977.   $template =~ s/<!--\s*rss\s*-->/$link_feed/ig if $with_rss; # Deprecated.
  978.  
  979.   # Substitute the list placeholders:
  980.   $template =~ s/<!--\s*pages\s*-->/$list_pages/ig;
  981.   $template =~ s/<!--\s*posts\s*-->/$list_posts/ig;
  982.   $template =~ s/<!--\s*archive\s*-->/$list_months/ig;
  983.   $template =~ s/<!--\s*tags\s*-->/$list_tags/ig;
  984.  
  985.   # Substitute body placeholders:
  986.   $template =~ s/<!--\s*title\s*-->/$conf_title/ig;
  987.   $template =~ s/<!--\s*subtitle\s*-->/$conf_subtitle/ig;
  988.   $template =~ s/<!--\s*name\s*-->/$conf_name/ig;
  989.   $template =~ s/<!--\s*nickname\s*-->/$conf_nickname/ig;
  990.   $template =~ s/<!--\s*e-mail\s*-->/$conf_email/ig;
  991.   $template =~ s/<!--\s*year\s*-->/$current_year/ig;
  992.  
  993.   # Store the template to the cache:
  994.   $cache_theme = $template;
  995.  
  996.   # Return the result:
  997.   return $template;
  998. }
  999.  
  1000. # Write a single page:
  1001. sub write_page {
  1002.   my $data     = shift || die 'Missing argument';
  1003.   my $target   = shift || '';
  1004.   my $root     = shift || '';
  1005.   my $content  = shift || '';
  1006.   my $heading  = shift || $conf->{blog}->{title} || 'My Blog';
  1007.   my $keywords = shift || '';
  1008.   my $index    = shift || '';
  1009.  
  1010.   # Initialize required variables:
  1011.   my $home    = fix_link($root);
  1012.   my $temp    = $root || '#';
  1013.  
  1014.   # Read required data from the configuration:
  1015.   my $conf_keywords  = $conf->{blog}->{keywords}  || 'blog keywords';
  1016.   my $conf_extension = $conf->{core}->{extension} || 'html';
  1017.  
  1018.   # Load the template:
  1019.   my $template = format_template($data);
  1020.  
  1021.   # Substitute the keywords:
  1022.   if ($keywords) {
  1023.     $template  =~ s/%keywords%/$keywords, $conf_keywords/ig;
  1024.   }
  1025.   else {
  1026.     $template  =~ s/%keywords%/$conf_keywords/ig;
  1027.   }
  1028.  
  1029.   # Substitute the page title:
  1030.   $template =~ s/<!--\s*page-title\s*-->/$heading/ig;
  1031.  
  1032.   # Add the page content:
  1033.   $template =~ s/<!--\s*content\s*-->/$content/ig;
  1034.  
  1035.   # Substitute the root directory:
  1036.   $template =~ s/%root%/$root/ig;
  1037.  
  1038.   # Substitute the home page:
  1039.   $template =~ s/%home%/$home/ig;
  1040.  
  1041.   # Substitute the `blog post / page / tag with the selected ID'
  1042.   # placeholder:
  1043.   while ($template =~ /%(post|page|tag)\[([^\]]+)\]%/i) {
  1044.     # Decompose the placeholder:
  1045.     my $type = $1;
  1046.     my $id   = lc($2);
  1047.  
  1048.     # Check whether the selected blog post / page / tag exists:
  1049.     if (defined $data->{links}->{"${type}s"}->{$id}) {
  1050.       # Get the blog post / page / tag link:
  1051.       my $link = $data->{links}->{"${type}s"}->{$id}->{url};
  1052.  
  1053.       # Compose the URL:
  1054.       $link    = ($type ne 'tag')
  1055.                ? fix_link("$root$link")
  1056.                : fix_link("${root}tags/$link");
  1057.  
  1058.       # Substitute the placeholder:
  1059.       $template =~ s/%$type\[$id\]%/$link/ig;
  1060.     }
  1061.     else {
  1062.       # Report the invalid reference:
  1063.       display_warning("Invalid reference to $type with ID $id.");
  1064.  
  1065.       # Disable the placeholder:
  1066.       $template =~ s/%$type\[$id\]%/#/ig;
  1067.     }
  1068.   }
  1069.  
  1070.   # Check whether to create a directory tree:
  1071.   if ($target) {
  1072.     # Create the target directory tree:
  1073.     eval { mkpath($target, 0) };
  1074.  
  1075.     # Make sure the directory creation was successful:
  1076.     exit_with_error("Creating `$target': $@", 13) if $@;
  1077.   }
  1078.  
  1079.   # Prepare the file name:
  1080.   my $file = $target
  1081.            ? catfile($target, "index$index.$conf_extension")
  1082.            : "index$index.$conf_extension";
  1083.  
  1084.   # Open the file for writing:
  1085.   open(FILE, ">$file") or return 0;
  1086.   binmode(FILE, ':utf8');
  1087.  
  1088.   # Write the line to the file:
  1089.   print FILE $template;
  1090.  
  1091.   # Close the file:
  1092.   close(FILE);
  1093.  
  1094.   # Report success:
  1095.   print "Created $file\n" if $verbose > 1;
  1096.  
  1097.   # Return success:
  1098.   return 1;
  1099. }
  1100.  
  1101. # Copy the style sheet:
  1102. sub copy_stylesheet {
  1103.   # Prepare file names:
  1104.   my $style = $conf->{blog}->{style} || 'default.css';
  1105.   my $from  = catfile($blogdir, '.blaze', 'style', $style);
  1106.   my $to    = ($destdir eq '.') ? $style : catfile($destdir, $style);
  1107.  
  1108.   # Check whether the existing style sheet differs:
  1109.   if (compare($from,$to)) {
  1110.     # Copy the file:
  1111.     copy($from, $to) or return 0;
  1112.  
  1113.     # Report success:
  1114.     print "Created $to\n" if $verbose > 1;
  1115.   }
  1116.  
  1117.   # Return success:
  1118.   return 1;
  1119. }
  1120.  
  1121. # Generate the RSS feed:
  1122. sub generate_rss {
  1123.   my $data          = shift || die 'Missing argument';
  1124.  
  1125.   # Read required data from the configuration:
  1126.   my $core_encoding  = $conf->{core}->{encoding}  || 'UTF-8';
  1127.   my $blog_title     = $conf->{blog}->{title}     || 'My Blog';
  1128.   my $blog_subtitle  = $conf->{blog}->{subtitle}  || 'yet another blog';
  1129.   my $feed_fullposts = $conf->{feed}->{fullposts} || 'false';
  1130.   my $feed_posts     = $conf->{feed}->{posts}     || 10;
  1131.   my $feed_baseurl   = $conf->{feed}->{baseurl};
  1132.  
  1133.   # Handle a deprecated setting; for the backward compatibility reasons
  1134.   # only, and to be removed in the near future:
  1135.   if ((defined $conf->{blog}->{url}) && (not $feed_baseurl)) {
  1136.     # Use the value from the deprecated option:
  1137.     $feed_baseurl = $conf->{blog}->{url};
  1138.  
  1139.     # Display the warning:
  1140.     display_warning("Option blog.url is deprecated. Use feed.baseurl " .
  1141.                     "instead.");
  1142.   }
  1143.  
  1144.   # Check whether the base URL is specified:
  1145.   unless ($feed_baseurl) {
  1146.     # Display the warning:
  1147.     display_warning("Missing feed.baseurl option. " .
  1148.                     "Skipping the RSS feed creation.");
  1149.  
  1150.     # Disable the RSS:
  1151.     $with_rss = 0;
  1152.  
  1153.     # Return success:
  1154.     return 1;
  1155.   }
  1156.  
  1157.   # Make sure the blog post number is a valid integer:
  1158.   unless ($feed_posts =~ /^\d+$/) {
  1159.     # Use default value:
  1160.     $feed_posts = 10;
  1161.  
  1162.     # Display a warning:
  1163.     display_warning("Invalid feed.posts option. Using the default value.");
  1164.   }
  1165.  
  1166.   # Set up the blog post item type:
  1167.   my $excerpt = ($feed_fullposts =~ /^(true|auto)\s*$/i) ? 0 : 1;
  1168.  
  1169.   # Initialize necessary variables:
  1170.   my $count      = 0;
  1171.  
  1172.   # Strip HTML elements:
  1173.   $blog_title    = strip_html($blog_title);
  1174.   $blog_subtitle = strip_html($blog_subtitle);
  1175.  
  1176.   # Strip trailing forward slash from the base URL:
  1177.   $feed_baseurl  =~ s/\/+$//;
  1178.  
  1179.   # Prepare the RSS feed file name:
  1180.   my $file = ($destdir eq '.') ? 'rss.xml'
  1181.                                : catfile($destdir, 'rss.xml');
  1182.  
  1183.   # Open the file for writing:
  1184.   open(RSS, ">$file") or return 0;
  1185.   binmode(RSS, ':utf8');
  1186.  
  1187.   # Write the RSS header:
  1188.   print RSS "<?xml version=\"1.0\" encoding=\"$core_encoding\"?>\n" .
  1189.             "<rss version=\"2.0\">\n<channel>\n" .
  1190.             "  <title>$blog_title</title>\n" .
  1191.             "  <link>$feed_baseurl/</link>\n" .
  1192.             "  <description>$blog_subtitle</description>\n" .
  1193.             "  <generator>BlazeBlogger " . VERSION . "</generator>\n";
  1194.  
  1195.   # Process the requested number of posts:
  1196.   foreach my $record (@{$data->{headers}->{posts}}) {
  1197.     # Stop when the post count reaches the limit:
  1198.     last if $count == $feed_posts;
  1199.  
  1200.     # Decompose the record:
  1201.     my $url        = $record->{url};
  1202.     my ($year, $month, $day) = split(/-/, $record->{date});
  1203.  
  1204.     # Get the RFC 822 date-time string:
  1205.     my $time       = timelocal_nocheck(1, 0, 0, $day, ($month - 1), $year);
  1206.     my $date_time  = rfc_822_date($time);
  1207.  
  1208.     # Prepare the blog post title:
  1209.     my $post_title = strip_html($record->{title});
  1210.  
  1211.     # Open the blog post item:
  1212.     print RSS "  <item>\n    <title>$post_title</title>\n  " .
  1213.               "  <link>$feed_baseurl/$year/$month/$url/</link>\n  " .
  1214.               "  <guid>$feed_baseurl/$year/$month/$url/</guid>\n  " .
  1215.               "  <pubDate>$date_time</pubDate>\n  ";
  1216.  
  1217.     # Read the blog post body:
  1218.     my $post_desc = read_entry($record->{id}, 'post', '', $excerpt);
  1219.  
  1220.     # Substitute the root directory placeholder:
  1221.     $post_desc =~ s/%root%/$feed_baseurl\//ig;
  1222.  
  1223.     # Substitute the home page placeholder:
  1224.     $post_desc =~ s/%home%/$feed_baseurl\//ig;
  1225.  
  1226.     # Add the blog post body:
  1227.     print RSS "  <description><![CDATA[$post_desc    ]]></description>\n";
  1228.  
  1229.     # Close the blog post item:
  1230.     print RSS "  </item>\n";
  1231.  
  1232.     # Increase the number of listed items:
  1233.     $count++;
  1234.   }
  1235.  
  1236.   # Write the RSS footer:
  1237.   print RSS "</channel>\n</rss>";
  1238.  
  1239.   # Close the file:
  1240.   close(RSS);
  1241.  
  1242.   # Report success:
  1243.   print "Created $file\n" if $verbose > 1;
  1244.  
  1245.   # Return success:
  1246.   return 1;
  1247. }
  1248.  
  1249. # Generate the index page:
  1250. sub generate_index {
  1251.   my $data       = shift || die 'Missing argument';
  1252.  
  1253.   # Initialize required variables:
  1254.   my $body       = '';                              # List of posts.
  1255.   my $count      = 0;                               # Post counter.
  1256.   my $page       = 0;                               # Page counter.
  1257.  
  1258.   # Read required data from the configuration:
  1259.   my $blog_posts = $conf->{blog}->{posts}     || 10;
  1260.   my $blog_title = $conf->{blog}->{title}     || 'My Blog';
  1261.  
  1262.   # Prepare the target directory name:
  1263.   my $target     = ($destdir eq '.') ? '' : $destdir;
  1264.  
  1265.   # Make sure the posts number is a valid integer:
  1266.   unless ($blog_posts =~ /^\d+$/) {
  1267.     # Use the default value:
  1268.     $blog_posts = 10;
  1269.  
  1270.     # Display a warning:
  1271.     display_warning("Invalid blog.posts option. Using the default value.");
  1272.   }
  1273.  
  1274.   # Check whether the blog posts are enabled:
  1275.   if ($with_posts) {
  1276.     # Process the requested number of blog posts:
  1277.     foreach my $record (@{$data->{headers}->{posts}}) {
  1278.       # Check whether the number of listed blog posts reached the limit:
  1279.       if ($count == $blog_posts) {
  1280.         # Prepare information for the page navigation:
  1281.         my $index = $page     || '';
  1282.         my $next  = $page - 1 || '';
  1283.         my $prev  = $page + 1;
  1284.  
  1285.         # Add the navigation:
  1286.         $body .= format_navigation('previous', $prev);
  1287.         $body .= format_navigation('next', $next) if $page;
  1288.  
  1289.         # Write the index page:
  1290.         write_page($data, $target, '', $body, $blog_title, '', $index)
  1291.           or return 0;
  1292.  
  1293.         # Clear the page body:
  1294.         $body  = '';
  1295.  
  1296.         # Reset the blog post counter:
  1297.         $count = 0;
  1298.  
  1299.         # Increase the page counter:
  1300.         $page++;
  1301.       }
  1302.  
  1303.       # Add the blog post synopsis to the page body:
  1304.       $body .= format_entry($data, $record, 'post', 1);
  1305.  
  1306.       # Increase the number of listed blog posts:
  1307.       $count++;
  1308.     }
  1309.  
  1310.     # Check whether there are unwritten data:
  1311.     if ($body) {
  1312.       # Prepare information for the page navigation:
  1313.       my $index = $page     || '';
  1314.       my $next  = $page - 1 || '';
  1315.  
  1316.       # Add navigation:
  1317.       $body .= format_navigation('next', $next) if $page;
  1318.  
  1319.       # Write the index page:
  1320.       write_page($data, $target, '', $body, $blog_title, '', $index)
  1321.         or return 0;
  1322.     }
  1323.   }
  1324.   else {
  1325.     # Write an empty index page:
  1326.     write_page($data, $target, '', $body, $blog_title) or return 0;
  1327.   }
  1328.  
  1329.   # Return success:
  1330.   return 1;
  1331. }
  1332.  
  1333. # Generate the blog posts:
  1334. sub generate_posts {
  1335.   my $data         = shift || die 'Missing argument';
  1336.  
  1337.   # Read required data from the configuration:
  1338.   my $blog_posts   = $conf->{blog}->{posts}      || 10;
  1339.  
  1340.   # Read required data from the localization:
  1341.   my $title_string = $locale->{lang}->{archive}  || 'Archive for';
  1342.  
  1343.   # Prepare the list of month names:
  1344.   my @names        = qw( january february march april may june july
  1345.                          august september october november december );
  1346.  
  1347.   # Initialize post related variables:
  1348.   my $post_body    = '';                            # Blog post content.
  1349.  
  1350.   # Inicialize yearly archive-related variables:
  1351.   my $year_body    = '';                            # List of months.
  1352.   my $year_curr    = '';                            # Current year.
  1353.   my $year_last    = '';                            # Last processed year.
  1354.  
  1355.   # Initialize monthly archive-related variables:
  1356.   my $month_body   = '';                            # List of posts.
  1357.   my $month_curr   = '';                            # Current month.
  1358.   my $month_last   = '';                            # Last processed month.
  1359.   my $month_count  = 0;                             # Post counter.
  1360.   my $month_page   = 0;                             # Page counter.
  1361.  
  1362.   # Declare other necessary variables:
  1363.   my ($year, $month, $target);
  1364.  
  1365.   # Make sure the blog post number is a valid integer:
  1366.   unless ($blog_posts =~ /^\d+$/) {
  1367.     # Use the default value:
  1368.     $blog_posts = 10;
  1369.  
  1370.     # Display a warning:
  1371.     display_warning("Invalid blog.posts option. Using the default value.");
  1372.   }
  1373.  
  1374.   # Process each record:
  1375.   foreach my $record (@{$data->{headers}->{posts}}) {
  1376.     # Decompose the record:
  1377.     ($year, $month) = split(/-/, $record->{date});
  1378.  
  1379.     # Prepare the blog post body:
  1380.     $post_body = format_entry($data, $record, 'post', 0);
  1381.  
  1382.     # Prepare the target directory name:
  1383.     $target    = ($destdir eq '.')
  1384.                ? catdir($year, $month, $record->{url})
  1385.                : catdir($destdir, $year, $month, $record->{url});
  1386.  
  1387.     # Write the blog post:
  1388.     write_page($data, $target, '../../../', $post_body, $record->{title},
  1389.                $record->{keywords}) or return 0;
  1390.  
  1391.     # Set the year:
  1392.     $year_curr = $year;
  1393.  
  1394.     # Check whether the year has changed:
  1395.     if ($year_last ne $year_curr) {
  1396.       # Prepare the section title:
  1397.       my $title   = "$title_string $year";
  1398.  
  1399.       # Add the yearly archive section title:
  1400.       $year_body  = format_section($title);
  1401.  
  1402.       # Add the yearly archive list of months:
  1403.       $year_body .= "<ul>\n" . list_of_months($data->{links}->{months},
  1404.                                               $year) . "\n</ul>";
  1405.  
  1406.       # Prepare the yearly archive target directory name:
  1407.       $target = ($destdir eq '.') ? $year : catdir($destdir, $year);
  1408.  
  1409.       # Write the yearly archive index page:
  1410.       write_page($data, $target, '../', $year_body, $title, $title)
  1411.         or return 0;
  1412.  
  1413.       # Change the previous year to the currently processed one:
  1414.       $year_last = $year_curr;
  1415.     }
  1416.  
  1417.     # If this is the first loop, fake the previous month as the current:
  1418.     $month_last = "$year/$month" unless $month_last;
  1419.  
  1420.     # Set the month:
  1421.     $month_curr = "$year/$month";
  1422.  
  1423.     # Check whether the month has changed, or whether the  number of listed
  1424.     # posts has reached the limit:
  1425.     if (($month_last ne $month_curr) || ($month_count == $blog_posts)) {
  1426.       # Prepare information for the page navigation:
  1427.       my $index = $month_page     || '';
  1428.       my $next  = $month_page - 1 || '';
  1429.       my $prev  = $month_page + 1;
  1430.  
  1431.       # Get information about the last processed month:
  1432.       ($year, $month) = split(/\//, $month_last);
  1433.  
  1434.       # Prepare the section tile:
  1435.       my $temp  = $names[int($month) - 1];
  1436.       my $name  = ($locale->{lang}->{$temp} || $temp) . " $year";
  1437.       my $title = "$title_string $name";
  1438.  
  1439.       # Add the section title:
  1440.       $month_body  = format_section($title) . $month_body;
  1441.  
  1442.       # Add the navigation:
  1443.       $month_body .= format_navigation('previous', $prev)
  1444.                      if $month_curr eq $month_last;
  1445.       $month_body .= format_navigation('next', $next)
  1446.                      if $month_page;
  1447.  
  1448.       # Prepare the monthly archive target directory name:
  1449.       $target = ($destdir eq '.')
  1450.               ? catdir($year, $month)
  1451.               : catdir($destdir, $year, $month);
  1452.  
  1453.       # Write the monthly archive index page:
  1454.       write_page($data, $target, '../../', $month_body, $title, $title,
  1455.                  $index) or return 0;
  1456.  
  1457.       # Check whether the month has changed:
  1458.       if ($month_curr ne $month_last) {
  1459.         # Reset the page counter:
  1460.         $month_page = 0;
  1461.       }
  1462.       else {
  1463.         # Increase the page counter:
  1464.         $month_page++;
  1465.       }
  1466.  
  1467.       # Change the previous month to the currently processed one:
  1468.       $month_last = $month_curr;
  1469.  
  1470.       # Clear the monthly archive body:
  1471.       $month_body = '';
  1472.  
  1473.       # Reset the blog post counter:
  1474.       $month_count = 0;
  1475.     }
  1476.  
  1477.     # Add the blog post synopsis:
  1478.     $month_body .= format_entry($data, $record, 'post', 1);
  1479.  
  1480.     # Increase the number of listed blog posts:
  1481.     $month_count++;
  1482.   }
  1483.  
  1484.   # Check whether there are any unwritten data:
  1485.   if ($month_body) {
  1486.     # Prepare information for the page navigation:
  1487.     my $index = $month_page     || '';
  1488.     my $next  = $month_page - 1 || '';
  1489.  
  1490.     # Get information about the last processed month:
  1491.     ($year, $month) = split(/\//, $month_curr);
  1492.  
  1493.     # Get information for the title:
  1494.     my $temp  = $names[int($month) - 1];
  1495.     my $name  = ($locale->{lang}->{$temp} || $temp) . " $year";
  1496.     my $title = "$title_string $name";
  1497.  
  1498.     # Add the section title:
  1499.     $month_body  = format_section($title) . $month_body;
  1500.  
  1501.     # Add the navigation:
  1502.     $month_body .= format_navigation('next', $next) if $month_page;
  1503.  
  1504.     # Prepare the monthly archive target directory name:
  1505.     $target = ($destdir eq '.')
  1506.             ? catdir($year, $month)
  1507.             : catdir($destdir, $year, $month);
  1508.  
  1509.     # Write the monthly archive index page:
  1510.     write_page($data, $target, '../../', $month_body, $title, $title,
  1511.                $index) or return 0;
  1512.   }
  1513.  
  1514.   # Return success:
  1515.   return 1;
  1516. }
  1517.  
  1518. # Generate the tags:
  1519. sub generate_tags {
  1520.   my $data         = shift || die 'Missing argument';
  1521.  
  1522.   # Read required data from the configuration:
  1523.   my $blog_posts   = $conf->{blog}->{posts}      || 10;
  1524.  
  1525.   # Read required data from the localization:
  1526.   my $title_string = $locale->{lang}->{tags}     || 'Posts tagged as';
  1527.   my $tags_string  = $locale->{lang}->{taglist}  || 'List of tags';
  1528.  
  1529.   # Make sure the blog post number is a valid integer:
  1530.   unless ($blog_posts =~ /^\d+$/) {
  1531.     # Use the default value:
  1532.     $blog_posts = 10;
  1533.  
  1534.     # Display a warning:
  1535.     display_warning("Invalid blog.posts option. Using the default value.");
  1536.   }
  1537.  
  1538.   # Process each tag separately:
  1539.   foreach my $tag (keys %{$data->{links}->{tags}}) {
  1540.     # Initialize tag related variables:
  1541.     my $tag_body  = '';                             # List of posts.
  1542.     my $tag_count = 0;                              # Post counter.
  1543.     my $tag_page  = 0;                              # Page counter.
  1544.  
  1545.     # Declare other necessary variables:
  1546.     my $target;
  1547.  
  1548.     # Process each record:
  1549.     foreach my $record (@{$data->{headers}->{posts}}) {
  1550.       # Check whether the blog post contains the current tag:
  1551.       next unless $record->{tags} =~ /(^|,\s*)$tag(,\s*|$)/;
  1552.  
  1553.       # Check whether the number of listed blog posts reached the limit:
  1554.       if ($tag_count == $blog_posts) {
  1555.         # Prepare information for the page navigation:
  1556.         my $index = $tag_page     || '';
  1557.         my $next  = $tag_page - 1 || '';
  1558.         my $prev  = $tag_page + 1;
  1559.  
  1560.         # Prepare the section title:
  1561.         my $title = "$title_string $tag";
  1562.  
  1563.         # Add the section title:
  1564.         $tag_body  = format_section($title) . $tag_body;
  1565.  
  1566.         # Add the navigation:
  1567.         $tag_body .= format_navigation('previous', $prev);
  1568.         $tag_body .= format_navigation('next', $next) if $tag_page;
  1569.  
  1570.         # Prepare the tag target directory name:
  1571.         $target = ($destdir eq '.')
  1572.                 ? catdir('tags', $data->{links}->{tags}->{$tag}->{url})
  1573.                 : catdir($destdir, 'tags',
  1574.                          $data->{links}->{tags}->{$tag}->{url});
  1575.  
  1576.         # Write the tag index page:
  1577.         write_page($data, $target, '../../', $tag_body, $title, $title,
  1578.                    $index) or return 0;
  1579.  
  1580.         # Clear the tag body:
  1581.         $tag_body  = '';
  1582.  
  1583.         # Reset the blog post counter:
  1584.         $tag_count = 0;
  1585.  
  1586.         # Increase the page counter:
  1587.         $tag_page++;
  1588.       }
  1589.  
  1590.       # Add the blog post synopsis:
  1591.       $tag_body .= format_entry($data, $record, 'post', 1);
  1592.  
  1593.       # Increase the number of listed blog posts:
  1594.       $tag_count++;
  1595.     }
  1596.  
  1597.     # Check whether there are unwritten data:
  1598.     if ($tag_body) {
  1599.       # Prepare information for the page navigation:
  1600.       my $index = $tag_page     || '';
  1601.       my $next  = $tag_page - 1 || '';
  1602.  
  1603.       # Prepare the section title:
  1604.       my $title = "$title_string $tag";
  1605.  
  1606.       # Add the section title:
  1607.       $tag_body  = format_section($title) . $tag_body;
  1608.  
  1609.       # Add the navigation:
  1610.       $tag_body .= format_navigation('next', $next) if $tag_page;
  1611.  
  1612.       # Prepare the tag target directory name:
  1613.       $target = ($destdir eq '.')
  1614.               ? catdir('tags', $data->{links}->{tags}->{$tag}->{url})
  1615.               : catdir($destdir, 'tags',
  1616.                        $data->{links}->{tags}->{$tag}->{url});
  1617.  
  1618.       # Write the tag index page:
  1619.       write_page($data, $target, '../../', $tag_body, $title, $title,
  1620.                  $index) or return 0;
  1621.     }
  1622.   }
  1623.  
  1624.   # Create the tag list, if any:
  1625.   if (%{$data->{links}->{tags}}) {
  1626.     # Add the tag list section title:
  1627.     my $taglist_body = format_section($tags_string);
  1628.  
  1629.     # Add the tag list:
  1630.     $taglist_body   .= "<ul>\n".list_of_tags($data->{links}->{tags},'../').
  1631.                        "\n</ul>";
  1632.  
  1633.     # Prepare the tag list target directory name:
  1634.     my $target = ($destdir eq '.') ? 'tags' : catdir($destdir, 'tags');
  1635.  
  1636.     # Write the tag list index page:
  1637.     write_page($data, $target, '../', $taglist_body, $tags_string,
  1638.                $tags_string) or return 0;
  1639.   }
  1640.  
  1641.   # Return success:
  1642.   return 1;
  1643. }
  1644.  
  1645. # Generate the pages:
  1646. sub generate_pages {
  1647.   my $data = shift || die 'Missing argument';
  1648.  
  1649.   # Process each record:
  1650.   foreach my $record (@{$data->{headers}->{pages}}) {
  1651.     # Prepare the page body:
  1652.     my $body   = format_entry($data, $record, 'page', 0);
  1653.  
  1654.     # Prepare the target directory name:
  1655.     my $target = ($destdir eq '.')
  1656.                ? catdir($record->{url})
  1657.                : catdir($destdir, $record->{url});
  1658.  
  1659.     # Write the page:
  1660.     write_page($data, $target, '../', $body, $record->{title},
  1661.                $record->{keywords}) or return 0;
  1662.   }
  1663.  
  1664.   # Return success:
  1665.   return 1;
  1666. }
  1667.  
  1668. # Set up the option parser:
  1669. Getopt::Long::Configure('no_auto_abbrev', 'no_ignore_case', 'bundling');
  1670.  
  1671. # Process command line options:
  1672. GetOptions(
  1673.   'help|h'        => sub { display_help();    exit 0; },
  1674.   'version|v'     => sub { display_version(); exit 0; },
  1675.   'quiet|q'       => sub { $verbose    = 0;     },
  1676.   'verbose|V'     => sub { $verbose    = 2;     },
  1677.   'blogdir|b=s'   => sub { $blogdir    = $_[1]; },
  1678.   'destdir|d=s'   => sub { $destdir    = $_[1]; },
  1679.   'with-index'    => sub { $with_index = 1 },
  1680.   'no-index|I'    => sub { $with_index = 0 },
  1681.   'with-posts'    => sub { $with_posts = 1 },
  1682.   'no-posts|p'    => sub { $with_posts = 0 },
  1683.   'with-pages'    => sub { $with_pages = 1 },
  1684.   'no-pages|P'    => sub { $with_pages = 0 },
  1685.   'with-tags'     => sub { $with_tags  = 1 },
  1686.   'no-tags|T'     => sub { $with_tags  = 0 },
  1687.   'with-rss'      => sub { $with_rss   = 1 },
  1688.   'no-rss|r'      => sub { $with_rss   = 0 },
  1689.   'with-css'      => sub { $with_css   = 1 },
  1690.   'no-css|c'      => sub { $with_css   = 0 },
  1691.   'full-paths|F'  => sub { $full_paths = 1 },
  1692.   'no-full-paths' => sub { $full_paths = 0 },
  1693. );
  1694.  
  1695. # Check superfluous options:
  1696. exit_with_error("Invalid option `$ARGV[0]'.", 22) if (scalar(@ARGV) != 0);
  1697.  
  1698. # Check whether the repository is present, no matter how naive this method
  1699. # actually is:
  1700. exit_with_error("Not a BlazeBlogger repository! Try `blaze-init' first.",1)
  1701.   unless (-d catdir($blogdir, '.blaze'));
  1702.  
  1703. # Make sure there is something to do at all:
  1704. unless ($with_posts || $with_pages) {
  1705.   # Report success:
  1706.   print "Nothing to do.\n" if $verbose;
  1707.  
  1708.   # Return success:
  1709.   exit 0;
  1710. }
  1711.  
  1712. # When the blog post creation is disabled, disable the RSS feed and the tag
  1713. # creation as well:
  1714. unless ($with_posts) {
  1715.   $with_tags = 0;
  1716.   $with_rss  = 0;
  1717. }
  1718.  
  1719. # Read the configuration file:
  1720. $conf    = read_conf();
  1721.  
  1722. # Read the localization file:
  1723. $locale  = read_lang($conf->{blog}->{lang});
  1724.  
  1725. # Collect the metadata:
  1726. my $data = collect_metadata();
  1727.  
  1728. # Copy the style sheet:
  1729. copy_stylesheet()
  1730.   or exit_with_error("An error has occurred while creating the stylesheet.")
  1731.   if $with_css;
  1732.  
  1733. # Generate RSS feed:
  1734. generate_rss($data)
  1735.   or exit_with_error("An error has occurred while creating the RSS feed.")
  1736.   if $with_rss;
  1737.  
  1738. # Generate index page:
  1739. generate_index($data)
  1740.   or exit_with_error("An error has occurred while creating the index page.")
  1741.   if $with_index;
  1742.  
  1743. # Generate posts:
  1744. generate_posts($data)
  1745.   or exit_with_error("An error has occurred while creating the blog posts.")
  1746.   if $with_posts;
  1747.  
  1748. # Generate tags:
  1749. generate_tags($data)
  1750.   or exit_with_error("An error has occurred while creating the tags.")
  1751.   if $with_tags;
  1752.  
  1753. # Generate pages:
  1754. generate_pages($data)
  1755.   or exit_with_error("An error has occurred while creating the pages.")
  1756.   if $with_pages;
  1757.  
  1758. # Report success:
  1759. print "Done.\n" if $verbose;
  1760.  
  1761. # Return success:
  1762. exit 0;
  1763.  
  1764. __END__
  1765.  
  1766. =head1 NAME
  1767.  
  1768. blaze-make - generates a blog from the BlazeBlogger repository
  1769.  
  1770. =head1 SYNOPSIS
  1771.  
  1772. B<blaze-make> [B<-cpqrIFPTV>] [B<-b> I<directory>] [B<-d> I<directory>]
  1773.  
  1774. B<blaze-make> B<-h>|B<-v>
  1775.  
  1776. =head1 DESCRIPTION
  1777.  
  1778. B<blaze-make> reads the BlazeBlogger repository, and generates a complete
  1779. directory tree of static pages, including blog posts, single pages, monthly
  1780. and yearly archives, tags, and even an RSS feed.
  1781.  
  1782. =head1 OPTIONS
  1783.  
  1784. =over
  1785.  
  1786. =item B<-b> I<directory>, B<--blogdir> I<directory>
  1787.  
  1788. Allows you to specify a I<directory> in which the BlazeBlogger repository
  1789. is placed. The default option is a current working directory.
  1790.  
  1791. =item B<-d> I<directory>, B<--destdir> I<directory>
  1792.  
  1793. Allows you to specify a I<directory> in which the generated blog is to be
  1794. placed. The default option is a current working directory.
  1795.  
  1796. =item B<-c>, B<--no-css>
  1797.  
  1798. Disables creating a style sheet.
  1799.  
  1800. =item B<-I>, B<--no-index>
  1801.  
  1802. Disables creating the index page.
  1803.  
  1804. =item B<-p>, B<--no-posts>
  1805.  
  1806. Disables creating blog posts.
  1807.  
  1808. =item B<-P>, B<--no-pages>
  1809.  
  1810. Disables creating pages.
  1811.  
  1812. =item B<-T>, B<--no-tags>
  1813.  
  1814. Disables creating tags.
  1815.  
  1816. =item B<-r>, B<--no-rss>
  1817.  
  1818. Disables creating the RSS feed.
  1819.  
  1820. =item B<-F>, B<--full-paths>
  1821.  
  1822. Enables including page names in generated links.
  1823.  
  1824. =item B<-q>, B<--quiet>
  1825.  
  1826. Disables displaying of unnecessary messages.
  1827.  
  1828. =item B<-V>, B<--verbose>
  1829.  
  1830. Enables displaying of all messages, including a list of created files.
  1831.  
  1832. =item B<-h>, B<--help>
  1833.  
  1834. Displays usage information and exits.
  1835.  
  1836. =item B<-v>, B<--version>
  1837.  
  1838. Displays version information and exits.
  1839.  
  1840. =back
  1841.  
  1842. =head1 FILES
  1843.  
  1844. =over
  1845.  
  1846. =item I<.blaze/theme/>
  1847.  
  1848. A directory containing blog themes.
  1849.  
  1850. =item I<.blaze/style/>
  1851.  
  1852. A directory containing style sheets.
  1853.  
  1854. =item B<.blaze/lang/>
  1855.  
  1856. A directory containing language files.
  1857.  
  1858. =back
  1859.  
  1860. =head1 EXAMPLE USAGE
  1861.  
  1862. Generate a blog in a current working directory:
  1863.  
  1864.   ~]$ blaze-make
  1865.   Done.
  1866.  
  1867. Generate a blog in the C<~/public_html/> directory:
  1868.  
  1869.   ~]$ blaze-make -d ~/public_html
  1870.   Done.
  1871.  
  1872. Generate a blog with full paths enabled:
  1873.  
  1874.   ~]$ blaze-make -F
  1875.   Done.
  1876.  
  1877. =head1 SEE ALSO
  1878.  
  1879. B<blaze-init>(1), B<blaze-config>(1), B<blaze-add>(1)
  1880.  
  1881. =head1 BUGS
  1882.  
  1883. To report a bug or to send a patch, please, add a new issue to the bug
  1884. tracker at <http://code.google.com/p/blazeblogger/issues/>, or visit the
  1885. discussion group at <http://groups.google.com/group/blazeblogger/>.
  1886.  
  1887. =head1 COPYRIGHT
  1888.  
  1889. Copyright (C) 2009-2011 Jaromir Hradilek
  1890.  
  1891. This program is free software; see the source for copying conditions. It is
  1892. distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  1893. without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  1894. PARTICULAR PURPOSE.
  1895.  
  1896. =cut
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement