summaryrefslogtreecommitdiff
path: root/lib/App/DocKnot/Spin
diff options
context:
space:
mode:
Diffstat (limited to 'lib/App/DocKnot/Spin')
-rw-r--r--lib/App/DocKnot/Spin/Pointer.pm428
-rw-r--r--lib/App/DocKnot/Spin/RSS.pm80
-rw-r--r--lib/App/DocKnot/Spin/Sitemap.pm11
-rw-r--r--lib/App/DocKnot/Spin/Thread.pm183
-rw-r--r--lib/App/DocKnot/Spin/Versions.pm8
5 files changed, 584 insertions, 126 deletions
diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm
new file mode 100644
index 0000000..05aa9ac
--- /dev/null
+++ b/lib/App/DocKnot/Spin/Pointer.pm
@@ -0,0 +1,428 @@
+# Generate HTML from a pointer to an external file.
+#
+# The input tree for spin may contain pointers to external files in various
+# formats. This module parses those pointer files and performs the conversion
+# of those external files into HTML.
+#
+# SPDX-License-Identifier: MIT
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package App::DocKnot::Spin::Pointer 6.00;
+
+use 5.024;
+use autodie;
+use parent qw(App::DocKnot);
+use warnings;
+
+use App::DocKnot::Config;
+use App::DocKnot::Util qw(is_newer print_fh);
+use Carp qw(croak);
+use Encode qw(decode encode);
+use File::BaseDir qw(config_files);
+use IPC::System::Simple qw(capturex);
+use Kwalify qw(validate);
+use POSIX qw(strftime);
+use Template ();
+use YAML::XS ();
+
+# The URL to the software page for all of my web page generation software,
+# used to embed a link to the software that generated the page.
+my $URL = 'https://www.eyrie.org/~eagle/software/web/';
+
+##############################################################################
+# Format conversions
+##############################################################################
+
+# Convert a Markdown file to HTML.
+#
+# $data_ref - Data from the pointer file
+# path - Path to the Markdown file to convert
+# style - Style sheet to use
+# $output - Path to the output file
+#
+# Throws: Text exception on conversion failure
+sub _spin_markdown {
+ my ($self, $data_ref, $output) = @_;
+ my $source = $data_ref->{path};
+
+ # Do the Markdown conversion using pandoc.
+ my $html = capturex(
+ $self->{pandoc_path}, '--wrap=preserve', '-f', 'markdown',
+ '-t', 'html', $source,
+ );
+
+ # Pull the title out of the contents of the <h1> header if not set.
+ my $title = $data_ref->{title};
+ if (!defined($title)) {
+ ($title) = $html =~ m{ <h1 [^>]+ > (.*?) </h1> }xms;
+ }
+
+ # Construct the template variables.
+ my ($links, $navbar, $style);
+ if ($self->{sitemap}) {
+ my $page = $output;
+ $page =~ s{ \A \Q$self->{output}\E }{}xms;
+ my @links = $self->{sitemap}->links($page);
+ if (@links) {
+ $links = join(q{}, @links);
+ }
+ my @navbar = $self->{sitemap}->navbar($page);
+ if (@navbar) {
+ $navbar = join(q{}, @navbar);
+ }
+ }
+ if ($data_ref->{style}) {
+ $style = $self->{style_url} . $data_ref->{style};
+ }
+ #<<<
+ my %vars = (
+ docknot_url => $URL,
+ html => decode('utf-8', $html),
+ links => $links,
+ modified => strftime('%Y-%m-%d', gmtime((stat($source))[9])),
+ navbar => $navbar,
+ now => strftime('%Y-%m-%d', gmtime()),
+ style => $style,
+ title => $title,
+ );
+ #>>>
+
+ # Construct the output page from those template variables.
+ my $result;
+ $self->{template}->process($self->{template_path}, \%vars, \$result)
+ or croak($self->{template}->error());
+
+ # Write the result to the output file.
+ open(my $outfh, '>', $output);
+ print_fh($outfh, $output, encode('utf-8', $result));
+ close($outfh);
+ return;
+}
+
+# Convert a POD file to HTML.
+#
+# $data_ref - Data from the pointer file
+# options - Hash of conversion options
+# contents - Whether to add a table of contents
+# navbar - Whether to add a navigation bar
+# path - Path to the POD file to convert
+# style - Style sheet to use
+# $output - Path to the output file
+#
+# Throws: Text exception on conversion failure
+sub _spin_pod {
+ my ($self, $data_ref, $output) = @_;
+ my $source = $data_ref->{path};
+
+ # Construct the Pod::Thread formatter object.
+ #<<<
+ my %options = (
+ contents => $data_ref->{options}{contents},
+ style => $data_ref->{style} // 'pod',
+ );
+ #<<<
+ if (exists($data_ref->{options}{navbar})) {
+ $options{navbar} = $data_ref->{options}{navbar};
+ } else {
+ $options{navbar} = 1;
+ }
+ if (exists($data_ref->{title})) {
+ $options{title} = $data_ref->{title};
+ }
+ my $podthread = Pod::Thread->new(%options);
+
+ # Convert the POD to thread.
+ my $data;
+ $podthread->output_string(\$data);
+ $podthread->parse_file($source);
+
+ # Spin that page into HTML.
+ $self->{thread}->spin_thread_output($data, $source, 'POD', $output);
+ return;
+}
+
+##############################################################################
+# Public interface
+##############################################################################
+
+# Create a new HTML converter for pointers. This object can (and should) be
+# reused for all pointer conversions done while spinning a tree of files.
+#
+# $args - Anonymous hash of arguments with the following keys:
+# output - Root of the output tree
+# sitemap - App::DocKnot::Spin::Sitemap object
+# style-url - Partial URL to style sheets
+# thread - App::DocKnot::Spin::Thread object
+#
+# Returns: Newly created object
+# Throws: Text exception on failure to initialize Template Toolkit
+sub new {
+ my ($class, $args_ref) = @_;
+
+ # Get the configured path to pandoc, if any.
+ my $config_reader = App::DocKnot::Config->new();
+ my $global_config_ref = $config_reader->global_config();
+ my $pandoc = $global_config_ref->{pandoc} // 'pandoc';
+
+ # Add a trailing slash to the partial URL for style sheets.
+ my $style_url = $args_ref->{'style-url'} // q{};
+ if ($style_url) {
+ $style_url =~ s{ /* \z }{/}xms;
+ }
+
+ # Create and return the object.
+ my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error());
+ #<<<
+ my $self = {
+ output => $args_ref->{output},
+ pandoc_path => $pandoc,
+ sitemap => $args_ref->{sitemap},
+ style_url => $style_url,
+ template => $tt,
+ thread => $args_ref->{thread},
+ };
+ #>>>
+ bless($self, $class);
+ $self->{template_path} = $self->appdata_path('templates', 'html.tmpl');
+ return $self;
+}
+
+# Check if the result of a pointer file needs to be regenerated.
+#
+# $pointer - Pointer file to process
+# $output - Corresponding output path
+#
+# Returns: True if the output file does not exist or has a modification date
+# older than either the pointer file or the underlying source file,
+# false otherwise
+# Throws: YAML::XS exception on invalid pointer
+sub is_out_of_date {
+ my ($self, $pointer, $output) = @_;
+ my $data_ref = $self->load_yaml_file($pointer, 'pointer');
+ if (!-e $data_ref->{path}) {
+ die "$pointer: path $data_ref->{path} does not exist\n";
+ }
+ return !is_newer($output, $pointer, $data_ref->{path});
+}
+
+# Process a given pointer file.
+#
+# $pointer - Pointer file to process
+# $output - Corresponding output path
+#
+# Throws: YAML::XS exception on invalid pointer
+# Text exception for missing input file
+# Text exception on failure to convert the file
+sub spin_pointer {
+ my ($self, $pointer, $output, $options_ref) = @_;
+ my $data_ref = $self->load_yaml_file($pointer, 'pointer');
+ $data_ref->{options} //= {};
+
+ # Dispatch to the appropriate conversion function.
+ if ($data_ref->{format} eq 'markdown') {
+ $self->_spin_markdown($data_ref, $output);
+ } elsif ($data_ref->{format} eq 'pod') {
+ $self->_spin_pod($data_ref, $output);
+ } else {
+ die "$pointer: unknown output format $data_ref->{format}\n";
+ }
+ return;
+}
+
+##############################################################################
+# Module return value and documentation
+##############################################################################
+
+1;
+
+__END__
+
+=for stopwords
+Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT Kwalify sublicense unstyled
+navbar
+
+=head1 NAME
+
+App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file
+
+=head1 SYNOPSIS
+
+ use App::DocKnot::Spin::Pointer;
+ use App::DocKnot::Spin::Sitemap;
+
+ my $sitemap = App::DocKnot::Spin::Sitemap->new('/input/.sitemap');
+ my $pointer = App::DocKnot::Spin::Pointer->new({
+ output => '/output',
+ sitemap => $sitemap,
+ });
+ $pointer->spin_pointer('/input/file.spin', '/output/file.html');
+
+=head1 REQUIREMENTS
+
+Perl 5.24 or later and the modules File::ShareDir, Kwalify, List::SomeUtils,
+Pod::Thread, and YAML::XS, all of which are available from CPAN.
+
+=head1 DESCRIPTION
+
+The tree of input files for App::DocKnot::Spin may contain pointers to
+external files in various formats. These files are in YAML format and end in
+C<.spin>. This module processes those files and converts them to HTML and, if
+so configured, adds the links to integrate the page with the rest of the site.
+
+For the details of the pointer file format, see L<POINTER FILES> below.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new(ARGS)
+
+Create a new App::DocKnot::Spin::Pointer object. A single converter object
+can be used repeatedly to convert pointers in a tree of files. ARGS should
+be a hash reference with one or more of the following keys, all of which are
+optional:
+
+=over 4
+
+=item output
+
+The path to the root of the output tree when converting a tree of files. This
+will be used to calculate relative path names for generating inter-page links
+using the provided C<sitemap> argument. If C<sitemap> is given, this option
+should also always be given.
+
+=item sitemap
+
+An App::DocKnot::Spin::Sitemap object. This will be used to create inter-page
+links. For inter-page links, the C<output> argument must also be provided.
+
+=item style-url
+
+The base URL for style sheets. A style sheet specified in a pointer file will
+be considered to be relative to this URL and this URL will be prepended to it.
+If this option is not given, the name of the style sheet will be used verbatim
+as its URL, except with C<.css> appended.
+
+=item thread
+
+An App::DocKnot::Spin::Thread object, used for converting POD into HTML. It
+should be configured with the same App::DocKnot::Spin::Sitemap object as the
+C<sitemap> argument.
+
+=back
+
+=back
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item is_out_of_date(POINTER, OUTPUT)
+
+Returns true if OUTPUT is missing or if it was modified less recently than the
+modification time of either POINTER or the underlying file that it points to.
+
+=item spin_pointer(POINTER, OUTPUT)
+
+Convert a single pointer file to HTML. POINTER is the path to the pointer
+file, and OUTPUT is the path to where to write the output.
+
+=back
+
+=head1 POINTER FILES
+
+A pointer file is a YAML file ending in C<.spin> that points to the source
+file for a generated HTML page and provides additional configuration for its
+conversion. The valid keys for a pointer file are:
+
+=over 4
+
+=item format
+
+The format of the source file. Supported values are C<markdown> and C<pod>.
+Required.
+
+=item path
+
+The path to the source file. It may be relative, in which case it's relative
+to the pointer file. Required.
+
+=item options
+
+Additional options that control the conversion to HTML. These will be
+different for each supported format.
+
+C<markdown> has no supported options.
+
+The supported options for a format of C<pod> are:
+
+=over 4
+
+=item contents
+
+Boolean saying whether to generate a table of contents. The default is false.
+
+=item navbar
+
+Boolean saying whether to generate a navigation bar at the top of the page.
+The default is true.
+
+=back
+
+=item style
+
+The style sheet to use for the converted output. Optional. If not set,
+converted C<markdown> output will be unstyled and converted C<pod> output will
+use a style sheet named C<pod>.
+
+=item title
+
+The title of the converted page. Optional. If not set, the title will be
+taken from the converted file in a format-specific way. For Markdown, the
+title will be the contents of the first top-level heading. For POD, the title
+will be taken from a NAME section formatted according to the conventions for
+manual pages.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <rra@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2021 Russ Allbery <rra@cpan.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+L<docknot(1)>, L<App::DocKnot::Spin>, L<App::DocKnot::Spin::Sitemap>
+
+This module is part of the App-DocKnot distribution. The current version of
+DocKnot is available from CPAN, or directly from its web site at
+L<https://www.eyrie.org/~eagle/software/docknot/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm
index 7599426..2a7460a 100644
--- a/lib/App/DocKnot/Spin/RSS.pm
+++ b/lib/App/DocKnot/Spin/RSS.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::RSS 5.00;
+package App::DocKnot::Spin::RSS 6.00;
use 5.024;
use autodie;
@@ -17,7 +17,9 @@ use warnings;
use App::DocKnot;
use App::DocKnot::Spin::Thread;
+use App::DocKnot::Util qw(print_checked print_fh);
use Cwd qw(getcwd);
+use Date::Language ();
use Date::Parse qw(str2time);
use File::Basename qw(fileparse);
use Perl6::Slurp qw(slurp);
@@ -27,30 +29,6 @@ use POSIX qw(strftime);
# Utility functions
##############################################################################
-# print with error checking. autodie unfortunately can't help us because
-# print can't be prototyped and hence can't be overridden.
-sub _print_checked {
- my (@args) = @_;
- print @args or croak('print failed');
- return;
-}
-
-# print with error checking and an explicit file handle. autodie
-# unfortunately can't help us because print can't be prototyped and hence
-# can't be overridden.
-#
-# $fh - Output file handle
-# $file - File name for error reporting
-# @args - Remaining arguments to print
-#
-# Returns: undef
-# Throws: Text exception on output failure
-sub _print_fh {
- my ($fh, $file, @args) = @_;
- print {$fh} @args or croak("cannot write to $file: $!");
- return;
-}
-
# Escapes &, <, and > characters for HTML or XML output.
#
# $string - Input string
@@ -154,7 +132,7 @@ sub _relative_url {
sub _spin_file {
my ($self, $file) = @_;
my $source = slurp($file);
- my $cwd = getcwd();
+ my $cwd = getcwd();
my (undef, $dir) = fileparse($file);
chdir($dir);
my $page = $self->{spin}->spin_thread($source);
@@ -174,7 +152,7 @@ sub _spin_file {
sub _read_rfc2822_file {
my ($self, $file) = @_;
my $key;
- my @blocks = ({});
+ my @blocks = ({});
my $current = $blocks[0];
# Parse the file. $key holds the last key seen, used to append
@@ -405,15 +383,16 @@ sub _rss_output {
# Determine the current date and latest publication date of all of the
# entries, published in the obnoxious format used by RSS.
+ my $lang = Date::Language->new('English');
my $format = '%a, %d %b %Y %H:%M:%S %z';
- my $now = strftime($format, localtime());
+ my $now = $lang->strftime($format, [localtime()]);
my $latest = $now;
if ($entries_ref->@*) {
$latest = strftime($format, localtime($entries_ref->[0]{date}));
}
# Output the RSS header.
- _print_fh($fh, $file, <<"EOC");
+ print_fh($fh, $file, <<"EOC");
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
@@ -428,18 +407,18 @@ EOC
if ($metadata_ref->{'rss-base'}) {
my ($name) = fileparse($file);
my $url = $metadata_ref->{'rss-base'} . $name;
- _print_fh(
+ print_fh(
$fh,
$file,
qq{ <atom:link href="$url" rel="self"\n},
qq{ type="application/rss+xml" />\n},
);
}
- _print_fh($fh, $file, "\n");
+ print_fh($fh, $file, "\n");
# Output each entry, formatting the contents of the entry as we go.
for my $entry_ref ($entries_ref->@*) {
- my $date = strftime($format, localtime($entry_ref->{date}));
+ my $date = $lang->strftime($format, [localtime($entry_ref->{date})]);
my $title = _escape($entry_ref->{title});
my $description;
if ($entry_ref->{description}) {
@@ -468,7 +447,7 @@ EOC
}
# Output the entry.
- _print_fh(
+ print_fh(
$fh,
$file,
" <item>\n",
@@ -484,7 +463,7 @@ EOC
}
# Close the RSS structure.
- _print_fh($fh, $file, " </channel>\n</rss>\n");
+ print_fh($fh, $file, " </channel>\n</rss>\n");
return;
}
@@ -503,9 +482,9 @@ sub _thread_output {
# Page prefix.
if ($metadata_ref->{'thread-prefix'}) {
- _print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n");
+ print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n");
} else {
- _print_fh(
+ print_fh(
$fh,
$file,
"\\heading[Recent Changes][indent]\n\n",
@@ -520,13 +499,13 @@ sub _thread_output {
# Put headings before each month.
if (!$last_month || $month ne $last_month) {
- _print_fh($fh, $file, "\\h2[$month]\n\n");
+ print_fh($fh, $file, "\\h2[$month]\n\n");
$last_month = $month;
}
# Format each entry.
my $date = strftime('%Y-%m-%d', localtime($entry_ref->{date}));
- _print_fh(
+ print_fh(
$fh,
$file,
"\\desc[$date \\entity[mdash]\n",
@@ -536,11 +515,11 @@ sub _thread_output {
my $description = $entry_ref->{description};
$description =~ s{ ^ }{ }xmsg;
$description =~ s{ \\ }{\\\\}xmsg;
- _print_fh($fh, $file, $description, "]\n\n");
+ print_fh($fh, $file, $description, "]\n\n");
}
# Print out the end of the page.
- _print_fh($fh, $file, "\\signature\n");
+ print_fh($fh, $file, "\\signature\n");
return;
}
@@ -649,14 +628,14 @@ sub _index_output {
# Output the prefix.
if ($metadata_ref->{'index-prefix'}) {
- _print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n");
+ print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n");
}
# Output each entry.
for my $entry_ref ($entries_ref->@*) {
my @time = localtime($entry_ref->{date});
my $date = strftime('%Y-%m-%d %H:%M', @time);
- my $day = strftime('%Y-%m-%d', @time);
+ my $day = strftime('%Y-%m-%d', @time);
# Get the text of the entry.
my $text;
@@ -679,7 +658,7 @@ sub _index_output {
}{$1 . _relative_url($2, $metadata_ref->{'index-base'}) . ']' }xmsge;
# Print out the entry.
- _print_fh(
+ print_fh(
$fh,
$file,
"\\h2[$day: $entry_ref->{title}]\n\n",
@@ -692,9 +671,9 @@ sub _index_output {
# Print out the end of the page.
if ($metadata_ref->{'index-suffix'}) {
- _print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n");
+ print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n");
}
- _print_fh($fh, $file, "\\signature\n");
+ print_fh($fh, $file, "\\signature\n");
return;
}
@@ -764,7 +743,7 @@ sub generate {
# Write the output.
if ($format eq 'thread') {
- _print_checked("Generating thread file $prettyfile\n");
+ print_checked("Generating thread file $prettyfile\n");
open(my $fh, '>', $path);
$self->_thread_output($fh, $path, $metadata_ref, \@entries);
close($fh);
@@ -772,7 +751,7 @@ sub generate {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- _print_checked("Generating RSS file $prettyfile\n");
+ print_checked("Generating RSS file $prettyfile\n");
open(my $fh, '>', $path);
$self->_rss_output($fh, $path, $metadata_ref, \@entries);
close($fh);
@@ -780,7 +759,7 @@ sub generate {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- _print_checked("Generating index file $prettyfile\n");
+ print_checked("Generating index file $prettyfile\n");
open(my $fh, '>', $path);
$self->_index_output($fh, $path, $metadata_ref, \@entries);
close($fh);
@@ -813,8 +792,9 @@ App::DocKnot::Spin::RSS - Generate RSS and thread from a feed description file
=head1 REQUIREMENTS
-Perl 5.006 or later and the modules Date::Parse (part of the TimeDate
-distribution) and Perl6::Slurp, both of which are available from CPAN.
+Perl 5.006 or later and the modules Date::Language, Date::Parse (both part of
+the TimeDate distribution), List::SomeUtils, and Perl6::Slurp, both of which
+are available from CPAN.
=head1 DESCRIPTION
diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm
index 038cc9f..22fbdd5 100644
--- a/lib/App/DocKnot/Spin/Sitemap.pm
+++ b/lib/App/DocKnot/Spin/Sitemap.pm
@@ -12,7 +12,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Sitemap 5.00;
+package App::DocKnot::Spin::Sitemap 6.00;
use 5.024;
use autodie;
@@ -141,7 +141,7 @@ sub _escape {
sub _relative {
my ($origin, $dest) = @_;
my @origin = split(qr{ / }xms, $origin, -1);
- my @dest = split(qr{ / }xms, $dest, -1);
+ my @dest = split(qr{ / }xms, $dest, -1);
# Remove the common prefix.
while (@origin && @dest && $origin[0] eq $dest[0]) {
@@ -207,11 +207,13 @@ sub new {
# links maps partial URLs to a list of other partial URLs (previous, next,
# and then the full upwards hierarchy to the top of the site) used for
# interpage links.
+ #<<<
my $self = {
links => {},
pagedesc => {},
sitemap => [],
};
+ #>>>
bless($self, $class);
# Parse the file into the newly-created object.
@@ -292,7 +294,7 @@ sub navbar {
# Construct the bread crumbs for the page hierarchy.
my @breadcrumbs = (" <td>\n");
- my $first = 1;
+ my $first = 1;
for my $parent (reverse(@parents)) {
my ($url, $desc) = $parent->@*;
my $prefix = q{ } x 4;
@@ -312,7 +314,6 @@ sub navbar {
@breadcrumbs,
$next_link,
"</tr></table>\n",
- "\n",
);
}
@@ -335,7 +336,7 @@ sub sitemap {
# Open or close <ul> elements as needed by the indentation.
if ($indent > $indents[-1]) {
- push(@output, (q{ } x $indent) . "<ul>\n");
+ push(@output, (q{ } x $indent) . "<ul>\n");
push(@indents, $indent);
} else {
while ($indent < $indents[-1]) {
diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm
index 492666d..bf58dc0 100644
--- a/lib/App/DocKnot/Spin/Thread.pm
+++ b/lib/App/DocKnot/Spin/Thread.pm
@@ -9,18 +9,20 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Thread 5.00;
+package App::DocKnot::Spin::Thread 6.00;
use 5.024;
use autodie;
use warnings;
use App::DocKnot;
+use App::DocKnot::Util qw(print_fh);
use Cwd qw(getcwd realpath);
use File::Basename qw(fileparse);
-use File::Spec ();
+use File::Spec ();
use Git::Repository ();
use Image::Size qw(html_imgsize);
+use Path::Tiny qw(path);
use Perl6::Slurp qw(slurp);
use POSIX qw(strftime);
use Text::Balanced qw(extract_bracketed);
@@ -34,6 +36,7 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/';
# 1. Number of arguments or -1 to consume as many arguments as it can find.
# 2. Name of the method to call with the arguments and (if wanted) format.
# 3. Whether to look for a format in parens before the arguments.
+#<<<
my %COMMANDS = (
# name args method want_format
block => [1, '_cmd_block', 1],
@@ -80,6 +83,7 @@ my %COMMANDS = (
q{==} => [3, '_define_macro', 0],
q{\\} => [0, '_literal', 0],
);
+#>>>
##############################################################################
# Input and output
@@ -104,22 +108,6 @@ sub _read_file {
return $text;
}
-# print with error checking and an explicit file handle. autodie
-# unfortunately can't help us because print can't be prototyped and hence
-# can't be overridden.
-#
-# $fh - Output file handle
-# $file - File name for error reporting
-# @args - Remaining arguments to print
-#
-# Returns: undef
-# Throws: Text exception on output failure
-sub _print_fh {
- my ($fh, $file, @args) = @_;
- print {$fh} @args or croak("cannot write to $file: $!");
- return;
-}
-
# Sends something to the output file with special handling of whitespace for
# more readable HTML output.
#
@@ -159,7 +147,7 @@ sub _output {
}
# Send the results to the output file.
- _print_fh($self->{out_fh}, $self->{out_path}, $output);
+ print_fh($self->{out_fh}, $self->{out_path}, $output);
return;
}
@@ -255,7 +243,7 @@ sub _paragraph {
# Returns: Output to write to start the structure
sub _border_start {
my ($self, $border, $start, $end) = @_;
- my $state = $self->{state}[-1];
+ my $state = $self->{state}[-1];
my $output = q{};
# If we're at the top-level block structure or inside a structure other
@@ -450,7 +438,7 @@ sub _expand {
my ($blocktag, $output) = $self->$handler($format, @args);
return ($output, $blocktag, $rest);
} else {
- my ($rest, @args) = $self->_extract($text, $args);
+ my ($rest, @args) = $self->_extract($text, $args);
my ($blocktag, $output) = $self->$handler(@args);
return ($output, $blocktag, $rest);
}
@@ -577,7 +565,7 @@ sub _parse_context {
if ($blocktag) {
if ($block && $paragraph ne q{}) {
$output .= $border . $self->_paragraph($paragraph);
- $border = q{};
+ $border = q{};
$paragraph = q{};
} else {
$output .= $space;
@@ -648,25 +636,29 @@ sub _parse {
# since thread may contain relative paths to files that the spinning process
# needs to access.
#
-# $thread - Thread to spin
-# $in_path - Input file path if any, used for error reporting
-# $out_fh - Output file handle to which to write the HTML
-# $out_path - Optional output file path for error reporting and page links
+# $thread - Thread to spin
+# $in_path - Input file path if any, used for error reporting
+# $out_fh - Output file handle to which to write the HTML
+# $out_path - Optional output file path for error reporting and page links
+# $input_type - Optional one-word description of input type
sub _parse_document {
- my ($self, $thread, $in_path, $out_fh, $out_path) = @_;
+ my ($self, $thread, $in_path, $out_fh, $out_path, $input_type) = @_;
# Parse the thread into paragraphs and reverse them to form a stack.
my @input = reverse($self->_split_paragraphs($thread));
# Initialize object state for a new document.
- $self->{input} = [[\@input, $in_path, 1]];
- $self->{macro} = {};
- $self->{out_fh} = $out_fh;
- $self->{out_path} = $out_path // q{-};
- $self->{rss} = [];
- $self->{space} = q{};
- $self->{state} = ['BLOCK'];
- $self->{variable} = {};
+ #<<<
+ $self->{input} = [[\@input, $in_path, 1]];
+ $self->{input_type} = $input_type // 'thread';
+ $self->{macro} = {};
+ $self->{out_fh} = $out_fh;
+ $self->{out_path} = $out_path // q{-};
+ $self->{rss} = [];
+ $self->{space} = q{};
+ $self->{state} = ['BLOCK'];
+ $self->{variable} = {};
+ #>>>
# Parse the thread file a paragraph at a time. _split_paragraphs takes
# care of ensuring that each paragraph contains the complete value of a
@@ -688,7 +680,7 @@ sub _parse_document {
}
# Close open tags and print any deferred whitespace.
- _print_fh($out_fh, $out_path, $self->_block_end(), $self->{space});
+ print_fh($out_fh, $out_path, $self->_block_end(), $self->{space});
return;
}
@@ -735,13 +727,13 @@ sub _split_paragraphs {
# Pull paragraphs off the text one by one.
while ($text ne q{} && $text =~ s{ \A ( .*? (?: \n\n+ | \s*\z ) )}{}xms) {
- my $para = $1;
- my $open_count = ($para =~ tr{\[}{});
+ my $para = $1;
+ my $open_count = ($para =~ tr{\[}{});
my $close_count = ($para =~ tr{\]}{});
while ($text ne q{} && $open_count > $close_count) {
if ($text =~ s{ \A ( .*? (?: \n\n+ | \s*\z ) )}{}xms) {
my $extra = $1;
- $open_count += ($extra =~ tr{\[}{});
+ $open_count += ($extra =~ tr{\[}{});
$close_count += ($extra =~ tr{\]}{});
$para .= $extra;
} else {
@@ -784,7 +776,7 @@ sub _block {
# Close the tag. The tag may have contained attributes, which aren't
# allowed in the closing tag.
- $tag =~ s{ [ ] .* }{}xms;
+ $tag =~ s{ [ ] .* }{}xms;
$output =~ s{ \s* \z }{</$tag>}xms;
if ($format ne 'packed') {
$output .= "\n";
@@ -930,6 +922,7 @@ sub _literal { return (0, q{\\}) }
##############################################################################
# Basic inline commands.
+#<<<
sub _cmd_break { return (0, '<br />') }
sub _cmd_bold { my ($self, @a) = @_; return $self->_inline('b', @a) }
sub _cmd_cite { my ($self, @a) = @_; return $self->_inline('cite', @a) }
@@ -942,6 +935,7 @@ sub _cmd_strong { my ($self, @a) = @_; return $self->_inline('strong', @a) }
sub _cmd_sub { my ($self, @a) = @_; return $self->_inline('sub', @a) }
sub _cmd_sup { my ($self, @a) = @_; return $self->_inline('sup', @a) }
sub _cmd_under { my ($self, @a) = @_; return $self->_inline('u', @a) }
+#>>>
# The headings.
sub _cmd_h1 { my ($self, @a) = @_; return $self->_heading(1, @a); }
@@ -990,8 +984,8 @@ sub _cmd_desc {
my ($self, $format, $heading, $text) = @_;
$heading = $self->_parse($heading);
my $format_attr = $self->_format_attr($format);
- my $border = $self->_border_start('desc', "<dl>\n", "</dl>\n\n");
- my $initial = $border . "<dt$format_attr>" . $heading . "</dt>\n";
+ my $border = $self->_border_start('desc', "<dl>\n", "</dl>\n\n");
+ my $initial = $border . "<dt$format_attr>" . $heading . "</dt>\n";
return $self->_block('dd', $initial, $format, $text);
}
@@ -1099,7 +1093,7 @@ sub _cmd_heading {
sub _cmd_image {
my ($self, $format, $image, $text) = @_;
$image = $self->_parse($image);
- $text = $self->_parse($text);
+ $text = $self->_parse($text);
# Determine the size attributes of the image if possible.
my $size = -e $image ? q{ } . lc(html_imgsize($image)) : q{};
@@ -1117,7 +1111,7 @@ sub _cmd_include {
$file = realpath($self->_parse($file));
# Read the thread, split it on paragraphs, and reverse it to make a stack.
- my $thread = $self->_read_file($file);
+ my $thread = $self->_read_file($file);
my @paragraphs = reverse($self->_split_paragraphs($thread));
# Add it to the file stack.
@@ -1134,7 +1128,7 @@ sub _cmd_include {
# $text - Anchor text
sub _cmd_link {
my ($self, $format, $url, $text) = @_;
- $url = $self->_parse($url);
+ $url = $self->_parse($url);
$text = $self->_parse($text);
my $format_attr = $self->_format_attr($format);
return (0, qq{<a href="$url"$format_attr>$text</a>});
@@ -1164,7 +1158,7 @@ sub _cmd_pre {
sub _cmd_quote {
my ($self, $format, $quote, $author, $cite) = @_;
$author = $self->_parse($author);
- $cite = $self->_parse($cite);
+ $cite = $self->_parse($cite);
my $output = $self->_border_end() . q{<blockquote class="quote">};
# Parse the contents of the quote in a new block context.
@@ -1238,7 +1232,7 @@ sub _cmd_release {
# directly; the RSS feed information is used later in _cmd_heading.
sub _cmd_rss {
my ($self, $url, $title) = @_;
- $url = $self->_parse($url);
+ $url = $self->_parse($url);
$title = $self->_parse($title);
push($self->{rss}->@*, [$url, $title]);
return (1, q{});
@@ -1251,9 +1245,9 @@ sub _cmd_signature {
my $source = $self->{input}[-1][1];
my $output = $self->_border_end();
- # If we're spinning from standard input, don't add any of the standard
- # footer, just close the HTML tags.
- if ($self->{input}[-1][1] eq q{-}) {
+ # If we're spinning from standard input to standard output, don't add any
+ # of the standard footer, just close the HTML tags.
+ if ($source eq q{-} && $self->{out_path} eq q{-}) {
$output .= "</body>\n</html>\n";
return (1, $output);
}
@@ -1262,27 +1256,33 @@ sub _cmd_signature {
if ($self->{sitemap} && $self->{output}) {
my $page = $self->{out_path};
$page =~ s{ \A \Q$self->{output}\E }{}xms;
- $output .= join(q{}, $self->{sitemap}->navbar($page));
+ $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
}
# Figure out the modification dates. Use the Git repository if available.
- my $now = strftime('%Y-%m-%d', gmtime());
- my $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9]));
+ my $now = strftime('%Y-%m-%d', gmtime());
+ my $modified = $now;
+ if ($source ne q{-}) {
+ $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9]));
+ }
if ($self->{repository} && $self->{source}) {
- my $repository = $self->{repository};
- $modified = $repository->run('log', '-1', '--format=%ct', $source);
- if ($modified) {
- $modified = strftime('%Y-%m-%d', gmtime($modified));
+ if (path($self->{source})->subsumes(path($source))) {
+ my $repository = $self->{repository};
+ $modified = $repository->run('log', '-1', '--format=%ct', $source);
+ if ($modified) {
+ $modified = strftime('%Y-%m-%d', gmtime($modified));
+ }
}
}
# Determine which template to use and substitute in the appropriate times.
- $output .= "<address>\n" . q{ } x 4;
+ $output .= "<address>\n";
my $link = qq{<a href="$URL">spun</a>};
if ($modified eq $now) {
- $output .= "Last modified and\n $link $modified\n";
+ $output .= " Last modified and\n $link $modified\n";
} else {
- $output .= "Last $link\n $now from thread modified $modified\n";
+ $output .= " Last $link\n";
+ $output .= " $now from $self->{input_type} modified $modified\n";
}
# Close out the document.
@@ -1306,7 +1306,7 @@ sub _cmd_size {
# Format the size using SI units.
my @suffixes = qw(K M G T);
- my $suffix = q{};
+ my $suffix = q{};
while ($size > 1024 && @suffixes) {
$size /= 1024;
$suffix = shift(@suffixes);
@@ -1410,6 +1410,7 @@ sub new {
}
# Create and return the object.
+ #<<<
my $self = {
output => $args_ref->{output},
repository => $repository,
@@ -1418,6 +1419,7 @@ sub new {
style_url => $style_url,
versions => $args_ref->{versions},
};
+ #>>>
bless($self, $class);
return $self;
}
@@ -1453,12 +1455,12 @@ sub spin_thread_file {
# ensure that relative file references resolve properly.
if (defined($input)) {
my $path = realpath($input) or die "cannot canonicalize $input: $!\n";
- $input = $path;
+ $input = $path;
$thread = slurp($input);
my (undef, $input_dir) = fileparse($input);
chdir($input_dir);
} else {
- $input = q{-};
+ $input = q{-};
$thread = slurp(\*STDIN);
}
@@ -1482,6 +1484,39 @@ sub spin_thread_file {
return;
}
+# Convert thread to HTML and write it to the given output file. This is used
+# when the thread isn't part of the input tree but instead is intermediate
+# output from some other conversion process.
+#
+# $thread - Thread to spin
+# $input - Original input file (for modification timestamps)
+# $input_type - One-word description of input type for the page footer
+# $output - Output file
+#
+# Returns: Resulting HTML
+sub spin_thread_output {
+ my ($self, $thread, $input, $input_type, $output) = @_;
+
+ # Open the output file.
+ my $out_fh;
+ if (defined($output)) {
+ my $path = realpath($output)
+ or die "cannot canonicalize $output: $!\n";
+ $output = $path;
+ open($out_fh, '>', $output);
+ } else {
+ $output = q{-};
+ open($out_fh, '>&', 'STDOUT');
+ }
+
+ # Do the work.
+ $self->_parse_document($thread, $input, $out_fh, $output, $input_type);
+
+ # Clean up and restore the working directory.
+ close($out_fh);
+ return;
+}
+
##############################################################################
# Module return value and documentation
##############################################################################
@@ -1501,8 +1536,9 @@ App::DocKnot::Spin::Thread - Generate HTML from the macro language thread
use App::DocKnot::Spin::Thread;
+ my $input = 'some thread';
my $thread = App::DocKnot::Spin::Thread->new();
- $thread->spin_file('/path/to/file.th', '/path/to/file.html');
+ my $output = $thread->spin_thread($input);
use App::DocKnot::Spin::Sitemap;
use App::DocKnot::Spin::Versions;
@@ -1515,12 +1551,15 @@ App::DocKnot::Spin::Thread - Generate HTML from the macro language thread
sitemap => $sitemap,
versions => $versions,
});
- $thread->spin_file('/input/file.th', '/output/file.th');
+ $thread->spin_thread_file('/input/file.th', '/output/file.html');
+ $thread->spin_thread_output(
+ $input, '/path/to/file.pod', 'POD', '/output/file.html'
+ );
=head1 REQUIREMENTS
-Perl 5.24 or later and the modules Git::Repository and Image::Size, both of
-which are available from CPAN.
+Perl 5.24 or later and the modules Git::Repository, Image::Size,
+List::SomeUtils, and Path::Tiny, all of which are available from CPAN.
=head1 DESCRIPTION
@@ -1601,6 +1640,16 @@ If OUTPUT is omitted, App::DocKnot::Spin::Thread will not be able to obtain
sitemap information even if a sitemap was provided and therefore will not add
inter-page links.
+=item spin_thread_output(THREAD, INPUT, TYPE[, OUTPUT])
+
+Convert the given thread to HTML, writing the result to OUTPUT. If OUTPUT is
+not given, write the results to standard output. This is like spin_thread()
+but does use sitemap information and adds inter-page links. It should be used
+when the thread input is the result of an intermediate conversion step of a
+known input file. INPUT should be the full path to the original source file,
+used for modification time information. TYPE should be set to a one-word
+description of the format of the input file and is used for the page footer.
+
=back
=head1 THREAD LANGUAGE
diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm
index b804b08..679b368 100644
--- a/lib/App/DocKnot/Spin/Versions.pm
+++ b/lib/App/DocKnot/Spin/Versions.pm
@@ -12,7 +12,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Versions 5.00;
+package App::DocKnot::Spin::Versions 6.00;
use 5.024;
use autodie;
@@ -84,9 +84,9 @@ sub _read_data {
if (!defined($time)) {
die "invalid line $. in $path\n";
}
- @depends = @files;
+ @depends = @files;
$timestamp = _datetime_to_seconds($date, $time, $path);
- $date = strftime('%Y-%m-%d', gmtime($timestamp));
+ $date = strftime('%Y-%m-%d', gmtime($timestamp));
$self->{versions}{$package} = [$version, $date];
}
@@ -120,7 +120,7 @@ sub new {
# Create an empty object.
my $self = {
- depends => {},
+ depends => {},
versions => {},
};
bless($self, $class);