From 75c718ef14e3b688e834984b146fd4f21c6c97a9 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 11:11:49 -0800 Subject: Fix docknot spin requirement for package metadata Now that docknot spin loads App::DocKnot::Config in order to get the global config, too-aggressive error checking in the constructor caused it to fail unless run from a directory containing a package metadata file. Fix this by not checking that the metadata path is valid in the constructor. YAML::XS will produce a reasonable error when trying to open a nonexistent file. --- Changes | 5 +++++ lib/App/DocKnot/Config.pm | 11 +---------- t/cli/spin.t | 7 ++++++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Changes b/Changes index cddeb0f..92e4e3a 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,10 @@ Revision history for DocKnot +6.01 - Not Released + + - Fix spurious requirement for a package metadata file when running + docknot spin. + 6.00 - 2021-12-25 - Add a new *.spin input file for docknot spin that points to an external diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm index 2eebe32..5efbc48 100644 --- a/lib/App/DocKnot/Config.pm +++ b/lib/App/DocKnot/Config.pm @@ -31,18 +31,9 @@ use YAML::XS (); # metadata - Path to the docknot.yaml file # # Returns: Newly created object -# Throws: Text exceptions on invalid metadata directory path sub new { my ($class, $args_ref) = @_; - - # Ensure we were given a valid metadata argument. - my $metadata = $args_ref->{metadata} // 'docs/docknot.yaml'; - if (!-e $metadata) { - croak("metadata path $metadata does not exist"); - } - - # Create and return the object. - my $self = { metadata => $metadata }; + my $self = { metadata => $args_ref->{metadata} // 'docs/docknot.yaml' }; bless($self, $class); return $self; } diff --git a/t/cli/spin.t b/t/cli/spin.t index ac7894f..2522eda 100755 --- a/t/cli/spin.t +++ b/t/cli/spin.t @@ -76,7 +76,11 @@ print_fh($fh, $pointer_path, "format: pod\n"); print_fh($fh, $pointer_path, "path: $pod_source\n"); close($fh); -# Spin a tree of files. +# Spin a tree of files. Do this from the temporary directory because 6.00 had +# a regression where docknot spin would fail if there were no package metadata +# even though it didn't use it. +my $cwd = getcwd(); +chdir($tempdir->dirname); $expected = File::Spec->catfile($datadir, 'output'); capture_stdout { $docknot->run( @@ -84,6 +88,7 @@ capture_stdout { $tempdir->dirname, ); }; +chdir($cwd); my $count = is_spin_output_tree($tempdir->dirname, $expected, 'spin'); # Spin a file with warnings. The specific warnings are checked in -- cgit v1.2.3 From ce27d84e1099ab334d0a1ab3bbd4501807da69d2 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 11:44:18 -0800 Subject: Don't overwrite files if generation fails App::DocKnot::Generate was overwriting the output file before it attempted to generate the new version, resulting in zeroing the file if generation failed with an error. Wait to open the output file for writing until after the new output has been generated without errors. --- Changes | 3 +++ lib/App/DocKnot/Generate.pm | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index 92e4e3a..49c9a19 100644 --- a/Changes +++ b/Changes @@ -5,6 +5,9 @@ - Fix spurious requirement for a package metadata file when running docknot spin. + - Don't overwrite output files from docknot generate or generate-all + if the generation fails. + 6.00 - 2021-12-25 - Add a new *.spin input file for docknot spin that points to an external diff --git a/lib/App/DocKnot/Generate.pm b/lib/App/DocKnot/Generate.pm index 8caa7a8..f94b718 100644 --- a/lib/App/DocKnot/Generate.pm +++ b/lib/App/DocKnot/Generate.pm @@ -18,6 +18,7 @@ use parent qw(App::DocKnot); use warnings; use App::DocKnot::Config; +use App::DocKnot::Util qw(print_fh); use Carp qw(croak); use Encode qw(encode); use Template; @@ -525,9 +526,9 @@ sub generate_output { } # Generate the output. + my $data = $self->generate($template); open(my $outfh, '>', $output); - print {$outfh} encode('utf-8', $self->generate($template)) - or croak("cannot write to $output: $!"); + print_fh($outfh, $output, encode('utf-8', $data)); close($outfh); return; } -- cgit v1.2.3 From aa0789403a9135227ff7b8b0d2b3e3873dd87a3a Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 11:45:20 -0800 Subject: Rewrite YAML::XS::LoadFile errors We're now relying on YAML::XS::LoadFile to report errors for nonexistent files, but its default error reporting is odd and doesn't match our conventions. Catch those errors and rewrite them appropriately. --- lib/App/DocKnot.pm | 17 ++++++++++++++--- t/cli/errors.t | 10 +++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/App/DocKnot.pm b/lib/App/DocKnot.pm index 721bb30..34f71c8 100644 --- a/lib/App/DocKnot.pm +++ b/lib/App/DocKnot.pm @@ -70,10 +70,21 @@ sub load_yaml_file { # data elements set to false. local $YAML::XS::Boolean = 'JSON::PP'; - # Load the metadata and check it against the schema. - my $data_ref = YAML::XS::LoadFile($path); + # Load the metadata and check it against the schema. YAML::XS for some + # reason puts a newline before the system error part of an error message + # when loading a file, so clean up the error a bit. my $schema_path = $self->appdata_path('schema', $schema . '.yaml'); - my $schema_ref = YAML::XS::LoadFile($schema_path); + my ($data_ref, $schema_ref); + eval { + $data_ref = YAML::XS::LoadFile($path); + $schema_ref = YAML::XS::LoadFile($schema_path); + }; + if ($@) { + my $error = lcfirst($@); + chomp($error); + $error =~ s{ \n }{ }xms; + die "$error\n"; + } eval { validate($schema_ref, $data_ref) }; if ($@) { my $errors = $@; diff --git a/t/cli/errors.t b/t/cli/errors.t index 2336977..64df6c3 100755 --- a/t/cli/errors.t +++ b/t/cli/errors.t @@ -10,9 +10,11 @@ use 5.024; use autodie; use warnings; +use POSIX qw(LC_ALL setlocale); use Test::More tests => 11; # Isolate from the environment. +setlocale(LC_ALL, 'C'); local $ENV{XDG_CONFIG_HOME} = '/nonexistent'; local $ENV{XDG_CONFIG_DIRS} = '/nonexistent'; @@ -61,7 +63,13 @@ is_error($@, 'generate-all: too many arguments', 'Too many arguments'); # Trigger an error in a submodule to test error rewriting. eval { $docknot->run('generate', '-m', '/nonexistent', 'readme') }; -is_error($@, 'generate: metadata path /nonexistent does not exist'); +is_error( + $@, + ( + 'generate: can\'t open \'/nonexistent\' for input:' + . ' No such file or directory' + ), +); # Check for a missing required argument. eval { $docknot->run('dist') }; -- cgit v1.2.3 From 3a81ce26dbeae0ccf77a521866fbd5c3386ef3e9 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 20:53:32 -0800 Subject: Require Pod::Thread 3.01 Require the latest version and use that to simplify how the title is passed from the pointer to Pod::Thread. --- Build.PL | 2 +- README | 2 +- README.md | 2 +- docs/docknot.yaml | 2 +- lib/App/DocKnot/Spin/Pointer.pm | 4 +--- t/data/generate/docknot/output/thread | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Build.PL b/Build.PL index 1304468..19dd784 100644 --- a/Build.PL +++ b/Build.PL @@ -76,7 +76,7 @@ my $build = Module::Build->new( 'List::SomeUtils' => '0.07', 'Path::Tiny' => 0, 'Perl6::Slurp' => 0, - 'Pod::Thread' => '3.00', + 'Pod::Thread' => '3.01', 'Template' => 0, 'YAML::XS' => '0.81', perl => '5.024', diff --git a/README b/README index cf25bbe..35d5a12 100644 --- a/README +++ b/README @@ -66,7 +66,7 @@ REQUIREMENTS * List::SomeUtils 0.07 or later * Path::Tiny * Perl6::Slurp - * Pod::Thread 3.00 or later + * Pod::Thread 3.01 or later * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/README.md b/README.md index f430882..10454d7 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The following additional Perl modules are required to use it: * List::SomeUtils 0.07 or later * Path::Tiny * Perl6::Slurp -* Pod::Thread 3.00 or later +* Pod::Thread 3.01 or later * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/docs/docknot.yaml b/docs/docknot.yaml index becc165..a64fdb3 100644 --- a/docs/docknot.yaml +++ b/docs/docknot.yaml @@ -143,7 +143,7 @@ requirements: | * List::SomeUtils 0.07 or later * Path::Tiny * Perl6::Slurp - * Pod::Thread 3.00 or later + * Pod::Thread 3.01 or later * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm index 05aa9ac..aaab6af 100644 --- a/lib/App/DocKnot/Spin/Pointer.pm +++ b/lib/App/DocKnot/Spin/Pointer.pm @@ -122,6 +122,7 @@ sub _spin_pod { my %options = ( contents => $data_ref->{options}{contents}, style => $data_ref->{style} // 'pod', + title => $data_ref->{title}, ); #<<< if (exists($data_ref->{options}{navbar})) { @@ -129,9 +130,6 @@ sub _spin_pod { } 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. diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread index 63e79c3..45c836a 100644 --- a/t/data/generate/docknot/output/thread +++ b/t/data/generate/docknot/output/thread @@ -119,7 +119,7 @@ The following additional Perl modules are required to use it: \bullet(packed)[List::SomeUtils 0.07 or later] \bullet(packed)[Path::Tiny] \bullet(packed)[Perl6::Slurp] -\bullet(packed)[Pod::Thread 3.00 or later] +\bullet(packed)[Pod::Thread 3.01 or later] \bullet(packed)[Template (part of Template Toolkit)] \bullet(packed)[YAML::XS 0.81 or later] -- cgit v1.2.3 From 3a32578ab06e1ab63f7ebcf93818c3758f0b3527 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 21:11:50 -0800 Subject: Use POSIX TZ format for tests One of the tests depends on time zone conversions, and it failed in tests from one of the CPAN testers. In the hope that this is due to a missing tzinfo database, set TZ to a full POSIX time zone string instead. --- t/spin/versions.t | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/t/spin/versions.t b/t/spin/versions.t index 640e143..7f1e62b 100755 --- a/t/spin/versions.t +++ b/t/spin/versions.t @@ -19,8 +19,10 @@ use Test::More tests => 20; require_ok('App::DocKnot::Spin::Versions'); -# All dates in the sample data are in America/Los_Angeles. -local $ENV{TZ} = 'America/Los_Angeles'; +# All dates in the sample data are in America/Los_Angeles. Specify this in +# the POSIX format in the hope this will also work on systems without tzinfo +# installed. +local $ENV{TZ} = 'PST8PDT,M3.2.0,M11.1.0'; tzset(); # Parse the file. -- cgit v1.2.3 From c4444d640d251e8617c91836a6e8f69fd4560337 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 26 Dec 2021 21:58:40 -0800 Subject: Hopefully better probe for git archive The t/dist/basic.t test fails on Solaris because git archive produces a tar file that's incompatible with pax. Try harder to probe for that error message, which for some reason isn't being triggered by the test to see if we should skip this test. --- t/dist/basic.t | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/t/dist/basic.t b/t/dist/basic.t index 6fcf2bc..dcc6b1b 100755 --- a/t/dist/basic.t +++ b/t/dist/basic.t @@ -20,6 +20,7 @@ use File::Temp; use Git::Repository; use IPC::Run qw(run); use IPC::System::Simple qw(capturex systemx); +use List::Util qw(first); use Test::More; @@ -52,9 +53,13 @@ $repo->run(add => '-A', q{.}); $repo->run(commit => '-q', '-m', 'Initial commit'); # Check whether we have all the necessary tools to run the test. +my @branches = $repo->run( + 'for-each-ref' => '--format=%(refname:short)', 'refs/heads/', +); +my $head = first { $_ eq 'main' || $_ eq 'master' } @branches; my $result; eval { - my $archive = $repo->command(archive => 'HEAD'); + my $archive = $repo->command(archive => '--prefix=foo/', $head); my $out; $result = run([qw(tar tf -)], '<', $archive->stdout, '>', \$out); $archive->close(); -- cgit v1.2.3 From 5c26c088aa5963a1000a437c291f8542ed947eb4 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Mon, 27 Dec 2021 13:25:22 -0800 Subject: Convert App::DocKnot::Spin::Thread to Path::Tiny Use Path::Tiny for all path manipulation, and use it to avoid relying on the current working directory for everything but spinning files from standard input. This will unlock avoiding worrying about working directories throughout docknot spin. --- lib/App/DocKnot/Spin/Sitemap.pm | 4 +- lib/App/DocKnot/Spin/Thread.pm | 148 +++++++++++++++++++++++----------------- lib/App/DocKnot/Util.pm | 4 +- t/spin/errors.t | 2 +- 4 files changed, 90 insertions(+), 68 deletions(-) diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm index 22fbdd5..e455723 100644 --- a/lib/App/DocKnot/Spin/Sitemap.pm +++ b/lib/App/DocKnot/Spin/Sitemap.pm @@ -226,7 +226,7 @@ sub new { # Return the tags for a given output file, suitable for its # section. # -# $path - Path to the output, relative to the top of the web site +# $path - URL path to the output with leading slash # # Returns: List of lines to add to the section sub links { @@ -268,7 +268,7 @@ sub links { # Return the navigation bar for a given output file. # -# $path - Path to the output, relative to the top of the web site +# $path - URL path to the output with leading slash # # Returns: List of lines that create the navbar sub navbar { diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm index bf58dc0..f40f445 100644 --- a/lib/App/DocKnot/Spin/Thread.pm +++ b/lib/App/DocKnot/Spin/Thread.pm @@ -17,9 +17,6 @@ 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 Git::Repository (); use Image::Size qw(html_imgsize); use Path::Tiny qw(path); @@ -89,19 +86,37 @@ my %COMMANDS = ( # Input and output ############################################################################## +# Determine the path to a file relative to the current file being processed. +# If the current file being processed is standard input, the path is relative +# to the current working directory. +# +# $path - File path as a string +# +# Returns: Path::Tiny object holding the absolute path +sub _file_path { + my ($self, $file) = @_; + my $input_path = $self->{input}[-1][1]; + if (defined($input_path)) { + my $path = $input_path->sibling($file); + return $path->exists() ? $path->realpath() : $path; + } else { + return path($file); + } +} + # Read a file and check it for bad line endings. # -# $path - File path +# $path - Path::Tiny object # # Returns: Contents of the file sub _read_file { - my ($self, $fh, $path) = @_; - my $text = slurp($fh); + my ($self, $path) = @_; + my $text = $path->slurp_utf8(); # Check for broken line endings. if ("\n" !~ m{ \015 }xms && $text =~ m{ \015 }xms) { my $m = 'found CR characters; are your line endings correct?'; - $self->_warning($m); + $self->_warning($m, $path); } # Return the contents. @@ -159,15 +174,24 @@ sub _output { sub _fatal { my ($self, $problem) = @_; my (undef, $file, $lineno) = $self->{input}[-1]->@*; + $file //= q{-}; die "$file:$lineno: $problem\n"; } # Warn about a problem with the current file and line. # # $problem - Warning message to report +# $file - Optional path where the problem was seen, otherwise the current +# input file is used sub _warning { - my ($self, $problem) = @_; - my (undef, $file, $lineno) = $self->{input}[-1]->@*; + my ($self, $problem, $file) = @_; + my $lineno; + if (!defined($file)) { + (undef, $file, $lineno) = $self->{input}[-1]->@*; + $file //= q{-}; + } else { + $lineno = 0; + } warn "$file:$lineno: $problem\n"; return; } @@ -637,9 +661,9 @@ sub _parse { # needs to access. # # $thread - Thread to spin -# $in_path - Input file path if any, used for error reporting +# $in_path - Input file path as a Path::Tiny object, or undef # $out_fh - Output file handle to which to write the HTML -# $out_path - Optional output file path for error reporting and page links +# $out_path - Output file path as a Path::Tiny object, or undef # $input_type - Optional one-word description of input type sub _parse_document { my ($self, $thread, $in_path, $out_fh, $out_path, $input_type) = @_; @@ -653,7 +677,7 @@ sub _parse_document { $self->{input_type} = $input_type // 'thread'; $self->{macro} = {}; $self->{out_fh} = $out_fh; - $self->{out_path} = $out_path // q{-}; + $self->{out_path} = $out_path; $self->{rss} = []; $self->{space} = q{}; $self->{state} = ['BLOCK']; @@ -1019,9 +1043,9 @@ sub _cmd_heading { $style = $self->_parse($style); # Get the relative URL of the output page, used for sitemap information. - my $page = $self->{out_path}; - if ($self->{output}) { - $page =~ s{ \A \Q$self->{output}\E }{}xms; + my $page; + if (defined($self->{out_path}) && defined($self->{output})) { + $page = $self->{out_path}->relative($self->{output}); } # Build the page header. @@ -1054,8 +1078,8 @@ sub _cmd_heading { } # Add tags based on the sitemap. - if ($self->{sitemap}) { - my @links = $self->{sitemap}->links($page); + if ($self->{sitemap} && defined($page)) { + my @links = $self->{sitemap}->links("/$page"); if (@links) { $output .= join(q{}, @links); } @@ -1066,17 +1090,15 @@ sub _cmd_heading { # Add some generator comments. my $date = strftime('%Y-%m-%d %T -0000', gmtime()); - my $from - = $self->{input}[-1][1] eq q{-} - ? q{} - : ' from ' . fileparse($self->{input}[-1][1]); + my $input_path = $self->{input}[-1][1]; + my $from = defined($input_path) ? ' from ' . $input_path->basename() : q{}; my $version = $App::DocKnot::VERSION; $output .= "\n"; # Add the tag and the navbar (if we have a sitemap). $output .= "\n\n"; - if ($self->{sitemap}) { - my @navbar = $self->{sitemap}->navbar($page); + if ($self->{sitemap} && defined($page)) { + my @navbar = $self->{sitemap}->navbar("/$page"); if (@navbar) { $output .= join(q{}, @navbar); } @@ -1096,7 +1118,8 @@ sub _cmd_image { $text = $self->_parse($text); # Determine the size attributes of the image if possible. - my $size = -e $image ? q{ } . lc(html_imgsize($image)) : q{}; + my $path = $self->_file_path($image); + my $size = $path->exists() ? q{ } . lc(html_imgsize("$path")) : q{}; # Generate the tag. my $output = qq{$text_parse($file)); + $file = $self->_file_path($self->_parse($file)); # Read the thread, split it on paragraphs, and reverse it to make a stack. my $thread = $self->_read_file($file); @@ -1242,33 +1265,34 @@ sub _cmd_rss { # address block. sub _cmd_signature { my ($self) = @_; - my $source = $self->{input}[-1][1]; + my $input_path = $self->{input}[-1][1]; my $output = $self->_border_end(); # 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{-}) { + if (!defined($input_path) && !defined($self->{out_path})) { $output .= "\n\n"; return (1, $output); } # Add the end-of-page navbar if we have sitemap information. 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)) . "\n"; + my $page = $self->{out_path}->relative($self->{output}); + $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 = $now; - if ($source ne q{-}) { - $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9])); + if (defined($input_path)) { + $modified = strftime('%Y-%m-%d', gmtime($input_path->stat()->[9])); } if ($self->{repository} && $self->{source}) { - if (path($self->{source})->subsumes(path($source))) { + if (path($self->{source})->subsumes($input_path)) { my $repository = $self->{repository}; - $modified = $repository->run('log', '-1', '--format=%ct', $source); + $modified = $self->{repository}->run( + 'log', '-1', '--format=%ct', "$input_path", + ); if ($modified) { $modified = strftime('%Y-%m-%d', gmtime($modified)); } @@ -1295,10 +1319,13 @@ sub _cmd_signature { # enough and doesn't seem worth the trouble of another dependency. sub _cmd_size { my ($self, $file) = @_; - $file = $self->_parse($file); + $file = $self->_file_path($self->_parse($file)); # Get the size of the file. - my ($size) = (stat($file))[7]; + my $size; + if ($file->exists()) { + $size = $file->stat()->[7]; + } if (!defined($size)) { $self->_warning("cannot stat file $file: $!"); return (0, q{}); @@ -1394,6 +1421,10 @@ sub _cmd_version { # Returns: Newly created object sub new { my ($class, $args_ref) = @_; + my $output; + if (defined($args_ref->{output})) { + $output = path($args_ref->{output}); + } # Add a trailing slash to the partial URL for style sheets. my $style_url = $args_ref->{'style-url'} // q{}; @@ -1403,19 +1434,21 @@ sub new { # Use a Git::Repository object to get modification timestamps if a source # tree was specified and it appears to be a git repository. - my $source = $args_ref->{source}; - my $repository; - if (defined($source) && -d File::Spec->catdir($source, '.git')) { - $repository = Git::Repository->new(work_tree => $source); + my ($source, $repository); + if (defined($args_ref->{source})) { + $source = path($args_ref->{source}); + if ($source->child('.git')->is_dir()) { + $repository = Git::Repository->new(work_tree => "$source"); + } } # Create and return the object. #<<< my $self = { - output => $args_ref->{output}, + output => $output, repository => $repository, sitemap => $args_ref->{sitemap}, - source => $args_ref->{source}, + source => $source, style_url => $style_url, versions => $args_ref->{versions}, }; @@ -1434,7 +1467,7 @@ sub spin_thread { my ($self, $thread) = @_; my $result; open(my $out_fh, '>', \$result); - $self->_parse_document($thread, q{-}, $out_fh, q{-}); + $self->_parse_document($thread, undef, $out_fh, undef); close($out_fh); return $result; } @@ -1447,40 +1480,31 @@ sub spin_thread { # Raises: Text exception on processing error sub spin_thread_file { my ($self, $input, $output) = @_; - my $cwd = getcwd() or die "cannot get current directory: $!\n"; 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. if (defined($input)) { - my $path = realpath($input) or die "cannot canonicalize $input: $!\n"; - $input = $path; - $thread = slurp($input); - my (undef, $input_dir) = fileparse($input); - chdir($input_dir); + $input = path($input)->absolute(); + $thread = $input->slurp_utf8(); } else { - $input = q{-}; $thread = slurp(\*STDIN); } # Open the output file. if (defined($output)) { - my $path = realpath($output) - or die "cannot canonicalize $output: $!\n"; - $output = $path; - open($out_fh, '>', $output); + $output = path($output)->absolute(); + $out_fh = $output->filehandle('>'); } else { - $output = q{-}; open($out_fh, '>&', 'STDOUT'); } # Do the work. $self->_parse_document($thread, $input, $out_fh, $output); - # Clean up and restore the working directory. + # Clean up. close($out_fh); - chdir($cwd); return; } @@ -1489,23 +1513,21 @@ sub spin_thread_file { # output from some other conversion process. # # $thread - Thread to spin -# $input - Original input file (for modification timestamps) +# $input - Original input file path (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) = @_; + $input = path($input); # 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); + $output = path($output)->absolute(); + $out_fh = $output->filehandle('>'); } else { - $output = q{-}; open($out_fh, '>&', 'STDOUT'); } diff --git a/lib/App/DocKnot/Util.pm b/lib/App/DocKnot/Util.pm index 203d3d3..5999b2b 100644 --- a/lib/App/DocKnot/Util.pm +++ b/lib/App/DocKnot/Util.pm @@ -123,8 +123,8 @@ doesn't because print cannot be prototyped). =item print_fh(FH, NAME, DATA[, DATA ...]) Writes the concatenation of the DATA elements (interpreted as scalar strings) -to the file handle FH. NAME should be the name of the file open as FH, and is -used for error reporting. +to the file handle FH. NAME should be the name of (or Path::Tiny object for) +the file open as FH, and is used for error reporting. This is mostly equivalent to C but throws a text exception in the event of a failure. diff --git a/t/spin/errors.t b/t/spin/errors.t index d4cc565..c6e1ce6 100755 --- a/t/spin/errors.t +++ b/t/spin/errors.t @@ -43,5 +43,5 @@ my ($stdout, $stderr) = capture { # Simplify the file name, and then check against the expected output. $stderr =~ s{ ^ [^:]+/errors[.]th: }{errors.th:}xmsg; -$stderr =~ s{ (cannot [ ] stat [^:]+): .* }{$1\n}xms; +$stderr =~ s{ (cannot [ ] stat [ ] file [ ]) /[^:]+/([^/:]+) : .* }{$1$2\n}xms; is($stderr, $EXPECTED_ERRORS, 'errors are correct'); -- cgit v1.2.3 From 84cd6a8fb1982d80132cfa675f397c0cb59148ab Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 31 Dec 2021 17:23:11 -0800 Subject: 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. --- Build.PL | 3 +- Changes | 4 + README | 3 +- README.md | 3 +- cpanfile | 3 +- docs/docknot.yaml | 3 +- lib/App/DocKnot/Command.pm | 5 +- lib/App/DocKnot/Spin.pm | 259 ++++++++++++++++++---------------- lib/App/DocKnot/Spin/Pointer.pm | 55 ++++---- lib/App/DocKnot/Spin/RSS.pm | 135 +++++++++--------- lib/App/DocKnot/Spin/Thread.pm | 25 ++-- t/data/generate/docknot/output/thread | 3 +- t/data/spin/input/journal/.rss | 4 +- t/spin/tree.t | 26 ++-- 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 first. Also look for a 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 = 'spun'; @@ -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, B, and B 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, B, and B 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 ; 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 { DocKnot $App::DocKnot::VERSION 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

}xms; $description =~ s{ \n* \z }{

\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, " \n\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 argument is given, output files will be relative to the value of C instead. Output files specified as absolute -paths will not be affected. +paths will not be affected. C 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 argument given to new(). +relative paths, and overrides any C 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)) } -- cgit v1.2.3 From c00d3652d51a440d6c5378631af4021c39afa644 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Fri, 31 Dec 2021 17:25:32 -0800 Subject: Require Pod::Thread 3.01 or later --- Changes | 2 ++ cpanfile | 2 +- lib/App/DocKnot/Spin.pm | 2 +- lib/App/DocKnot/Spin/Pointer.pm | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index 1a2741d..ceb8d09 100644 --- a/Changes +++ b/Changes @@ -12,6 +12,8 @@ - Don't overwrite output files from docknot generate or generate-all if the generation fails. + - Require Pod::Thread 3.01 or later. + 6.00 - 2021-12-25 - Add a new *.spin input file for docknot spin that points to an external diff --git a/cpanfile b/cpanfile index de7a654..b9c7d8e 100644 --- a/cpanfile +++ b/cpanfile @@ -15,7 +15,7 @@ requires 'List::SomeUtils', '0.07'; requires 'Path::Iterator::Rule'; requires 'Path::Tiny', '0.101'; requires 'Perl6::Slurp'; -requires 'Pod::Thread', '3.00'; +requires 'Pod::Thread', '3.01'; requires 'Template'; requires 'YAML::XS', '0.81'; diff --git a/lib/App/DocKnot/Spin.pm b/lib/App/DocKnot/Spin.pm index d95a1dd..eade414 100644 --- a/lib/App/DocKnot/Spin.pm +++ b/lib/App/DocKnot/Spin.pm @@ -27,7 +27,7 @@ use Git::Repository (); use IPC::System::Simple qw(capture); use Path::Iterator::Rule (); use Path::Tiny qw(path); -use Pod::Thread 3.00 (); +use Pod::Thread 3.01 (); use POSIX qw(strftime); # The default list of files and/or directories to exclude from spinning. This diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm index b08c193..d378bc9 100644 --- a/lib/App/DocKnot/Spin/Pointer.pm +++ b/lib/App/DocKnot/Spin/Pointer.pm @@ -24,6 +24,7 @@ use Encode qw(decode); use File::BaseDir qw(config_files); use IPC::System::Simple qw(capturex); use Path::Tiny qw(path); +use Pod::Thread 3.01 (); use POSIX qw(strftime); use Template (); use YAML::XS (); -- cgit v1.2.3 From 7c2fb0c0588ca7cb7e54695935afd965c22cab27 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 2 Jan 2022 13:23:07 -0800 Subject: Add new docknot update-spin command Add new docknot update-spin command and corresponding update_spin method in App::DocKnot::Update to update a spin input tree to the latest expectations. Currently, all this does is convert *.rpod pointer files to *.spin pointer files. --- Changes | 5 ++ bin/docknot | 21 ++++- lib/App/DocKnot/Command.pm | 7 +- lib/App/DocKnot/Update.pm | 147 ++++++++++++++++++++++++++++++---- t/data/spin/update/input/module.rpod | 1 + t/data/spin/update/input/readme.rpod | 2 + t/data/spin/update/input/script.rpod | 3 + t/data/spin/update/output/module.spin | 2 + t/data/spin/update/output/readme.spin | 6 ++ t/data/spin/update/output/script.spin | 3 + t/lib/Test/DocKnot/Spin.pm | 6 +- t/update/spin.t | 65 +++++++++++++++ 12 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 t/data/spin/update/input/module.rpod create mode 100644 t/data/spin/update/input/readme.rpod create mode 100644 t/data/spin/update/input/script.rpod create mode 100644 t/data/spin/update/output/module.spin create mode 100644 t/data/spin/update/output/readme.spin create mode 100644 t/data/spin/update/output/script.spin create mode 100755 t/update/spin.t diff --git a/Changes b/Changes index ceb8d09..adf5cfc 100644 --- a/Changes +++ b/Changes @@ -2,6 +2,11 @@ 6.01 - Not Released + - Add new docknot update-spin command and corresponding update_spin + method in App::DocKnot::Update to update a spin input tree to the + latest expectations. Currently, all this does is convert *.rpod + pointer files to *.spin pointer files. + - 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. diff --git a/bin/docknot b/bin/docknot index c9e3f12..3de49a9 100755 --- a/bin/docknot +++ b/bin/docknot @@ -47,6 +47,8 @@ B spin-thread [B<-f>] [B<-s> I] [I [I]] B update [B<-m> I] [B<-o> I] +B update-spin [I] + =head1 DESCRIPTION B is a static web site generator with special support for managing @@ -97,6 +99,12 @@ Like C, but convert a single file written in thread to HTML. Update the DocKnot package configuration from an older format. +=item update-spin + +Update an input tree for C to the latest expectations. This will, for +example, convert old-style F<*.rpod> pointer files to new-style F<*.spin> +pointer files. + =back =head1 OPTIONS @@ -285,6 +293,17 @@ recommended metadata path for a project). =back +=head2 update-spin + +=over 4 + +=item I + +The path to the spin input tree to update. If not given, defaults to the +current directory. + +=back + =head1 DIAGNOSTICS If B fails with errors, see the underlying module for that subcommand @@ -320,7 +339,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2016, 2018-2021 Russ Allbery +Copyright 2016, 2018-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm index fdbc184..48c7240 100644 --- a/lib/App/DocKnot/Command.pm +++ b/lib/App/DocKnot/Command.pm @@ -104,6 +104,11 @@ our %COMMANDS = ( options => ['metadata|m=s', 'output|o=s'], maximum => 0, }, + 'update-spin' => { + method => 'update_spin', + module => 'App::DocKnot::Update', + maximum => 1, + }, ); ############################################################################## @@ -338,7 +343,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2018-2021 Russ Allbery +Copyright 2018-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot/Update.pm b/lib/App/DocKnot/Update.pm index 5c6a999..e14a726 100644 --- a/lib/App/DocKnot/Update.pm +++ b/lib/App/DocKnot/Update.pm @@ -20,6 +20,8 @@ use Carp qw(croak); use File::Spec; use JSON::MaybeXS qw(JSON); use Kwalify qw(validate); +use Path::Iterator::Rule; +use Path::Tiny qw(path); use Perl6::Slurp; use YAML::XS (); @@ -157,6 +159,90 @@ sub _config_from_json { return $data_ref; } +############################################################################## +# Spin helper methods +############################################################################## + +# Given an old-format *.rpod pointer file, read the master file name and any +# options. Return them in the structure used for *.spin pointer files. +# +# $path - Path::Tiny for the file to read +# +# Returns: Hash in the format of a *.spin pointer file +# Throws: Text exception if no master file is present in the pointer +# autodie exception if the pointer file could not be read +sub _read_rpod_pointer { + my ($self, $path) = @_; + + # Read the pointer file. + my ($master, $options, $style) = $path->lines_utf8(); + if (!$master) { + die "no master file specified in $path\n"; + } + chomp($master); + + # Put the results into the correct format. + my %results = (format => 'pod', path => $master); + if (defined($style)) { + chomp($style); + $results{style} = $style; + } + if (defined($options)) { + if ($options =~ m{ -c ( \s | \z ) }xms) { + $results{options} = { + contents => JSON::MaybeXS::true, + navbar => JSON::MaybeXS::false, + }; + } + if ($options =~ m{ -t \s+ (?: '(.*)' | ( [^\'] \S+ ) ) }xms) { + $results{title} = $1 || $2; + } + } + + # Return the parsed file. + return \%results; +} + +# Given its representation as a hash, write out a new-style *.spin file. +# +# $data_ref - Hash of data for the file +# $path - Path to output file +sub _write_spin_pointer { + my ($self, $data_ref, $path) = @_; + + # Generate the YAML output and strip off the leading document separator. + local $YAML::XS::Boolean = 'JSON::PP'; + my $yaml = YAML::XS::Dump($data_ref); + $yaml =~ s{ \A --- \n }{}xms; + + # Write the output. + $path->spew_utf8($yaml); + return; +} + +# Convert an *.rpod file to a *.spin file. Intended to be run via +# Path::Iterator::Rule. +# +# $rpod_path - Path to *.rpod file +# $repo - Optional Git::Repository object for input tree +sub _convert_rpod_pointer { + my ($self, $rpod_path, $repo) = @_; + + # Convert the file. + my $data_ref = $self->_read_rpod_pointer($rpod_path); + my $basename = $rpod_path->basename('.rpod'); + my $spin_path = $rpod_path->sibling($basename . '.spin'); + $self->_write_spin_pointer($data_ref, $spin_path); + + # If we have a Git repository, update Git. + if (defined($repo)) { + my $root = path($repo->work_tree()); + $repo->run('add', $spin_path->relative($root)->stringify()); + $repo->run('rm', $rpod_path->relative($root)->stringify()); + } + return; +} + ############################################################################## # Public Interface ############################################################################## @@ -172,16 +258,8 @@ sub _config_from_json { # Throws: Text exceptions on invalid metadata directory path sub new { my ($class, $args_ref) = @_; - - # Ensure we were given a valid metadata argument. - my $metadata = $args_ref->{metadata} // 'docs/metadata'; - if (!-d $metadata) { - croak("metadata path $metadata does not exist or is not a directory"); - } - - # Create and return the object. my $self = { - metadata => $metadata, + metadata => $args_ref->{metadata} // 'docs/metadata', output => $args_ref->{output} // 'docs/docknot.yaml', }; bless($self, $class); @@ -197,6 +275,12 @@ sub new { sub update { my ($self) = @_; + # Ensure we were given a valid metadata argument. + if (!-d $self->{metadata}) { + my $metadata = $self->{metadata}; + croak("metadata path $metadata does not exist or is not a directory"); + } + # Tell YAML::XS that we'll be feeding it JSON::PP booleans. local $YAML::XS::Boolean = 'JSON::PP'; @@ -278,6 +362,28 @@ sub update { return; } +# Update an input tree for spin to the current format. +# +# $path - Optional path to the spin input tree, defaults to current directory +# +# Raises: Text exception on failure +sub update_spin { + my ($self, $path) = @_; + $path = defined($path) ? path($path) : path(q{.}); + my $repo; + if ($path->child('.git')->is_dir()) { + $repo = Git::Repository->new(work_tree => "$path"); + } + + # Convert all *.rpod files to *.spin files. + my $rule = Path::Iterator::Rule->new()->name(qr{ [.] rpod \z }xms); + my $iter = $rule->iter($path, { follow_symlinks => 0 }); + while (defined(my $file = $iter->())) { + $self->_convert_rpod_pointer(path($file), $repo); + } + return; +} + ############################################################################## # Module return value and documentation ############################################################################## @@ -290,23 +396,27 @@ Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG =head1 NAME -App::DocKnot::Update - Update DocKnot package configuration for new formats +App::DocKnot::Update - Update DocKnot input or package configuration =head1 SYNOPSIS use App::DocKnot::Update; - my $reader = App::DocKnot::Update->new( + + my $update = App::DocKnot::Update->new( { metadata => 'docs/metadata', output => 'docs/docknot.yaml', } ); - my $config = $reader->update(); + $update->update(); + + $update->update_spin('/path/to/spin/input'); =head1 REQUIREMENTS -Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON, -Perl6::Slurp, and YAML::XS, all of which are available from CPAN. +Perl 5.24 or later and the modules Git::Repository, File::BaseDir, +File::ShareDir, JSON::MaybeXS, Path::Iterator::Rule, Path::Tiny, Perl6::Slurp, +and YAML::XS, all of which are available from CPAN. =head1 DESCRIPTION @@ -348,6 +458,13 @@ F relative to the current directory. Load the legacy JSON metadata and write out the YAML equivalent. +=item update_spin([PATH]) + +Update the input tree for App::DocKnot::Spin to follow current expectations. +PATH is the path to the input tree, which defaults to the current directory +if not given. If the input tree is the working tree for a Git repository, +any changes are also registered with Git (but not committed). + =back =head1 AUTHOR @@ -356,7 +473,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2013-2021 Russ Allbery +Copyright 2013-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/t/data/spin/update/input/module.rpod b/t/data/spin/update/input/module.rpod new file mode 100644 index 0000000..5f75697 --- /dev/null +++ b/t/data/spin/update/input/module.rpod @@ -0,0 +1 @@ +/path/Module.pm diff --git a/t/data/spin/update/input/readme.rpod b/t/data/spin/update/input/readme.rpod new file mode 100644 index 0000000..042274b --- /dev/null +++ b/t/data/spin/update/input/readme.rpod @@ -0,0 +1,2 @@ +/path/readme.pod +-c -t 'Basic Information' diff --git a/t/data/spin/update/input/script.rpod b/t/data/spin/update/input/script.rpod new file mode 100644 index 0000000..00f79c7 --- /dev/null +++ b/t/data/spin/update/input/script.rpod @@ -0,0 +1,3 @@ +/path/script + +/~eagle/styles/script.css diff --git a/t/data/spin/update/output/module.spin b/t/data/spin/update/output/module.spin new file mode 100644 index 0000000..31b97ce --- /dev/null +++ b/t/data/spin/update/output/module.spin @@ -0,0 +1,2 @@ +format: pod +path: /path/Module.pm diff --git a/t/data/spin/update/output/readme.spin b/t/data/spin/update/output/readme.spin new file mode 100644 index 0000000..c78a5cf --- /dev/null +++ b/t/data/spin/update/output/readme.spin @@ -0,0 +1,6 @@ +format: pod +options: + contents: true + navbar: false +path: /path/readme.pod +title: Basic Information diff --git a/t/data/spin/update/output/script.spin b/t/data/spin/update/output/script.spin new file mode 100644 index 0000000..5be3d64 --- /dev/null +++ b/t/data/spin/update/output/script.spin @@ -0,0 +1,3 @@ +format: pod +path: /path/script +style: /~eagle/styles/script.css diff --git a/t/lib/Test/DocKnot/Spin.pm b/t/lib/Test/DocKnot/Spin.pm index c7c33b8..2c520a2 100644 --- a/t/lib/Test/DocKnot/Spin.pm +++ b/t/lib/Test/DocKnot/Spin.pm @@ -82,6 +82,10 @@ sub is_spin_output_tree { # File::Find on the output directory. my $check_output = sub { my $file = $_; + if ($file eq '.git') { + $File::Find::prune = 1; + return; + } return if -d $file; # Determine the relative path and mark it as seen. @@ -191,7 +195,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2021 Russ Allbery +Copyright 2021-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/t/update/spin.t b/t/update/spin.t new file mode 100755 index 0000000..c692c80 --- /dev/null +++ b/t/update/spin.t @@ -0,0 +1,65 @@ +#!/usr/bin/perl +# +# Tests for the spin part of the App::DocKnot::Update module API. +# +# Copyright 2022 Russ Allbery +# +# SPDX-License-Identifier: MIT + +use 5.024; +use autodie; +use warnings; + +use lib 't/lib'; + +use File::Copy::Recursive qw(dircopy); +use Git::Repository (); +use Path::Tiny qw(path); +use Test::DocKnot::Spin qw(is_spin_output_tree); + +use Test::More; + +# Isolate from the environment. +local $ENV{XDG_CONFIG_HOME} = '/nonexistent'; +local $ENV{XDG_CONFIG_DIRS} = '/nonexistent'; + +# Load the module. +require_ok('App::DocKnot::Update'); + +# Construct the source tree. Copy t/data/spin/update/input into a fresh Git +# repository and commit it so that we can test the Git interaction. +my $input = path('t', 'data', 'spin', 'update', 'input'); +my $tempdir = Path::Tiny->tempdir(); +Git::Repository->run('init', { cwd => "$tempdir", quiet => 1 }); +dircopy($input, "$tempdir") + or die "$0: cannot copy $input to $tempdir: $!\n"; +my $repo = Git::Repository->new(work_tree => "$tempdir"); +$repo->run(config => '--add', 'user.name', 'Test'); +$repo->run(config => '--add', 'user.email', 'test@example.com'); +$repo->run(add => '-A', q{.}); +$repo->run(commit => '-q', '-m', 'Initial commit'); + +# Update the tree. +my $update = App::DocKnot::Update->new(); +$update->update_spin($tempdir); + +# Check the resulting output. +my $expected = path('t', 'data', 'spin', 'update', 'output'); +my $count = is_spin_output_tree("$tempdir", "$expected", 'Tree updated'); +my @changes = grep { m{ deleted | new [ ] file }xms } $repo->run('status'); +@changes = map { [split(q{ })] } sort(@changes); +is_deeply( + \@changes, + [ + ['deleted:', 'module.rpod'], + ['deleted:', 'readme.rpod'], + ['deleted:', 'script.rpod'], + ['new', 'file:', 'module.spin'], + ['new', 'file:', 'readme.spin'], + ['new', 'file:', 'script.spin'], + ], + 'Git operations', +); + +# Report the end of testing. +done_testing($count + 2); -- cgit v1.2.3 From abf331abc91809690467f83d15397cac4a09b8ae Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 2 Jan 2022 13:33:23 -0800 Subject: Convert App::DocKnot::Update to Path::Tiny Change the remaining uses of File::Spec and Perl6::Slurp to use Path::Tiny. --- lib/App/DocKnot/Update.pm | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/App/DocKnot/Update.pm b/lib/App/DocKnot/Update.pm index e14a726..500856d 100644 --- a/lib/App/DocKnot/Update.pm +++ b/lib/App/DocKnot/Update.pm @@ -17,12 +17,10 @@ use parent qw(App::DocKnot); use warnings; use Carp qw(croak); -use File::Spec; use JSON::MaybeXS qw(JSON); use Kwalify qw(validate); use Path::Iterator::Rule; use Path::Tiny qw(path); -use Perl6::Slurp; use YAML::XS (); # The older JSON metadata format stored text snippets in separate files in the @@ -51,10 +49,10 @@ our @JSON_METADATA_FILES = qw( # # @path - The relative path of the file as a list of components # -# Returns: The absolute path in the metadata directory +# Returns: Path::Tiny for the metadata file sub _metadata_path { my ($self, @path) = @_; - return File::Spec->catdir($self->{metadata}, @path); + return path($self->{metadata}, @path); } # Internal helper routine to read a file from the package metadata directory @@ -67,7 +65,8 @@ sub _metadata_path { # Throws: slurp exception on failure to read the file sub _load_metadata { my ($self, @path) = @_; - return slurp('<:utf8', $self->_metadata_path(@path)); + my $path = $self->_metadata_path(@path); + return $path->slurp_utf8(); } # Like _load_metadata, but interprets the contents of the metadata file as @@ -259,8 +258,8 @@ sub _convert_rpod_pointer { sub new { my ($class, $args_ref) = @_; my $self = { - metadata => $args_ref->{metadata} // 'docs/metadata', - output => $args_ref->{output} // 'docs/docknot.yaml', + metadata => path($args_ref->{metadata} // 'docs/metadata'), + output => path($args_ref->{output} // 'docs/docknot.yaml'), }; bless($self, $class); return $self; @@ -276,7 +275,7 @@ sub update { my ($self) = @_; # Ensure we were given a valid metadata argument. - if (!-d $self->{metadata}) { + if (!$self->{metadata}->is_dir()) { my $metadata = $self->{metadata}; croak("metadata path $metadata does not exist or is not a directory"); } @@ -358,7 +357,7 @@ sub update { } # Write the new YAML package configuration. - YAML::XS::DumpFile($self->{output}, $data_ref); + YAML::XS::DumpFile($self->{output}->stringify(), $data_ref); return; } -- cgit v1.2.3 From 66d9ae6a41b46481882c10474d92b8633a11e279 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Sun, 2 Jan 2022 13:52:21 -0800 Subject: Convert App::DocKnot::Spin::Versions to Path::Tiny Use Path::Tiny for all paths and file reading, and in the process add better support for UTF-8. Track the path to the .versions file in the object to support later methods to update a specific version. --- lib/App/DocKnot/Spin.pm | 5 ++--- lib/App/DocKnot/Spin/Versions.pm | 35 +++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/App/DocKnot/Spin.pm b/lib/App/DocKnot/Spin.pm index eade414..628bc19 100644 --- a/lib/App/DocKnot/Spin.pm +++ b/lib/App/DocKnot/Spin.pm @@ -540,8 +540,7 @@ sub spin { } my $versions_path = $input->child('.versions'); if ($versions_path->exists()) { - $self->{versions} - = App::DocKnot::Spin::Versions->new("$versions_path"); + $self->{versions} = App::DocKnot::Spin::Versions->new($versions_path); } if ($input->child('.git')->is_dir()) { $self->{repository} = Git::Repository->new(work_tree => $input); @@ -741,7 +740,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 1999-2011, 2013, 2021 Russ Allbery +Copyright 1999-2011, 2013, 2021-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm index 679b368..c0e04f4 100644 --- a/lib/App/DocKnot/Spin/Versions.pm +++ b/lib/App/DocKnot/Spin/Versions.pm @@ -18,6 +18,7 @@ use 5.024; use autodie; use warnings; +use Path::Tiny qw(path); use POSIX qw(mktime strftime); ############################################################################## @@ -56,18 +57,18 @@ sub _datetime_to_seconds { return mktime(@datetime); } -# Parse a .versions file and populate the App::DocKnot::Spin::Versions object. -# -# $path - Path to the .versions file +# Parse the .versions file and populate the App::DocKnot::Spin::Versions +# object. # # Raises: autodie exception on file read errors # Text exception on file parsing errors sub _read_data { - my ($self, $path) = @_; + my ($self) = @_; my $timestamp; - open(my $fh, '<', $path); - while (defined(my $line = <$fh>)) { + my $lineno = 0; + for my $line ($self->{path}->lines_utf8()) { + $lineno++; next if $line =~ m{ \A \s* \z }xms; next if $line =~ m{ \A \s* \# }xms; @@ -75,17 +76,17 @@ sub _read_data { my @depends; if ($line =~ m{ \A \s }xms) { if (!defined($timestamp)) { - die "continuation without previous entry in $path\n"; + die "continuation without previous entry in $self->{path}\n"; } @depends = split(qr{ \s+ }xms, $line); } else { my @line = split(qr{ \s+ }xms, $line); my ($package, $version, $date, $time, @files) = @line; if (!defined($time)) { - die "invalid line $. in $path\n"; + die "invalid line $lineno in $self->{path}\n"; } @depends = @files; - $timestamp = _datetime_to_seconds($date, $time, $path); + $timestamp = _datetime_to_seconds($date, $time, $self->{path}); $date = strftime('%Y-%m-%d', gmtime($timestamp)); $self->{versions}{$package} = [$version, $date]; } @@ -100,7 +101,6 @@ sub _read_data { } } } - close($fh); return; } @@ -119,14 +119,17 @@ sub new { my ($class, $path) = @_; # Create an empty object. + #<<< my $self = { - depends => {}, + depends => {}, + path => path($path), versions => {}, }; + #>>> bless($self, $class); # Parse the file into the newly-created object. - $self->_read_data($path); + $self->_read_data(); # Return the populated object. return $self; @@ -134,13 +137,13 @@ sub new { # Return the timestamp of the latest release affecting a different page. # -# $file - File name that may be listed as an affected file for a release +# $file - File path that may be listed as an affected file for a release # # Returns: The timestamp in seconds since epoch of the latest release # affecting that file, or 0 if there are none sub latest_release { my ($self, $file) = @_; - return $self->{depends}{$file} // 0; + return $self->{depends}{"$file"} // 0; } # Return the release date for a given package. @@ -189,7 +192,7 @@ App::DocKnot::Spin::Versions - Parse package release information for spin =head1 REQUIREMENTS -Perl 5.24 or later. +Perl 5.24 or later and the Path::Tiny module, available from CPAN. =head1 DESCRIPTION @@ -284,7 +287,7 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2004, 2021 Russ Allbery +Copyright 2004, 2021-2022 Russ Allbery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -- cgit v1.2.3 From e7e6c9a95fb4cd68110ecd51a8ea2b5accc17e78 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 11 Jan 2022 20:45:16 -0800 Subject: Add docknot release command Add new docknot release command and corresponding App::DocKnot::Release module to copy a tarball releaes (normally created by docknot dist) into a release area, update symlinks, archive any previous releases, and update the .versions file used by docknot spin. DocKnot now depends on Sort::Versions. --- Build.PL | 3 +- Changes | 6 + README | 1 + README.md | 1 + bin/docknot | 67 +++++++-- cpanfile | 1 + docs/docknot.yaml | 1 + lib/App/DocKnot/Command.pm | 6 + lib/App/DocKnot/Config.pm | 17 ++- lib/App/DocKnot/Dist.pm | 8 +- lib/App/DocKnot/Release.pm | 268 ++++++++++++++++++++++++++++++++++ lib/App/DocKnot/Spin/Versions.pm | 61 +++++++- lib/App/DocKnot/Util.pm | 71 ++++++++- share/schema/config.yaml | 6 +- t/data/generate/docknot/output/thread | 1 + t/release/basic.t | 116 +++++++++++++++ 16 files changed, 599 insertions(+), 35 deletions(-) create mode 100644 lib/App/DocKnot/Release.pm create mode 100755 t/release/basic.t diff --git a/Build.PL b/Build.PL index a4c8d70..c2258b3 100644 --- a/Build.PL +++ b/Build.PL @@ -2,7 +2,7 @@ # # Build script for the docknot application. # -# Copyright 2013, 2016, 2018-2021 Russ Allbery +# Copyright 2013, 2016, 2018-2022 Russ Allbery # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -78,6 +78,7 @@ my $build = Module::Build->new( 'Path::Tiny' => '0.101', 'Perl6::Slurp' => 0, 'Pod::Thread' => '3.01', + 'Sort::Versions' => 0, 'Template' => 0, 'YAML::XS' => '0.81', perl => '5.024', diff --git a/Changes b/Changes index adf5cfc..b761adf 100644 --- a/Changes +++ b/Changes @@ -2,6 +2,12 @@ 6.01 - Not Released + - Add new docknot release command and corresponding App::DocKnot::Release + module to copy a tarball releaes (normally created by docknot dist) + into a release area, update symlinks, archive any previous releases, + and update the .versions file used by docknot spin. DocKnot now + depends on Sort::Versions. + - Add new docknot update-spin command and corresponding update_spin method in App::DocKnot::Update to update a spin input tree to the latest expectations. Currently, all this does is convert *.rpod diff --git a/README b/README index a5c138f..febcd60 100644 --- a/README +++ b/README @@ -68,6 +68,7 @@ REQUIREMENTS * Path::Tiny 0.101 or later * Perl6::Slurp * Pod::Thread 3.01 or later + * Sort::Versions * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/README.md b/README.md index 4ab09e4..092cd3e 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ The following additional Perl modules are required to use it: * Path::Tiny 0.101 or later * Perl6::Slurp * Pod::Thread 3.01 or later +* Sort::Versions * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/bin/docknot b/bin/docknot index 3de49a9..b091f4f 100755 --- a/bin/docknot +++ b/bin/docknot @@ -22,7 +22,7 @@ __END__ =for stopwords Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense subcommand -subcommands distdir pgp-key cl2xhtml cvs2xhtml faq2html spin-rss +subcommands distdir pgp-key cl2xhtml cvs2xhtml faq2html spin-rss archivedir =head1 NAME @@ -38,6 +38,8 @@ B generate [B<-m> I] [B<-w> I] I