diff options
author | Russ Allbery <rra@cpan.org> | 2021-12-31 17:23:11 -0800 |
---|---|---|
committer | Russ Allbery <rra@cpan.org> | 2021-12-31 17:23:11 -0800 |
commit | 84cd6a8fb1982d80132cfa675f397c0cb59148ab (patch) | |
tree | 8f94b307c3b6b0200b1d59bc6e5d792b217a67cb | |
parent | 5c26c088aa5963a1000a437c291f8542ed947eb4 (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.PL | 3 | ||||
-rw-r--r-- | Changes | 4 | ||||
-rw-r--r-- | README | 3 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | cpanfile | 3 | ||||
-rw-r--r-- | docs/docknot.yaml | 3 | ||||
-rw-r--r-- | lib/App/DocKnot/Command.pm | 5 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin.pm | 259 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Pointer.pm | 55 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/RSS.pm | 135 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Thread.pm | 25 | ||||
-rw-r--r-- | t/data/generate/docknot/output/thread | 3 | ||||
-rw-r--r-- | t/data/spin/input/journal/.rss | 4 | ||||
-rwxr-xr-x | t/spin/tree.t | 26 |
14 files changed, 284 insertions, 247 deletions
@@ -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, @@ -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. @@ -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) @@ -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) @@ -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)) } |