summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2021-12-31 17:23:11 -0800
committerRuss Allbery <rra@cpan.org>2021-12-31 17:23:11 -0800
commit84cd6a8fb1982d80132cfa675f397c0cb59148ab (patch)
tree8f94b307c3b6b0200b1d59bc6e5d792b217a67cb
parent5c26c088aa5963a1000a437c291f8542ed947eb4 (diff)
Use Path::Tiny and Path::Iterator::Rule
docknot spin now uses Path::Iterator::Rule and Path::Tiny to construct its paths, which eliminates the need to change the working directory while processing input files.
-rw-r--r--Build.PL3
-rw-r--r--Changes4
-rw-r--r--README3
-rw-r--r--README.md3
-rw-r--r--cpanfile3
-rw-r--r--docs/docknot.yaml3
-rw-r--r--lib/App/DocKnot/Command.pm5
-rw-r--r--lib/App/DocKnot/Spin.pm259
-rw-r--r--lib/App/DocKnot/Spin/Pointer.pm55
-rw-r--r--lib/App/DocKnot/Spin/RSS.pm135
-rw-r--r--lib/App/DocKnot/Spin/Thread.pm25
-rw-r--r--t/data/generate/docknot/output/thread3
-rw-r--r--t/data/spin/input/journal/.rss4
-rwxr-xr-xt/spin/tree.t26
14 files changed, 284 insertions, 247 deletions
diff --git a/Build.PL b/Build.PL
index 19dd784..a4c8d70 100644
--- a/Build.PL
+++ b/Build.PL
@@ -74,7 +74,8 @@ my $build = Module::Build->new(
'JSON::MaybeXS' => 0,
'Kwalify' => 0,
'List::SomeUtils' => '0.07',
- 'Path::Tiny' => 0,
+ 'Path::Iterator::Rule' => 0,
+ 'Path::Tiny' => '0.101',
'Perl6::Slurp' => 0,
'Pod::Thread' => '3.01',
'Template' => 0,
diff --git a/Changes b/Changes
index 49c9a19..1a2741d 100644
--- a/Changes
+++ b/Changes
@@ -2,6 +2,10 @@
6.01 - Not Released
+ - docknot spin now uses Path::Iterator::Rule and Path::Tiny to construct
+ its paths, which eliminates the need to change the working directory
+ while processing input files.
+
- Fix spurious requirement for a package metadata file when running
docknot spin.
diff --git a/README b/README
index 35d5a12..a5c138f 100644
--- a/README
+++ b/README
@@ -64,7 +64,8 @@ REQUIREMENTS
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
- * Path::Tiny
+ * Path::Iterator::Rule
+ * Path::Tiny 0.101 or later
* Perl6::Slurp
* Pod::Thread 3.01 or later
* Template (part of Template Toolkit)
diff --git a/README.md b/README.md
index 10454d7..4ab09e4 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,8 @@ The following additional Perl modules are required to use it:
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
-* Path::Tiny
+* Path::Iterator::Rule
+* Path::Tiny 0.101 or later
* Perl6::Slurp
* Pod::Thread 3.01 or later
* Template (part of Template Toolkit)
diff --git a/cpanfile b/cpanfile
index 22a0136..de7a654 100644
--- a/cpanfile
+++ b/cpanfile
@@ -12,7 +12,8 @@ requires 'IPC::System::Simple';
requires 'JSON::MaybeXS';
requires 'Kwalify';
requires 'List::SomeUtils', '0.07';
-requires 'Path::Tiny';
+requires 'Path::Iterator::Rule';
+requires 'Path::Tiny', '0.101';
requires 'Perl6::Slurp';
requires 'Pod::Thread', '3.00';
requires 'Template';
diff --git a/docs/docknot.yaml b/docs/docknot.yaml
index a64fdb3..044acd6 100644
--- a/docs/docknot.yaml
+++ b/docs/docknot.yaml
@@ -141,7 +141,8 @@ requirements: |
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
- * Path::Tiny
+ * Path::Iterator::Rule
+ * Path::Tiny 0.101 or later
* Perl6::Slurp
* Pod::Thread 3.01 or later
* Template (part of Template Toolkit)
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm
index 7ef45e5..fdbc184 100644
--- a/lib/App/DocKnot/Command.pm
+++ b/lib/App/DocKnot/Command.pm
@@ -297,8 +297,9 @@ Perl 5.24 or later and the modules Date::Language, Date::Parse (both part of
TimeDate), File::BaseDir, File::ShareDir, Git::Repository, Image::Size,
IO::Compress::Xz (part of IO-Compress-Lzma), IO::Uncompress::Gunzip (part of
IO-Compress), IPC::Run, IPC::System::Simple, JSON::MaybeXS, Kwalify,
-List::SomeUtils, Path::Tiny, Perl6::Slurp, Template (part of Template
-Toolkit), and YAML::XS, all of which are available from CPAN.
+List::SomeUtils, Path::Iterator::Rule, Path::Tiny, Perl6::Slurp, Template
+(part of Template Toolkit), and YAML::XS, all of which are available from
+CPAN.
=head1 DESCRIPTION
diff --git a/lib/App/DocKnot/Spin.pm b/lib/App/DocKnot/Spin.pm
index 73666e0..d95a1dd 100644
--- a/lib/App/DocKnot/Spin.pm
+++ b/lib/App/DocKnot/Spin.pm
@@ -23,14 +23,10 @@ use App::DocKnot::Spin::Sitemap;
use App::DocKnot::Spin::Thread;
use App::DocKnot::Spin::Versions;
use App::DocKnot::Util qw(is_newer print_checked print_fh);
-use Carp qw(croak);
-use Cwd qw(getcwd realpath);
-use File::Basename qw(fileparse);
-use File::Copy qw(copy);
-use File::Find qw(find finddepth);
-use File::Spec ();
use Git::Repository ();
use IPC::System::Simple qw(capture);
+use Path::Iterator::Rule ();
+use Path::Tiny qw(path);
use Pod::Thread 3.00 ();
use POSIX qw(strftime);
@@ -53,8 +49,8 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/';
# Build te page footer, which consists of the navigation links, the regular
# signature, and the last modified date.
#
-# $source - Full path to the source file
-# $out_path - Full path to the output file
+# $source - Path::Tiny path to the source file
+# $out_path - Path::Tiny path to the output file
# $id - CVS Id of the source file or undef if not known
# @templates - Two templates to use. The first will be used if the
# modification and current dates are the same, and the second
@@ -67,15 +63,14 @@ sub _footer {
my ($self, $source, $out_path, $id, @templates) = @_;
my $output = q{};
my $in_tree = 0;
- if ($self->{source} && $source =~ m{ \A \Q$self->{source}\E }xms) {
+ if ($self->{source} && $self->{source}->subsumes($source)) {
$in_tree = 1;
}
# Add the end-of-page navbar if we have sitemap information.
if ($self->{sitemap} && $self->{output}) {
- my $page = $out_path;
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
+ my $page = $out_path->relative($self->{output});
+ $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n";
}
# Figure out the modification dates. Use the RCS/CVS Id if available,
@@ -88,13 +83,13 @@ sub _footer {
}
} elsif ($self->{repository} && $in_tree) {
$modified
- = $self->{repository}->run('log', '-1', '--format=%ct', $source);
+ = $self->{repository}->run('log', '-1', '--format=%ct', "$source");
if ($modified) {
$modified = strftime('%Y-%m-%d', gmtime($modified));
}
}
if (!$modified) {
- $modified = strftime('%Y-%m-%d', gmtime((stat $source)[9]));
+ $modified = strftime('%Y-%m-%d', gmtime($source->stat()->[9]));
}
my $now = strftime('%Y-%m-%d', gmtime());
@@ -121,9 +116,8 @@ sub _footer {
# the output of an external converter.
sub _write_converter_output {
my ($self, $page_ref, $output, $footer) = @_;
- my $page = $output;
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- open(my $out_fh, '>', $output);
+ my $page = $output->relative($self->{output});
+ my $out_fh = $output->openw_utf8();
# Grab the first few lines of input, looking for a blurb and Id string.
# Give up if we encounter <body> first. Also look for a </head> tag and
@@ -211,7 +205,8 @@ sub _cvs2xhtml {
$style ||= $self->{style_url} . 'cvs.css';
# Separate the source file into a directory and filename.
- my ($name, $dir) = fileparse($source);
+ my $name = $source->basename();
+ my $dir = $source->parent();
# Construct the options to cvs2xhtml.
if ($options !~ m{ -n [ ] }xms) {
@@ -274,14 +269,12 @@ sub _pod2html {
# Grab the thread output.
my $data;
$podthread->output_string(\$data);
- $podthread->parse_file($source);
+ $podthread->parse_file("$source");
# Spin that thread into HTML.
my $page = $self->{thread}->spin_thread($data);
# Push the result through _write_converter_output.
- my $file = $source;
- $file =~ s{ [.] [^.]+ \z }{.html}xms;
my $footer = sub {
my ($blurb) = @_;
my $link = '<a href="%URL%">spun</a>';
@@ -305,7 +298,7 @@ sub _pod2html {
# Given a pointer file, read the master file name and any options, returning
# them as a list with the newlines chomped off.
#
-# $file - The path to the file to read
+# $file - Path::Tiny for the file to read
#
# Returns: List of the master file, any command-line options, and the style
# sheet to use, as strings
@@ -315,11 +308,7 @@ sub _read_pointer {
my ($self, $file) = @_;
# Read the pointer file.
- open(my $pointer, '<', $file);
- my $master = <$pointer>;
- my $options = <$pointer>;
- my $style = <$pointer>;
- close($pointer);
+ my ($master, $options, $style) = $file->lines_utf8();
# Clean up the contents.
if (!$master) {
@@ -339,29 +328,42 @@ sub _read_pointer {
return ($master, $options, $style);
}
-# This routine is called by File::Find for every file in the source tree. It
-# decides what to do with each file, whether spinning it or copying it.
+# Convert an input path to an output path.
+#
+# $input - Path::Tiny input path
+# $extension - If given, remove this extension and add .html in its place
+sub _output_for_file {
+ my ($self, $input, $extension) = @_;
+ my $output = $input->relative($self->{source})->absolute($self->{output});
+ if ($extension) {
+ my $output_file = $input->basename($extension) . '.html';
+ $output = $output->sibling($output_file);
+ }
+ return $output;
+}
+
+# Report an action to standard output.
+#
+# $action - String description of the action
+# $output - Output file generated
+sub _report_action {
+ my ($self, $action, $output) = @_;
+ my $shortout = $output->relative($self->{output});
+ print_checked("$action .../$shortout\n");
+ return;
+}
+
+# This routine is called for every file in the source tree. It decides what
+# to do with each file, whether spinning it or copying it.
+#
+# $input - Path::Tiny path to the input file
#
# Throws: Text exception on any processing error
# autodie exception if files could not be accessed or written
#
## no critic (Subroutines::ProhibitExcessComplexity)
sub _process_file {
- my ($self) = @_;
- my $file = $_;
- return if $file eq q{.};
- for my $regex ($self->{excludes}->@*) {
- if ($file =~ m{$regex}xms) {
- $File::Find::prune = 1;
- return;
- }
- }
- my $input = $File::Find::name;
- my $output = $input;
- $output =~ s{ \A \Q$self->{source}\E }{$self->{output}}xms
- or die "input file $file out of tree\n";
- my $shortout = $output;
- $shortout =~ s{ \A \Q$self->{output}\E }{...}xms;
+ my ($self, $input) = @_;
# Conversion rules for pointers. The key is the extension, the first
# value is the name of the command for the purposes of output, and the
@@ -376,87 +378,84 @@ sub _process_file {
#>>>
# Figure out what to do with the input.
- if (-d $file) {
- $self->{generated}{$output} = 1;
- if (-e $output && !-d $output) {
+ if ($input->is_dir()) {
+ my $output = $self->_output_for_file($input);
+ $self->{generated}{"$output"} = 1;
+ if ($output->exists() && !$output->is_dir()) {
die "cannot replace $output with a directory\n";
- } elsif (!-d $output) {
- print_checked("Creating $shortout\n");
- mkdir($output, 0755);
+ } elsif (!$output->is_dir()) {
+ $self->_report_action('Creating', $output);
+ $output->mkpath();
}
- my $rss_path = File::Spec->catfile($file, '.rss');
- if (-e $rss_path) {
- $self->{rss}->generate($rss_path, $file);
+ my $rss_path = path($input, '.rss');
+ if ($rss_path->exists()) {
+ $self->{rss}->generate("$rss_path", "$input");
}
- } elsif ($file =~ m{ [.] spin \z }xms) {
- $output =~ s{ [.] spin \z }{.html}xms;
- $shortout =~ s{ [.] spin \z }{.html}xms;
- $self->{generated}{$output} = 1;
- if ($self->{pointer}->is_out_of_date($input, $output)) {
- print_checked("Converting $shortout\n");
- $self->{pointer}->spin_pointer($input, $output);
+ } elsif ($input->basename() =~ m{ [.] spin \z }xms) {
+ my $output = $self->_output_for_file($input, '.spin');
+ $self->{generated}{"$output"} = 1;
+ if ($self->{pointer}->is_out_of_date("$input", "$output")) {
+ $self->_report_action('Converting', $output);
+ $self->{pointer}->spin_pointer("$input", "$output");
}
- } elsif ($file =~ m{ [.] th \z }xms) {
- $output =~ s{ [.] th \z }{.html}xms;
- $shortout =~ s{ [.] th \z }{.html}xms;
- $self->{generated}{$output} = 1;
+ } elsif ($input->basename() =~ m{ [.] th \z }xms) {
+ my $output = $self->_output_for_file($input, '.th');
+ $self->{generated}{"$output"} = 1;
# See if we're forced to regenerate the file because it is affected by
# a software release.
- if (-e $output && $self->{versions}) {
- my $relative = $input;
- $relative =~ s{ ^ \Q$self->{source}\E / }{}xms;
- my $time = $self->{versions}->latest_release($relative);
- return if is_newer($output, $file) && (stat($output))[9] >= $time;
+ if ($output->exists() && $self->{versions}) {
+ my $relative = $input->relative($self->{source});
+ my $time = $self->{versions}->latest_release("$relative");
+ return
+ if is_newer("$output", "$input")
+ && $output->stat()->[9] >= $time;
} else {
- return if is_newer($output, $file);
+ return if is_newer("$output", "$input");
}
# The output file is not newer. Respin it.
- print_checked("Spinning $shortout\n");
+ $self->_report_action('Spinning', $output);
$self->{thread}->spin_thread_file($input, $output);
} else {
- my ($extension) = ($file =~ m{ [.] ([^.]+) \z }xms);
+ my ($extension) = ($input->basename =~ m{ [.] ([^.]+) \z }xms);
if (defined($extension) && $rules{$extension}) {
my ($name, $sub) = $rules{$extension}->@*;
- $output =~ s{ [.] \Q$extension\E \z }{.html}xms;
- $shortout =~ s{ [.] \Q$extension\E \z }{.html}xms;
- $self->{generated}{$output} = 1;
+ my $output = $self->_output_for_file($input, $extension);
+ $self->{generated}{"$output"} = 1;
my ($source, $options, $style) = $self->_read_pointer($input);
return if is_newer($output, $input, $source);
- print_checked("Running $name for $shortout\n");
+ $self->_report_action("Running $name for", $output);
$self->$sub($source, $output, $options, $style);
} else {
- $self->{generated}{$output} = 1;
- return if is_newer($output, $file);
- print_checked("Updating $shortout\n");
- copy($file, $output)
- or die "copy of $input to $output failed: $!\n";
+ my $output = $self->_output_for_file($input);
+ $self->{generated}{"$output"} = 1;
+ return if is_newer("$output", "$input");
+ $self->_report_action('Updating', $output);
+ $input->copy($output);
}
}
return;
}
## use critic
-# This routine is called by File::Find for every file in the destination tree
-# in depth-first order, if the user requested file deletion of files not
-# generated from the source tree. It checks each file to see if it is in the
-# $self->{generated} hash that was generated during spin processing, and if
-# not, removes it.
+# This routine is called for every file in the destination tree in depth-first
+# order, if the user requested file deletion of files not generated from the
+# source tree. It checks each file to see if it is in the $self->{generated}
+# hash that was generated during spin processing, and if not, removes it.
+#
+# $file - Path::Tiny path to the file
#
# Throws: autodie exception on failure of rmdir or unlink
sub _delete_files {
- my ($self) = @_;
- return if $_ eq q{.};
- my $file = $File::Find::name;
- return if $self->{generated}{$file};
- my $shortfile = $file;
- $shortfile =~ s{ ^ \Q$self->{output}\E }{...}xms;
- print_checked("Deleting $shortfile\n");
- if (-d $file) {
+ my ($self, $file) = @_;
+ return if $self->{generated}{"$file"};
+ my $shortfile = $file->relative($self->{output});
+ print_checked("Deleting .../$shortfile\n");
+ if ($file->is_dir()) {
rmdir($file);
} else {
- unlink($file);
+ $file->remove();
}
return;
}
@@ -495,7 +494,6 @@ sub new {
my $self = {
delete => $args_ref->{delete},
excludes => [@excludes],
- rss => App::DocKnot::Spin::RSS->new(),
style_url => $style_url,
};
#>>>
@@ -514,44 +512,48 @@ sub spin {
# Reset data from a previous run.
delete $self->{repository};
+ delete $self->{rss};
delete $self->{sitemap};
delete $self->{versions};
# Canonicalize and check input.
- $input = realpath($input) or die "cannot canonicalize $input: $!\n";
- if (!-d $input) {
+ $input = path($input)->realpath();
+ if (!$input->is_dir()) {
die "input tree $input must be a directory\n";
}
$self->{source} = $input;
# Canonicalize and check output.
- if (!-d $output) {
- print_checked("Creating $output\n");
- mkdir($output, 0755);
+ $output = path($output);
+ if (!$output->is_dir()) {
+ for my $created ($output->mkpath()) {
+ print_checked("Creating $created\n");
+ }
}
- $output = realpath($output) or die "cannot canonicalize $output: $!\n";
+ $output = $output->realpath();
$self->{output} = $output;
# Read metadata from the top of the input directory.
- my $sitemap_path = File::Spec->catfile($input, '.sitemap');
- if (-e $sitemap_path) {
- $self->{sitemap} = App::DocKnot::Spin::Sitemap->new($sitemap_path);
+ my $sitemap_path = $input->child('.sitemap');
+ if ($sitemap_path->exists()) {
+ $self->{sitemap} = App::DocKnot::Spin::Sitemap->new("$sitemap_path");
}
- my $versions_path = File::Spec->catfile($input, '.versions');
- if (-e $versions_path) {
- $self->{versions} = App::DocKnot::Spin::Versions->new($versions_path);
+ my $versions_path = $input->child('.versions');
+ if ($versions_path->exists()) {
+ $self->{versions}
+ = App::DocKnot::Spin::Versions->new("$versions_path");
}
- if (-d File::Spec->catdir($input, '.git')) {
+ if ($input->child('.git')->is_dir()) {
$self->{repository} = Git::Repository->new(work_tree => $input);
}
+ # Create a new RSS generator object.
+ $self->{rss} = App::DocKnot::Spin::RSS->new({ base => $input });
+
# Process an .rss file at the top of the tree, if present.
- my $rss_path = File::Spec->catfile($input, '.rss');
- if (-e $rss_path) {
- my $cwd = getcwd();
- chdir($input);
- $self->{rss}->generate($rss_path);
- chdir($cwd);
+ my $rss_path = $input->child('.rss');
+ if ($rss_path->exists()) {
+ $self->{rss}->generate("$rss_path", "$input");
}
# Create a new thread converter object.
@@ -571,7 +573,7 @@ sub spin {
#<<<
$self->{pointer} = App::DocKnot::Spin::Pointer->new(
{
- output => $output,
+ output => "$output",
sitemap => $self->{sitemap},
'style-url' => $self->{style_url},
thread => $self->{thread},
@@ -580,11 +582,20 @@ sub spin {
#>>>
# Process the input tree.
- my $preprocess = sub { my @files = sort(@_); return @files };
- my $wanted = sub { $self->_process_file(@_) };
- find({ preprocess => $preprocess, wanted => $wanted }, $input);
+ my $rule = Path::Iterator::Rule->new();
+ $rule = $rule->skip($rule->new()->name($self->{excludes}->@*));
+ my $iter = $rule->iter("$input", { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ $self->_process_file(path($file));
+ }
+
+ # Remove stray files from the output tree.
if ($self->{delete}) {
- finddepth(sub { $self->_delete_files(@_) }, $output);
+ my %options = (depthfirst => 1, follow_symlinks => 0);
+ $iter = $rule->iter("$output", \%options);
+ while (defined(my $file = $iter->())) {
+ $self->_delete_files(path($file));
+ }
}
return;
}
@@ -614,10 +625,10 @@ App::DocKnot::Spin - Static site builder supporting thread macro language
=head1 REQUIREMENTS
Perl 5.24 or later and the modules Git::Repository, Image::Size,
-List::SomeUtils, Path::Tiny, Pod::Thread, Template (part of Template Toolkit),
-and YAML::XS, all of which are available from CPAN. Also expects to find
-B<faq2html>, B<cvs2xhtml>, and B<cl2xhtml> on the user's PATH to convert
-certain types of files.
+List::SomeUtils, Path::Iterator::Rule, Path::Tiny, Pod::Thread, Template (part
+of Template Toolkit), and YAML::XS, all of which are available from CPAN.
+Also expects to find B<faq2html>, B<cvs2xhtml>, and B<cl2xhtml> on the user's
+PATH to convert certain types of files.
=head1 DESCRIPTION
diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm
index aaab6af..b08c193 100644
--- a/lib/App/DocKnot/Spin/Pointer.pm
+++ b/lib/App/DocKnot/Spin/Pointer.pm
@@ -18,12 +18,12 @@ use parent qw(App::DocKnot);
use warnings;
use App::DocKnot::Config;
-use App::DocKnot::Util qw(is_newer print_fh);
+use App::DocKnot::Util qw(is_newer);
use Carp qw(croak);
-use Encode qw(decode encode);
+use Encode qw(decode);
use File::BaseDir qw(config_files);
use IPC::System::Simple qw(capturex);
-use Kwalify qw(validate);
+use Path::Tiny qw(path);
use POSIX qw(strftime);
use Template ();
use YAML::XS ();
@@ -41,12 +41,13 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/';
# $data_ref - Data from the pointer file
# path - Path to the Markdown file to convert
# style - Style sheet to use
+# $base - Base path of pointer file (for relative paths)
# $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};
+ my ($self, $data_ref, $base, $output) = @_;
+ my $source = path($data_ref->{path})->absolute($base);
# Do the Markdown conversion using pandoc.
my $html = capturex(
@@ -63,13 +64,12 @@ sub _spin_markdown {
# 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);
+ my $page = $output->relative($self->{output});
+ my @links = $self->{sitemap}->links("/$page");
if (@links) {
$links = join(q{}, @links);
}
- my @navbar = $self->{sitemap}->navbar($page);
+ my @navbar = $self->{sitemap}->navbar("/$page");
if (@navbar) {
$navbar = join(q{}, @navbar);
}
@@ -82,7 +82,7 @@ sub _spin_markdown {
docknot_url => $URL,
html => decode('utf-8', $html),
links => $links,
- modified => strftime('%Y-%m-%d', gmtime((stat($source))[9])),
+ modified => strftime('%Y-%m-%d', gmtime($source->stat()->[9])),
navbar => $navbar,
now => strftime('%Y-%m-%d', gmtime()),
style => $style,
@@ -96,9 +96,7 @@ sub _spin_markdown {
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);
+ $output->spew_utf8($result);
return;
}
@@ -110,12 +108,13 @@ sub _spin_markdown {
# navbar - Whether to add a navigation bar
# path - Path to the POD file to convert
# style - Style sheet to use
+# $base - Base path of pointer file (for relative paths)
# $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};
+ my ($self, $data_ref, $base, $output) = @_;
+ my $source = path($data_ref->{path})->absolute($base);
# Construct the Pod::Thread formatter object.
#<<<
@@ -135,7 +134,7 @@ sub _spin_pod {
# Convert the POD to thread.
my $data;
$podthread->output_string(\$data);
- $podthread->parse_file($source);
+ $podthread->parse_file("$source");
# Spin that page into HTML.
$self->{thread}->spin_thread_output($data, $source, 'POD', $output);
@@ -190,8 +189,8 @@ sub new {
# Check if the result of a pointer file needs to be regenerated.
#
-# $pointer - Pointer file to process
-# $output - Corresponding output path
+# $pointer - Path to pointer file
+# $output - Path to corresponding output file
#
# 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,
@@ -199,31 +198,35 @@ sub new {
# Throws: YAML::XS exception on invalid pointer
sub is_out_of_date {
my ($self, $pointer, $output) = @_;
+ $pointer = path($pointer);
my $data_ref = $self->load_yaml_file($pointer, 'pointer');
- if (!-e $data_ref->{path}) {
- die "$pointer: path $data_ref->{path} does not exist\n";
+ my $path = path($data_ref->{path})->absolute($pointer->parent());
+ if (!$path->exists()) {
+ die "$pointer: path $data_ref->{path} ($path) does not exist\n";
}
- return !is_newer($output, $pointer, $data_ref->{path});
+ return !is_newer($output, $pointer, $path);
}
# Process a given pointer file.
#
-# $pointer - Pointer file to process
-# $output - Corresponding output path
+# $pointer - Path to pointer file to process
+# $output - Path to corresponding output file
#
# 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) = @_;
+ $pointer = path($pointer);
+ $output = path($output);
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);
+ $self->_spin_markdown($data_ref, $pointer->parent(), $output);
} elsif ($data_ref->{format} eq 'pod') {
- $self->_spin_pod($data_ref, $output);
+ $self->_spin_pod($data_ref, $pointer->parent(), $output);
} else {
die "$pointer: unknown output format $data_ref->{format}\n";
}
@@ -261,7 +264,7 @@ App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file
=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.
+Path::Tiny, Pod::Thread, and YAML::XS, all of which are available from CPAN.
=head1 DESCRIPTION
diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm
index 2a7460a..c63222b 100644
--- a/lib/App/DocKnot/Spin/RSS.pm
+++ b/lib/App/DocKnot/Spin/RSS.pm
@@ -18,10 +18,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 Path::Tiny qw(path);
use Perl6::Slurp qw(slurp);
use POSIX qw(strftime);
@@ -123,23 +122,30 @@ sub _relative_url {
return ('../' x scalar(@base)) . $url;
}
-# Spin a file into HTML, changing directories to the directory of that file so
-# that relative file references resolve correctly.
+# Spin a file into HTML.
#
-# $file - Path to the file
+# $file - Path::Tiny path to the file
#
# Returns: Rendered HTML as a list with one element per line
sub _spin_file {
my ($self, $file) = @_;
- my $source = slurp($file);
- my $cwd = getcwd();
- my (undef, $dir) = fileparse($file);
- chdir($dir);
- my $page = $self->{spin}->spin_thread($source);
- chdir($cwd);
+ my $source = $file->slurp_utf8();
+ my $page = $self->{spin}->spin_thread($source, $file);
return map { "$_\n" } split(m{ \n }xms, $page);
}
+# Report an action to standard output.
+#
+# $action - String description of the action
+# $output - Output file generated
+# $base - Base path for all output
+sub _report_action {
+ my ($self, $action, $output) = @_;
+ my $shortout = $output->relative($self->{base} // path());
+ print_checked("$action .../$shortout\n");
+ return;
+}
+
##############################################################################
# Parsing
##############################################################################
@@ -158,7 +164,7 @@ sub _read_rfc2822_file {
# Parse the file. $key holds the last key seen, used to append
# continuation values to the previous key. $current holds the current
# block being parsed and @blocks all blocks seen so far.
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
while (defined(my $line = <$fh>)) {
if ($line =~ m{ \A \s* \z }xms) {
if ($key) {
@@ -208,7 +214,7 @@ sub _read_rfc2822_file {
# the changes into the provided array reference. Each element of the array
# will be a hash with keys title, date, link, and description.
#
-# $file - File to read
+# $file - Path::Tiny path to file to read
#
# Returns: List of reference to metadata hash and reference to a list of
# hashes of changes
@@ -273,7 +279,7 @@ sub _parse_changes {
# Format a journal post into HTML for inclusion in an RSS feed. This depends
# heavily on my personal layout for journal posts.
#
-# $file - Path to the journal post
+# $file - Path::Tiny path to the journal post
#
# Returns: HTML suitable for including in an RSS feed
sub _rss_journal {
@@ -303,7 +309,7 @@ sub _rss_journal {
# Format a review into HTML for inclusion in an RSS feed. This depends even
# more heavily on my personal layout for review posts.
#
-# $file - Path to the review
+# $file - Path::Tiny path to the review
#
# Returns: HTML suitable for inclusion in an RSS feed
sub _rss_review {
@@ -373,12 +379,13 @@ sub _rss_review {
# time as <lastBuildDate>; it's not completely clear to me that this is
# correct.
#
-# $fh - Output file handle
-# $file - Name of the output file
+# $file - Path::Tiny path to the output file
+# $base - Base Path::Tiny path for input files
# $metadata_ref - Hash of metadata for the RSS feed
# $entries_ref - Array of entries in the RSS feed
sub _rss_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $base, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
my $version = '1.25';
# Determine the current date and latest publication date of all of the
@@ -405,7 +412,7 @@ sub _rss_output {
<generator>DocKnot $App::DocKnot::VERSION</generator>
EOC
if ($metadata_ref->{'rss-base'}) {
- my ($name) = fileparse($file);
+ my $name = $file->basename();
my $url = $metadata_ref->{'rss-base'} . $name;
print_fh(
$fh,
@@ -427,9 +434,11 @@ EOC
$description =~ s{ \A (\s*) }{$1<p>}xms;
$description =~ s{ \n* \z }{</p>\n}xms;
} elsif ($entry_ref->{journal}) {
- $description = $self->_rss_journal($entry_ref->{journal});
+ my $path = path($entry_ref->{journal})->absolute($base);
+ $description = $self->_rss_journal($path);
} elsif ($entry_ref->{review}) {
- $description = $self->_rss_review($entry_ref->{review});
+ my $path = path($entry_ref->{review})->absolute($base);
+ $description = $self->_rss_review($path);
}
# Make all relative URLs absolute.
@@ -464,6 +473,7 @@ EOC
# Close the RSS structure.
print_fh($fh, $file, " </channel>\n</rss>\n");
+ close($fh);
return;
}
@@ -473,12 +483,12 @@ EOC
# Print out the thread version of the recent changes list.
#
-# $fh - File handle to which to output
-# $file - Name of the file for error reporting
+# $file - Path::Tiny output path
# $metadata_ref - RSS feed metadata
# $entries_ref - Entries
sub _thread_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
# Page prefix.
if ($metadata_ref->{'thread-prefix'}) {
@@ -520,6 +530,7 @@ sub _thread_output {
# Print out the end of the page.
print_fh($fh, $file, "\\signature\n");
+ close($fh);
return;
}
@@ -529,12 +540,12 @@ sub _thread_output {
# Translate the thread of a journal entry for inclusion in an index page.
#
-# $file - Path to the journal entry
+# $file - Path::Tiny to the journal entry
#
# Returns: Thread to include in the index page
sub _index_journal {
my ($self, $file, $url) = @_;
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
# Skip to the first \h1 and exclude it.
while (defined(my $line = <$fh>)) {
@@ -558,7 +569,7 @@ sub _index_journal {
# Translate the thread of a book review for inclusion into an index page.
#
-# $file - Path to the book review
+# $file - Path::Tiny to the book review
#
# Returns: Thread to include in the index page
sub _index_review {
@@ -571,7 +582,7 @@ sub _index_review {
# Scan for the author information and save it. Handle the case where the
# \header or \edited line is continued on the next line.
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
while (defined(my $line = <$fh>)) {
if ($line =~ m{ \\ (?:header|edited) \s* \[ $char+ \] \s* \z }xms) {
$line .= <$fh>;
@@ -619,12 +630,13 @@ sub _index_review {
# Print out the index version of the recent changes list.
#
-# $fh - File handle to which to output
-# $file - Name of the file for error reporting
+# $file - Path::Tiny path to the output file
+# $base - Base Path::Tiny path for input files
# $metadata_ref - RSS feed metadata
# $entries_ref - Entries
sub _index_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $base, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
# Output the prefix.
if ($metadata_ref->{'index-prefix'}) {
@@ -640,9 +652,11 @@ sub _index_output {
# Get the text of the entry.
my $text;
if ($entry_ref->{journal}) {
- $text = $self->_index_journal($entry_ref->{journal});
+ my $path = path($entry_ref->{journal})->absolute($base);
+ $text = $self->_index_journal($path);
} elsif ($entry_ref->{review}) {
- $text = $self->_index_review($entry_ref->{review});
+ my $path = path($entry_ref->{review})->absolute($base);
+ $text = $self->_index_review($path);
} else {
die "unknown entry type\n";
}
@@ -674,6 +688,7 @@ sub _index_output {
print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n");
}
print_fh($fh, $file, "\\signature\n");
+ close($fh);
return;
}
@@ -683,8 +698,8 @@ sub _index_output {
# Create a new RSS generator object.
#
-# $args - Anonymous hash of arguments with the following keys:
-# base - Base path for output files
+# $args_ref - Anonymous hash of arguments with the following keys:
+# base - Path::Tiny base path for output files
#
# Returns: Newly created object
sub new {
@@ -692,7 +707,7 @@ sub new {
# Create and return the object.
my $self = {
- base => $args_ref->{base},
+ base => defined($args_ref->{base}) ? path($args_ref->{base}) : undef,
spin => App::DocKnot::Spin::Thread->new(),
};
bless($self, $class);
@@ -701,14 +716,13 @@ sub new {
# Generate specified output files from an .rss input file.
#
-# $source - Path to the .rss file
-# $base - Optional base path for output
+# $source - Path::Tiny path to the .rss file
+# $base - Optional Path::Tiny base path for output
sub generate {
my ($self, $source, $base) = @_;
+ $source = path($source);
$base //= $self->{base};
- if ($base) {
- $base =~ s{ /* \z}{/}xms;
- }
+ $base = defined($base) ? path($base) : path();
# Read in the changes.
my ($metadata_ref, $changes_ref) = $self->_parse_changes($source);
@@ -722,14 +736,13 @@ sub generate {
# Iterate through each specified output file.
for my $output (@output) {
my ($tags, $format, $file) = split(m{ : }xms, $output);
- my $path = ($base && $file !~ m{ \A / }xms) ? "$base$file" : $file;
- my $prettyfile = $path;
- if ($prettyfile !~ m{ \A / }xms) {
- $prettyfile = ".../$prettyfile";
+ $file = path($file);
+ if ($file->is_relative()) {
+ $file = $file->absolute($base);
}
# If the output file is newer than the input file, do nothing.
- next if (-e $path && -M $path <= -M $source);
+ next if ($file->exists() && -M "$file" <= -M "$source");
# Find all the changes of interest to this output file.
my @entries;
@@ -743,26 +756,21 @@ sub generate {
# Write the output.
if ($format eq 'thread') {
- print_checked("Generating thread file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_thread_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating thread file', $file);
+ $self->_thread_output($file, $metadata_ref, \@entries);
} elsif ($format eq 'rss') {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- print_checked("Generating RSS file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_rss_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating RSS file', $file);
+ $self->_rss_output($file, $base, $metadata_ref, \@entries);
} elsif ($format eq 'index') {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- print_checked("Generating index file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_index_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating index file', $file);
+ my $index_base = $source->parent();
+ $self->_index_output($file, $index_base, $metadata_ref, \@entries);
}
}
return;
@@ -792,9 +800,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::Language, Date::Parse (both part of
-the TimeDate distribution), List::SomeUtils, and Perl6::Slurp, both of which
-are available from CPAN.
+Perl 5.24 or later and the modules Date::Language, Date::Parse (both part of
+the TimeDate distribution), List::SomeUtils, Path::Tiny, and Perl6::Slurp,
+both of which are available from CPAN.
=head1 DESCRIPTION
@@ -834,7 +842,7 @@ with one or more of the following keys, all of which are optional:
By default, App::DocKnot::Spin::RSS output files are relative to the current
working directory. If the C<base> argument is given, output files will be
relative to the value of C<base> instead. Output files specified as absolute
-paths will not be affected.
+paths will not be affected. C<base> may be a string or a Path::Tiny object.
=back
@@ -848,7 +856,8 @@ paths will not be affected.
Parse the input file FILE and generate the output files that it specifies.
BASE, if given, specifies the root directory for output files specified with
-relative paths, and overrides any C<base> argument given to new().
+relative paths, and overrides any C<base> argument given to new(). Both FILE
+and BASE may be strings or Path::Tiny objects.
=back
diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm
index f40f445..0d75796 100644
--- a/lib/App/DocKnot/Spin/Thread.pm
+++ b/lib/App/DocKnot/Spin/Thread.pm
@@ -1460,14 +1460,15 @@ sub new {
# Convert thread to HTML and return the output as a string. The working
# directory still matters for file references in the thread.
#
-# $thread - Thread to spin
+# $thread - Thread to spin
+# $input - Optional input file path (for relative path and timestamps)
#
# Returns: Resulting HTML
sub spin_thread {
- my ($self, $thread) = @_;
+ my ($self, $thread, $input) = @_;
my $result;
open(my $out_fh, '>', \$result);
- $self->_parse_document($thread, undef, $out_fh, undef);
+ $self->_parse_document($thread, $input, $out_fh, undef);
close($out_fh);
return $result;
}
@@ -1483,10 +1484,9 @@ sub spin_thread_file {
my $out_fh;
my $thread;
- # Read the input file. We do the work from the directory of the file to
- # ensure that relative file references resolve properly.
+ # Read the input file.
if (defined($input)) {
- $input = path($input)->absolute();
+ $input = path($input)->realpath();
$thread = $input->slurp_utf8();
} else {
$thread = slurp(\*STDIN);
@@ -1495,7 +1495,7 @@ sub spin_thread_file {
# Open the output file.
if (defined($output)) {
$output = path($output)->absolute();
- $out_fh = $output->filehandle('>');
+ $out_fh = $output->openw_utf8();
} else {
open($out_fh, '>&', 'STDOUT');
}
@@ -1513,7 +1513,7 @@ sub spin_thread_file {
# output from some other conversion process.
#
# $thread - Thread to spin
-# $input - Original input file path (for modification timestamps)
+# $input - Original input file path (for relative path and timestamps)
# $input_type - One-word description of input type for the page footer
# $output - Output file
#
@@ -1645,11 +1645,13 @@ data for the C<\release> and C<\version> commands.
=over 4
-=item spin_thread(THREAD)
+=item spin_thread(THREAD[, INPUT])
Convert the given thread to HTML, returning the result. When run via this
API, 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.
+INPUT, if given, is the full path to the original source file, used for
+relative paths and modification time information.
=item spin_thread_file([INPUT[, OUTPUT]])
@@ -1669,8 +1671,9 @@ 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.
+used for relative paths and 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
diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread
index 45c836a..2b4371a 100644
--- a/t/data/generate/docknot/output/thread
+++ b/t/data/generate/docknot/output/thread
@@ -117,7 +117,8 @@ The following additional Perl modules are required to use it:
\bullet(packed)[JSON::MaybeXS]
\bullet(packed)[Kwalify]
\bullet(packed)[List::SomeUtils 0.07 or later]
-\bullet(packed)[Path::Tiny]
+\bullet(packed)[Path::Iterator::Rule]
+\bullet(packed)[Path::Tiny 0.101 or later]
\bullet(packed)[Perl6::Slurp]
\bullet(packed)[Pod::Thread 3.01 or later]
\bullet(packed)[Template (part of Template Toolkit)]
diff --git a/t/data/spin/input/journal/.rss b/t/data/spin/input/journal/.rss
index 62f7c63..07c8e82 100644
--- a/t/data/spin/input/journal/.rss
+++ b/t/data/spin/input/journal/.rss
@@ -30,10 +30,10 @@ Index-Suffix:
Date: 2011-08-13 00:09
Title: NPR Top 100 SFF meme
Link: journal/2011-08/006.html
-Journal: journal/2011-08/006.th
+Journal: 2011-08/006.th
Tags: debian
Date: 2007-01-14 21:30
Title: Review: Fermat's Enigma
Link: reviews/books/1-250-30112-2.html
-Review: reviews/books/0-385-49362-2.th
+Review: ../reviews/books/0-385-49362-2.th
diff --git a/t/spin/tree.t b/t/spin/tree.t
index d2daab1..700f961 100755
--- a/t/spin/tree.t
+++ b/t/spin/tree.t
@@ -35,35 +35,35 @@ Generating RSS file .../changes.rss
Updating .../changes.rss
Spinning .../changes.html
Spinning .../index.html
-Updating .../names.png
-Spinning .../random.html
Creating .../journal
Generating index file .../journal/index.th
Generating RSS file .../journal/index.rss
Generating RSS file .../journal/debian.rss
Generating RSS file .../journal/reviews.rss
+Updating .../names.png
+Spinning .../random.html
+Creating .../reviews
+Creating .../software
+Creating .../usefor
+Creating .../journal/2011-08
Updating .../journal/debian.rss
Updating .../journal/index.rss
Spinning .../journal/index.html
Updating .../journal/reviews.rss
-Creating .../journal/2011-08
-Spinning .../journal/2011-08/006.html
-Creating .../reviews
Creating .../reviews/books
-Spinning .../reviews/books/0-385-49362-2.html
-Creating .../software
-Spinning .../software/index.html
Creating .../software/docknot
-Spinning .../software/docknot/index.html
-Creating .../software/docknot/api
-Converting .../software/docknot/api/app-docknot.html
-Creating .../usefor
-Spinning .../usefor/index.html
+Spinning .../software/index.html
Creating .../usefor/drafts
+Spinning .../usefor/index.html
+Spinning .../journal/2011-08/006.html
+Spinning .../reviews/books/0-385-49362-2.html
+Creating .../software/docknot/api
+Spinning .../software/docknot/index.html
Updating .../usefor/drafts/draft-ietf-usefor-message-id-01.txt
Updating .../usefor/drafts/draft-ietf-usefor-posted-mailed-01.txt
Updating .../usefor/drafts/draft-ietf-usefor-useage-01.txt
Updating .../usefor/drafts/draft-lindsey-usefor-signed-01.txt
+Converting .../software/docknot/api/app-docknot.html
OUTPUT
BEGIN { use_ok('App::DocKnot::Util', qw(print_fh)) }