summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/App/DocKnot.pm12
-rw-r--r--lib/App/DocKnot/Command.pm3
-rw-r--r--lib/App/DocKnot/Config.pm11
-rw-r--r--lib/App/DocKnot/Dist.pm117
-rw-r--r--lib/App/DocKnot/Generate.pm26
-rw-r--r--lib/App/DocKnot/Release.pm14
-rw-r--r--lib/App/DocKnot/Spin.pm52
-rw-r--r--lib/App/DocKnot/Spin/Pointer.pm28
-rw-r--r--lib/App/DocKnot/Spin/RSS.pm234
-rw-r--r--lib/App/DocKnot/Spin/Sitemap.pm48
-rw-r--r--lib/App/DocKnot/Spin/Thread.pm23
-rw-r--r--lib/App/DocKnot/Spin/Versions.pm9
-rw-r--r--lib/App/DocKnot/Update.pm2
-rw-r--r--lib/App/DocKnot/Util.pm42
14 files changed, 315 insertions, 306 deletions
diff --git a/lib/App/DocKnot.pm b/lib/App/DocKnot.pm
index c288ed9..947b9d2 100644
--- a/lib/App/DocKnot.pm
+++ b/lib/App/DocKnot.pm
@@ -11,7 +11,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot 6.01;
+package App::DocKnot 7.00;
use 5.024;
use autodie;
@@ -19,8 +19,8 @@ use warnings;
use File::BaseDir qw(config_files);
use File::ShareDir qw(module_file);
-use File::Spec;
use Kwalify qw(validate);
+use Path::Tiny qw(path);
use YAML::XS ();
##############################################################################
@@ -50,7 +50,7 @@ sub appdata_path {
# If that doesn't work, use the data that came with the module.
if (!defined($path)) {
- $path = module_file('App::DocKnot', File::Spec->catfile(@path));
+ $path = module_file('App::DocKnot', path(@path)->stringify());
}
return $path;
}
@@ -113,8 +113,8 @@ App::DocKnot - Documentation and software release management
=head1 REQUIREMENTS
-Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify, and
-YAML::XS, all of which are available from CPAN.
+Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify,
+Path::Tiny, and YAML::XS, all of which are available from CPAN.
=head1 DESCRIPTION
@@ -154,7 +154,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2013-2021 Russ Allbery <rra@cpan.org>
+Copyright 2013-2022 Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm
index f032ab6..a0fa90c 100644
--- a/lib/App/DocKnot/Command.pm
+++ b/lib/App/DocKnot/Command.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Command 6.01;
+package App::DocKnot::Command 7.00;
use 5.024;
use autodie;
@@ -18,6 +18,7 @@ use warnings;
use App::DocKnot::Dist;
use App::DocKnot::Generate;
+use App::DocKnot::Release;
use App::DocKnot::Spin;
use App::DocKnot::Spin::RSS;
use App::DocKnot::Spin::Thread;
diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm
index 6b6716d..597e6cb 100644
--- a/lib/App/DocKnot/Config.pm
+++ b/lib/App/DocKnot/Config.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Config 6.01;
+package App::DocKnot::Config 7.00;
use 5.024;
use autodie;
@@ -195,6 +195,12 @@ release> is mandatory.
Sign distribution tarballs generated via C<docknot dist> with this PGP key.
Equivalent to the B<-p> option to C<docknot dist>.
+=item versions
+
+Path to the F<.versions> file that should be updated by C<docknot release>. A
+F<.versions> file records the versions and release dates of software packages.
+See L<App::Docknot::Spin::Versions> for more information.
+
=back
=head1 CLASS METHODS
@@ -261,7 +267,8 @@ SOFTWARE.
=head1 SEE ALSO
-L<docknot(1)>
+L<docknot(1)>, L<App::DocKnot::Dist>, L<App:DocKnot::Release>,
+L<App::DocKnot::Spin::Versions>
This module is part of the App-DocKnot distribution. The current version of
DocKnot is available from CPAN, or directly from its web site at
diff --git a/lib/App/DocKnot/Dist.pm b/lib/App/DocKnot/Dist.pm
index ccbeff4..585d243 100644
--- a/lib/App/DocKnot/Dist.pm
+++ b/lib/App/DocKnot/Dist.pm
@@ -10,19 +10,17 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Dist 6.01;
+package App::DocKnot::Dist 7.00;
use 5.024;
use autodie;
use warnings;
use App::DocKnot::Config;
+use App::DocKnot::Util qw(latest_tarball print_checked);
use Archive::Tar ();
use Carp qw(croak);
-use Cwd qw(getcwd);
-use File::Copy qw(move);
use File::Find qw(find);
-use File::Path qw(remove_tree);
use Git::Repository ();
use IO::Compress::Xz ();
use IO::Uncompress::Gunzip ();
@@ -30,6 +28,7 @@ use IPC::Run qw(run);
use IPC::System::Simple qw(systemx);
use List::SomeUtils qw(lastval);
use List::Util qw(any first);
+use Path::Tiny qw(path);
# Base commands to run for various types of distributions. Additional
# variations may be added depending on additional configuration parameters.
@@ -113,6 +112,11 @@ sub _expected_dist_files {
# Find all files in the source directory, stripping its path from the file
# name and excluding (and pruning) anything matching @DIST_IGNORE or in
# the distribution/ignore key of the package configuration.
+ #
+ # This uses File::Find rather than Path::Iterator::Rule like other parts
+ # of DocKnot because the ignore patterns are based on the whole path
+ # relative to the top of the distribution, and that's more annoying to do
+ # with Path::Iterator::Rule.
my $wanted = sub {
my $name = $File::Find::name;
$name =~ s{ \A \Q$path\E / }{}xms;
@@ -126,7 +130,7 @@ sub _expected_dist_files {
};
# Generate and return the list of files.
- find($wanted, $path);
+ find($wanted, "$path");
return @files;
}
@@ -135,31 +139,16 @@ sub _expected_dist_files {
# $path - The directory path
# $prefix - The tarball file prefix
#
-# Returns: The full path to the gzip tarball
+# Returns: The path to the gzip tarball
# Throws: Text exception if no gzip tarball was found
sub _find_gzip_tarball {
my ($self, $path, $prefix) = @_;
- my @files = $self->_find_matching_tarballs($path, $prefix);
- my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } @files;
+ my $files_ref = latest_tarball($path, $prefix)->{files};
+ my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } $files_ref->@*;
if (!defined($gzip_file)) {
die "cannot find gzip tarball for $prefix in $path\n";
}
- return File::Spec->catfile($path, $gzip_file);
-}
-
-# Find matching tarballs given a directory and a prefix.
-#
-# $path - The directory path
-# $prefix - The tarball file prefix
-#
-# Returns: All matching files, without the directory name, as a list
-sub _find_matching_tarballs {
- my ($self, $path, $prefix) = @_;
- my $pattern = qr{ \A \Q$prefix\E - \d.* [.]tar [.][xg]z \z }xms;
- opendir(my $source, $path);
- my @files = grep { $_ =~ $pattern } readdir($source);
- closedir($source);
- return @files;
+ return $path->child($gzip_file);
}
# Given a directory and a prefix for tarballs in that directory, ensure that
@@ -172,17 +161,15 @@ sub _find_matching_tarballs {
# Throws: Text exception on failure to read or write compressed files.
sub _generate_compression_formats {
my ($self, $path, $prefix) = @_;
- my @files = $self->_find_matching_tarballs($path, $prefix);
- if (!any { m{ [.]tar [.]xz \z }xms } @files) {
- my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } @files;
- my $xz_file = $gzip_file;
- $xz_file =~ s{ [.]gz \z }{.xz}xms;
- my $gzip_path = File::Spec->catfile($path, $gzip_file);
- my $xz_path = File::Spec->catfile($path, $xz_file);
+ my $files_ref = latest_tarball($path, $prefix)->{files};
+ if (!any { m{ [.]tar [.]xz \z }xms } $files_ref->@*) {
+ my $gzip_file = lastval { m{ [.]tar [.]gz \z }xms } $files_ref->@*;
+ my $gzip_path = $path->child($gzip_file);
+ my $xz_path = $path->child($gzip_path->basename('.gz') . '.xz');
# Open the input and output files.
- my $gzip_fh = IO::Uncompress::Gunzip->new($gzip_path);
- my $xz_fh = IO::Compress::Xz->new($xz_path);
+ my $gzip_fh = IO::Uncompress::Gunzip->new("$gzip_path");
+ my $xz_fh = IO::Compress::Xz->new("$xz_path");
# Read from the gzip file and write to the xz-compressed file.
my $buffer;
@@ -207,11 +194,9 @@ sub _generate_compression_formats {
# Text exception on failure to move a file
sub _move_tarballs {
my ($self, $source_path, $prefix, $dest_path) = @_;
- my @files = $self->_find_matching_tarballs($source_path, $prefix);
- for my $file (@files) {
- my $source_file = File::Spec->catfile($source_path, $file);
- move($source_file, $dest_path)
- or die "cannot move $source_file to $dest_path: $!\n";
+ my $files_ref = latest_tarball($source_path, $prefix)->{files};
+ for my $file ($files_ref->@*) {
+ $source_path->child($file)->move($dest_path->child($file));
}
return;
}
@@ -244,9 +229,13 @@ sub _replace_perl_path {
# Throws: Text exception on failure to sign the file
sub _sign_tarballs {
my ($self, $path, $prefix) = @_;
- my @files = $self->_find_matching_tarballs($path, $prefix);
- for my $file (@files) {
- my $tarball_path = File::Spec->catdir($path, $file);
+ my $files_ref = latest_tarball($path, $prefix)->{files};
+ for my $file (grep { m{ [.]tar [.][xg]z }xms } $files_ref->@*) {
+ my $tarball_path = $path->child($file);
+ my $sig_path = $path->child($tarball_path->basename() . '.asc');
+ if ($sig_path->exists()) {
+ $sig_path->remove();
+ }
systemx(
$self->{gpg}, '--detach-sign', '--armor', '-u',
$self->{pgp_key}, $tarball_path,
@@ -296,7 +285,7 @@ sub new {
#<<<
my $self = {
config => $config_reader->config(),
- distdir => $distdir,
+ distdir => path($distdir),
gpg => $args_ref->{gpg} // 'gpg',
perl => $args_ref->{perl},
pgp_key => $args_ref->{pgp_key} // $global_config_ref->{pgp_key},
@@ -318,8 +307,8 @@ sub new {
# means all expected files were found)
sub check_dist {
my ($self, $source, $tarball) = @_;
- my @expected = $self->_expected_dist_files(getcwd());
- my %expected = map { $_ => 1 } @expected;
+ my @expected = $self->_expected_dist_files(path(q{.}));
+ my %expected = map { ("$_", 1) } @expected;
my $archive = Archive::Tar->new($tarball);
for my $file ($archive->list_files()) {
$file =~ s{ \A [^/]* / }{}xms;
@@ -380,20 +369,21 @@ sub make_distribution {
my ($self) = @_;
# Determine the source directory and the distribution directory name.
- my $source = getcwd() or die "cannot get current directory: $!\n";
+ my $source = path(q{.})->realpath();
my $prefix = $self->{config}{distribution}{tarname};
# If the distribution directory name already exists, remove it. Automake
# may have made parts of it read-only, so be forceful in the removal.
- # Note that this does not pass the safe parameter and therefore should not
- # be called on attacker-controlled directories.
+ # Note that this disables safe mode and therefore should not be called on
+ # attacker-controlled directories.
chdir($self->{distdir});
- if (-d $prefix) {
- remove_tree($prefix);
+ my $workdir = path($prefix);
+ if ($workdir->is_dir()) {
+ $workdir->remove_tree({ safe => 0 });
}
# Export the Git repository into a new directory.
- my $repo = Git::Repository->new(work_tree => $source);
+ my $repo = Git::Repository->new(work_tree => "$source");
my @branches = $repo->run(
'for-each-ref' => '--format=%(refname:short)', 'refs/heads/',
);
@@ -408,31 +398,29 @@ sub make_distribution {
}
# Change to that directory and run the configured commands.
- chdir($prefix);
+ chdir($workdir);
for my $command_ref ($self->commands()) {
systemx($command_ref->@*);
}
- # Move the generated tarball to the parent directory.
- $self->_move_tarballs(File::Spec->curdir(), $prefix, File::Spec->updir());
+ # Generate additional compression formats if needed.
+ $self->_generate_compression_formats(path(q{.}), $prefix);
- # Remove the working tree.
- chdir(File::Spec->updir());
- remove_tree($prefix, { safe => 1 });
+ # Move the generated tarballs to the parent directory.
+ $self->_move_tarballs(path(q{.}), $prefix, $self->{distdir});
- # Generate additional compression formats if needed.
- $self->_generate_compression_formats(getcwd(), $prefix);
+ # Remove the working tree.
+ chdir($self->{distdir});
+ $workdir->remove_tree();
# Check the distribution for any missing files. If there are any, report
# them and then fail with an error.
- my $tarball = $self->_find_gzip_tarball(getcwd(), $prefix);
+ my $tarball = $self->_find_gzip_tarball($self->{distdir}, $prefix);
chdir($source);
my @missing = $self->check_dist($source, $tarball);
if (@missing) {
- print "Files found in local tree but not in distribution:\n"
- or die "cannot print to stdout: $!\n";
- print q{ } . join(qq{\n }, @missing) . "\n"
- or die "cannot print to stdout: $!\n";
+ print_checked("Files found in local tree but not in distribution:\n");
+ print_checked(q{ }, join(qq{\n }, @missing), "\n");
my $count = scalar(@missing);
my $files = ($count == 1) ? '1 file' : "$count files";
die "$files missing from distribution\n";
@@ -472,7 +460,8 @@ App::DocKnot::Dist - Prepare a distribution tarball
Git, Perl 5.24 or later, and the modules File::BaseDir, File::ShareDir,
Git::Repository, IO::Compress::Xz (part of IO-Compress-Lzma),
IO::Uncompress::Gunzip (part of IO-Compress), IPC::Run, IPC::System::Simple,
-Kwalify, List::SomeUtils, and YAML::XS, all of which are available from CPAN.
+Kwalify, List::SomeUtils, Path::Tiny, and YAML::XS, all of which are available
+from CPAN.
The tools to build whatever type of software distribution is being prepared
are also required, since the distribution is built and tested as part of
diff --git a/lib/App/DocKnot/Generate.pm b/lib/App/DocKnot/Generate.pm
index cab4365..72eb686 100644
--- a/lib/App/DocKnot/Generate.pm
+++ b/lib/App/DocKnot/Generate.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Generate 6.01;
+package App::DocKnot::Generate 7.00;
use 5.024;
use autodie;
@@ -18,17 +18,18 @@ 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 Path::Tiny qw(path);
use Template;
use Text::Wrap qw(wrap);
# Default output files for specific templates.
+#<<<
my %DEFAULT_OUTPUT = (
- 'readme' => 'README',
+ 'readme' => 'README',
'readme-md' => 'README.md',
);
+#>>>
##############################################################################
# Generator functions
@@ -480,11 +481,10 @@ sub generate {
$vars{to_text} = $self->_code_for_to_text;
$vars{to_thread} = $self->_code_for_to_thread;
- # Ensure we were given a valid template.
- $template = $self->appdata_path('templates', "${template}.tmpl");
-
# Run Template Toolkit processing.
- my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error());
+ $template = $self->appdata_path('templates', "${template}.tmpl");
+ my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' })
+ or croak(Template->error());
my $result;
$tt->process($template, \%vars, \$result) or croak($tt->error);
@@ -527,9 +527,7 @@ sub generate_output {
# Generate the output.
my $data = $self->generate($template);
- open(my $outfh, '>', $output);
- print_fh($outfh, $output, encode('utf-8', $data));
- close($outfh);
+ path($output)->spew_utf8($data);
return;
}
@@ -561,8 +559,8 @@ App::DocKnot::Generate - Generate documentation from package metadata
=head1 REQUIREMENTS
Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, Kwalify,
-Template (part of Template Toolkit), and YAML::XS, all of which are available
-from CPAN.
+Path::Tiny, Template (part of Template Toolkit), and YAML::XS, all of which
+are available from CPAN.
=head1 DESCRIPTION
@@ -661,7 +659,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2013-2021 Russ Allbery <rra@cpan.org>
+Copyright 2013-2022 Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/lib/App/DocKnot/Release.pm b/lib/App/DocKnot/Release.pm
index b5d8d83..b118281 100644
--- a/lib/App/DocKnot/Release.pm
+++ b/lib/App/DocKnot/Release.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Release 6.01;
+package App::DocKnot::Release 7.00;
use 5.024;
use autodie;
@@ -20,6 +20,7 @@ use App::DocKnot::Config;
use App::DocKnot::Spin::Versions;
use App::DocKnot::Util qw(latest_tarball);
use Carp qw(croak);
+use List::Util qw(min);
use Path::Tiny qw(path);
##############################################################################
@@ -122,9 +123,16 @@ sub release {
}
# Copy the new version into place and update the symlinks.
+ my @times;
$current_path->mkpath();
for my $file ($tarball_ref->{files}->@*) {
- $self->{distdir}->child($file)->copy($current_path->child($file));
+ my $source = $self->{distdir}->child($file);
+ my $dest = $current_path->child($file);
+ $source->copy($dest);
+ my ($atime, $mtime) = $source->stat()->@[8, 9];
+ push(@times, $mtime);
+ utime($atime, $mtime, $dest)
+ or die "cannot reset timestamps of $dest: $!\n";
my $generic_name = $file;
$generic_name =~ s{ \A (\Q$self->{tarname}\E) - [\d.]+ [.] }{$1.}xms;
my $generic_path = $current_path->child($generic_name);
@@ -136,7 +144,7 @@ sub release {
if ($self->{versions}) {
my $name = $self->{version_name};
my $version = $tarball_ref->{version};
- my $date = $tarball_ref->{date};
+ my $date = min(@times);
$self->{versions}->update_version($name, $version, $date);
}
return;
diff --git a/lib/App/DocKnot/Spin.pm b/lib/App/DocKnot/Spin.pm
index 5f4632e..f7a5978 100644
--- a/lib/App/DocKnot/Spin.pm
+++ b/lib/App/DocKnot/Spin.pm
@@ -11,11 +11,11 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin 6.01;
+package App::DocKnot::Spin 7.00;
use 5.024;
use autodie;
-use warnings;
+use warnings FATAL => 'utf8';
use App::DocKnot::Spin::Pointer;
use App::DocKnot::Spin::RSS;
@@ -70,7 +70,7 @@ sub _footer {
# Add the end-of-page navbar if we have sitemap information.
if ($self->{sitemap} && $self->{output}) {
my $page = $out_path->relative($self->{output});
- $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n";
+ $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
}
# Figure out the modification dates. Use the RCS/CVS Id if available,
@@ -325,7 +325,7 @@ sub _read_pointer {
}
# Return the details.
- return ($master, $options, $style);
+ return (path($master), $options, $style);
}
# Convert an input path to an output path.
@@ -360,8 +360,6 @@ sub _report_action {
#
# 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, $input) = @_;
@@ -387,16 +385,12 @@ sub _process_file {
$self->_report_action('Creating', $output);
$output->mkpath();
}
- my $rss_path = path($input, '.rss');
- if ($rss_path->exists()) {
- $self->{rss}->generate("$rss_path", "$input");
- }
} 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")) {
+ if ($self->{pointer}->is_out_of_date($input, $output)) {
$self->_report_action('Converting', $output);
- $self->{pointer}->spin_pointer("$input", "$output");
+ $self->{pointer}->spin_pointer($input, $output);
}
} elsif ($input->basename() =~ m{ [.] th \z }xms) {
my $output = $self->_output_for_file($input, '.th');
@@ -406,12 +400,11 @@ sub _process_file {
# a software release.
if ($output->exists() && $self->{versions}) {
my $relative = $input->relative($self->{source});
- my $time = $self->{versions}->latest_release("$relative");
+ my $time = $self->{versions}->latest_release($relative);
return
- if is_newer("$output", "$input")
- && $output->stat()->[9] >= $time;
+ if is_newer($output, $input) && $output->stat()->[9] >= $time;
} else {
- return if is_newer("$output", "$input");
+ return if is_newer($output, $input);
}
# The output file is not newer. Respin it.
@@ -421,7 +414,7 @@ sub _process_file {
my ($extension) = ($input->basename =~ m{ [.] ([^.]+) \z }xms);
if (defined($extension) && $rules{$extension}) {
my ($name, $sub) = $rules{$extension}->@*;
- my $output = $self->_output_for_file($input, $extension);
+ my $output = $self->_output_for_file($input, q{.} . $extension);
$self->{generated}{"$output"} = 1;
my ($source, $options, $style) = $self->_read_pointer($input);
return if is_newer($output, $input, $source);
@@ -430,14 +423,13 @@ sub _process_file {
} else {
my $output = $self->_output_for_file($input);
$self->{generated}{"$output"} = 1;
- return if is_newer("$output", "$input");
+ return if is_newer($output, $input);
$self->_report_action('Updating', $output);
$input->copy($output);
}
}
return;
}
-## use critic
# 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
@@ -512,7 +504,6 @@ sub spin {
# Reset data from a previous run.
delete $self->{repository};
- delete $self->{rss};
delete $self->{sitemap};
delete $self->{versions};
@@ -536,7 +527,7 @@ sub spin {
# Read metadata from the top of the input directory.
my $sitemap_path = $input->child('.sitemap');
if ($sitemap_path->exists()) {
- $self->{sitemap} = App::DocKnot::Spin::Sitemap->new("$sitemap_path");
+ $self->{sitemap} = App::DocKnot::Spin::Sitemap->new($sitemap_path);
}
my $versions_path = $input->child('.versions');
if ($versions_path->exists()) {
@@ -546,13 +537,14 @@ sub spin {
$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 = $input->child('.rss');
- if ($rss_path->exists()) {
- $self->{rss}->generate("$rss_path", "$input");
+ # Process all .rss files in the input tree first. This is done as a
+ # separate pass because Path::Iterator::Rule appears to not always re-read
+ # the directory when it's modified during the iteration.
+ my $rss = App::DocKnot::Spin::RSS->new({ base => $input });
+ my $rule = Path::Iterator::Rule->new()->name('.rss');
+ my $iter = $rule->iter("$input", { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ $rss->generate(path($file), path($file)->parent);
}
# Create a new thread converter object.
@@ -581,9 +573,9 @@ sub spin {
#>>>
# Process the input tree.
- my $rule = Path::Iterator::Rule->new();
+ $rule = Path::Iterator::Rule->new();
$rule = $rule->skip($rule->new()->name($self->{excludes}->@*));
- my $iter = $rule->iter("$input", { follow_symlinks => 0 });
+ $iter = $rule->iter("$input", { follow_symlinks => 0 });
while (defined(my $file = $iter->())) {
$self->_process_file(path($file));
}
diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm
index 871e267..a16250c 100644
--- a/lib/App/DocKnot/Spin/Pointer.pm
+++ b/lib/App/DocKnot/Spin/Pointer.pm
@@ -10,12 +10,12 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Pointer 6.01;
+package App::DocKnot::Spin::Pointer 7.00;
use 5.024;
use autodie;
use parent qw(App::DocKnot);
-use warnings;
+use warnings FATAL => 'utf8';
use App::DocKnot::Config;
use App::DocKnot::Util qw(is_newer);
@@ -66,11 +66,11 @@ sub _spin_markdown {
my ($links, $navbar, $style);
if ($self->{sitemap}) {
my $page = $output->relative($self->{output});
- my @links = $self->{sitemap}->links("/$page");
+ 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);
}
@@ -172,7 +172,8 @@ sub new {
}
# Create and return the object.
- my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error());
+ my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' })
+ or croak(Template->error());
#<<<
my $self = {
output => $args_ref->{output},
@@ -199,7 +200,6 @@ 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');
my $path = path($data_ref->{path})->absolute($pointer->parent());
if (!$path->exists()) {
@@ -218,8 +218,6 @@ sub is_out_of_date {
# 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} //= {};
@@ -254,13 +252,17 @@ App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file
use App::DocKnot::Spin::Pointer;
use App::DocKnot::Spin::Sitemap;
+ use Path::Tiny qw(path);
- my $sitemap = App::DocKnot::Spin::Sitemap->new('/input/.sitemap');
+ my $sitemap_path = path('/input/.sitemap');
+ my $sitemap = App::DocKnot::Spin::Sitemap->new($sitemap_path);
my $pointer = App::DocKnot::Spin::Pointer->new({
- output => '/output',
+ output => path('/output'),
sitemap => $sitemap,
});
- $pointer->spin_pointer('/input/file.spin', '/output/file.html');
+ my $input = path('/input/file.spin');
+ my $output = path('/output/file.html');
+ $pointer->spin_pointer($input, $output);
=head1 REQUIREMENTS
@@ -326,11 +328,13 @@ C<sitemap> argument.
Returns true if OUTPUT is missing or if it was modified less recently than the
modification time of either POINTER or the underlying file that it points to.
+Both paths must be Path::Tiny objects.
=item spin_pointer(POINTER, OUTPUT)
Convert a single pointer file to HTML. POINTER is the path to the pointer
-file, and OUTPUT is the path to where to write the output.
+file, and OUTPUT is the path to where to write the output. Both paths must
+be Path::Tiny objects.
=back
diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm
index 3f8fb70..2b0e0fc 100644
--- a/lib/App/DocKnot/Spin/RSS.pm
+++ b/lib/App/DocKnot/Spin/RSS.pm
@@ -9,19 +9,19 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::RSS 6.01;
+package App::DocKnot::Spin::RSS 7.00;
use 5.024;
use autodie;
-use warnings;
+use parent qw(App::DocKnot);
+use warnings FATAL => 'utf8';
-use App::DocKnot;
use App::DocKnot::Spin::Thread;
use App::DocKnot::Util qw(print_checked print_fh);
+use Carp qw(croak);
use Date::Language ();
use Date::Parse qw(str2time);
use Path::Tiny qw(path);
-use Perl6::Slurp qw(slurp);
use POSIX qw(strftime);
##############################################################################
@@ -141,7 +141,7 @@ sub _spin_file {
# $base - Base path for all output
sub _report_action {
my ($self, $action, $output) = @_;
- my $shortout = $output->relative($self->{base} // path());
+ my $shortout = $output->relative($self->{base} // path(q{.}));
print_checked("$action .../$shortout\n");
return;
}
@@ -385,8 +385,6 @@ sub _rss_review {
# $entries_ref - Array of entries in the RSS feed
sub _rss_output {
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
# entries, published in the obnoxious format used by RSS.
@@ -398,35 +396,17 @@ sub _rss_output {
$latest = strftime($format, localtime($entries_ref->[0]{date}));
}
- # Output the RSS header.
- print_fh($fh, $file, <<"EOC");
-<?xml version="1.0" encoding="UTF-8"?>
-<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
- <channel>
- <title>$metadata_ref->{title}</title>
- <link>$metadata_ref->{base}</link>
- <description>$metadata_ref->{description}</description>
- <language>$metadata_ref->{language}</language>
- <pubDate>$latest</pubDate>
- <lastBuildDate>$now</lastBuildDate>
- <generator>DocKnot $App::DocKnot::VERSION</generator>
-EOC
+ # Determine the URL of the RSS file we're generating, if possible.
+ my $url;
if ($metadata_ref->{'rss-base'}) {
my $name = $file->basename();
- my $url = $metadata_ref->{'rss-base'} . $name;
- print_fh(
- $fh,
- $file,
- qq{ <atom:link href="$url" rel="self"\n},
- qq{ type="application/rss+xml" />\n},
- );
+ $url = $metadata_ref->{'rss-base'} . $name;
}
- print_fh($fh, $file, "\n");
- # Output each entry, formatting the contents of the entry as we go.
+ # Format the entries.
+ my @formatted_entries;
for my $entry_ref ($entries_ref->@*) {
my $date = $lang->strftime($format, [localtime($entry_ref->{date})]);
- my $title = _escape($entry_ref->{title});
my $description;
if ($entry_ref->{description}) {
$description = _escape($entry_ref->{description});
@@ -448,32 +428,39 @@ EOC
( [./\w] [^\"]+ ) \"
}{ $1 . _absolute_url($2, $entry_ref->{link}) . qq{\"} }xmsge;
- # Optionally add an attribute indicating this is not a permanent link.
- # Assume any URL is a permanent link.
- my $perma = q{};
- if ($entry_ref->{guid} !~ m{ \A http }xms) {
- $perma = ' isPermaLink="false"';
- }
-
- # Output the entry.
- print_fh(
- $fh,
- $file,
- " <item>\n",
- " <title>$title</title>\n",
- " <link>$entry_ref->{link}</link>\n",
- " <description><![CDATA[\n",
- $description,
- " ]]></description>\n",
- " <pubDate>$date</pubDate>\n",
- " <guid$perma>$entry_ref->{guid}</guid>\n",
- " </item>\n",
- );
+ # Convert this into an object suitable for the output template.
+ #<<<
+ my $formatted_ref = {
+ date => $date,
+ description => $description,
+ guid => $entry_ref->{guid},
+ link => $entry_ref->{link},
+ title => $entry_ref->{title},
+ };
+ #>>>
+ push(@formatted_entries, $formatted_ref);
}
- # Close the RSS structure.
- print_fh($fh, $file, " </channel>\n</rss>\n");
- close($fh);
+ # Generate the RSS output using the template.
+ #<<<
+ my %vars = (
+ base => $metadata_ref->{base},
+ description => $metadata_ref->{description},
+ docknot_version => $App::DocKnot::VERSION,
+ entries => \@formatted_entries,
+ language => $metadata_ref->{language},
+ latest => $latest,
+ now => $now,
+ title => $metadata_ref->{title},
+ url => $url,
+ );
+ #>>>
+ my $result;
+ $self->{template}->process($self->{templates}{rss}, \%vars, \$result)
+ or croak($self->{template}->error());
+
+ # Write the result to the output file.
+ $file->spew_utf8($result);
return;
}
@@ -488,49 +475,51 @@ EOC
# $entries_ref - Entries
sub _thread_output {
my ($self, $file, $metadata_ref, $entries_ref) = @_;
- my $fh = $file->openw_utf8();
-
- # Page prefix.
- if ($metadata_ref->{'thread-prefix'}) {
- print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n");
- } else {
- print_fh(
- $fh,
- $file,
- "\\heading[Recent Changes][indent]\n\n",
- "\\h1[Recent Changes]\n\n",
- );
- }
- # Print out each entry.
- my $last_month;
+ # The entries are in a flat list, but we want a two-level list of entries
+ # by month so that the template can add appropriate month headings.
+ # Restructure the entry list accordingly.
+ my (@entries_by_month, $last_month);
for my $entry_ref ($entries_ref->@*) {
my $month = strftime('%B %Y', localtime($entry_ref->{date}));
-
- # Put headings before each month.
- if (!$last_month || $month ne $last_month) {
- print_fh($fh, $file, "\\h2[$month]\n\n");
- $last_month = $month;
- }
-
- # Format each entry.
my $date = strftime('%Y-%m-%d', localtime($entry_ref->{date}));
- print_fh(
- $fh,
- $file,
- "\\desc[$date \\entity[mdash]\n",
- " \\link[$entry_ref->{link}]\n",
- " [$entry_ref->{title}]][\n",
- );
+
+ # Copy the entry with a reformatted description.
my $description = $entry_ref->{description};
$description =~ s{ ^ }{ }xmsg;
$description =~ s{ \\ }{\\\\}xmsg;
- print_fh($fh, $file, $description, "]\n\n");
+ #<<<
+ my $formatted_ref = {
+ date => $date,
+ description => $description,
+ link => $entry_ref->{link},
+ title => $entry_ref->{title},
+ };
+ #<<<
+
+ # Add the entry to the appropriate month.
+ if (!$last_month || $month ne $last_month) {
+ my $month_ref = { heading => $month, entries => [$formatted_ref] };
+ push(@entries_by_month, $month_ref);
+ $last_month = $month;
+ } else {
+ push($entries_by_month[-1]{entries}->@*, $formatted_ref);
+ }
}
- # Print out the end of the page.
- print_fh($fh, $file, "\\signature\n");
- close($fh);
+ # Generate the RSS output using the template.
+ #<<<
+ my %vars = (
+ prefix => $metadata_ref->{'thread-prefix'},
+ entries => \@entries_by_month,
+ );
+ #>>>
+ my $result;
+ $self->{template}->process($self->{templates}{changes}, \%vars, \$result)
+ or croak($self->{template}->error());
+
+ # Write the result to the output file.
+ $file->spew_utf8($result);
return;
}
@@ -636,14 +625,9 @@ sub _index_review {
# $entries_ref - Entries
sub _index_output {
my ($self, $file, $base, $metadata_ref, $entries_ref) = @_;
- my $fh = $file->openw_utf8();
- # Output the prefix.
- if ($metadata_ref->{'index-prefix'}) {
- print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n");
- }
-
- # Output each entry.
+ # Format each entry.
+ my @formatted_entries;
for my $entry_ref ($entries_ref->@*) {
my @time = localtime($entry_ref->{date});
my $date = strftime('%Y-%m-%d %H:%M', @time);
@@ -671,24 +655,33 @@ sub _index_output {
( \\ image \s* \[ ) ( [^\]]+ ) \]
}{$1 . _relative_url($2, $metadata_ref->{'index-base'}) . ']' }xmsge;
- # Print out the entry.
- print_fh(
- $fh,
- $file,
- "\\h2[$day: $entry_ref->{title}]\n\n",
- $text,
- "\\class(footer)[$date \\entity[mdash]\n",
- " \\link[$entry_ref->{link}]\n",
- " [Permanent link]]\n\n",
- );
+ # Add the entry to the list.
+ #<<<
+ my $formatted_ref = {
+ date => $date,
+ day => $day,
+ link => $entry_ref->{link},
+ title => $entry_ref->{title},
+ text => $text,
+ };
+ #>>>
+ push(@formatted_entries, $formatted_ref);
}
- # Print out the end of the page.
- if ($metadata_ref->{'index-suffix'}) {
- print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n");
- }
- print_fh($fh, $file, "\\signature\n");
- close($fh);
+ # Generate the RSS output using the template.
+ #<<<
+ my %vars = (
+ prefix => $metadata_ref->{'index-prefix'},
+ suffix => $metadata_ref->{'index-suffix'},
+ entries => \@formatted_entries,
+ );
+ #>>>
+ my $result;
+ $self->{template}->process($self->{templates}{index}, \%vars, \$result)
+ or croak($self->{template}->error());
+
+ # Write the result to the output file.
+ $file->spew_utf8($result);
return;
}
@@ -706,11 +699,22 @@ sub new {
my ($class, $args_ref) = @_;
# Create and return the object.
+ my $base = defined($args_ref->{base}) ? path($args_ref->{base}) : undef;
+ my $tt = Template->new({ ABSOLUTE => 1, ENCODING => 'utf8' })
+ or croak(Template->error());
+ #<<<
my $self = {
- base => defined($args_ref->{base}) ? path($args_ref->{base}) : undef,
- spin => App::DocKnot::Spin::Thread->new(),
+ base => $base,
+ spin => App::DocKnot::Spin::Thread->new(),
+ template => $tt,
};
bless($self, $class);
+ $self->{templates} = {
+ changes => $self->appdata_path('templates', 'changes.tmpl'),
+ index => $self->appdata_path('templates', 'index.tmpl'),
+ rss => $self->appdata_path('templates', 'rss.tmpl'),
+ };
+ #>>>
return $self;
}
@@ -722,7 +726,7 @@ sub generate {
my ($self, $source, $base) = @_;
$source = path($source);
$base //= $self->{base};
- $base = defined($base) ? path($base) : path();
+ $base = defined($base) ? path($base) : path(q{.});
# Read in the changes.
my ($metadata_ref, $changes_ref) = $self->_parse_changes($source);
@@ -1082,7 +1086,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2008, 2010-2012, 2021 Russ Allbery <rra@cpan.org>
+Copyright 2008, 2010-2012, 2021-2022 Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm
index 3cbc97a..884765c 100644
--- a/lib/App/DocKnot/Spin/Sitemap.pm
+++ b/lib/App/DocKnot/Spin/Sitemap.pm
@@ -12,13 +12,14 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Sitemap 6.01;
+package App::DocKnot::Spin::Sitemap 7.00;
use 5.024;
use autodie;
use warnings;
use List::SomeUtils qw(pairwise);
+use Path::Tiny qw(path);
##############################################################################
# File parsing
@@ -48,7 +49,7 @@ sub _read_data {
my @indents;
# Parse the file.
- open(my $fh, '<', $path);
+ my $fh = $path->openr_utf8();
while (defined(my $line = <$fh>)) {
next if $line =~ m{ \A \s* \# }xms;
chomp($line);
@@ -67,14 +68,14 @@ sub _read_data {
# Regular line. Parse it.
my ($spaces, $url, $desc)
- = $line =~ m{ \A ([ ]*) ([^\s:]+): \s+ (.+) \z}xms;
+ = $line =~ m{ \A ([ ]*) /([^\s:]*): \s+ (.+) \z}xms;
if (!defined($desc)) {
die "invalid line $. in $path\n";
}
# Error on duplicate lines.
if ($seen{$url}) {
- die "duplicate entry for $url in $path (line $.)\n";
+ die "duplicate entry for /$url in $path (line $.)\n";
}
$seen{$url} = 1;
@@ -121,9 +122,9 @@ sub _read_data {
# Returns: $desc escaped so that it's safe to interpolate into an attribute
sub _escape {
my ($desc, $is_attr) = @_;
- $desc =~ s{ & }{&amp;}xmsg;
- $desc =~ s{ < }{&lt;}xmsg;
- $desc =~ s{ > }{&gt;}xmsg;
+ $desc =~ s{ & }{&amp;}xmsg;
+ $desc =~ s{ < }{&lt;}xmsg;
+ $desc =~ s{ > }{&gt;}xmsg;
if ($is_attr) {
$desc =~ s{ \" }{&quot;}xmsg;
}
@@ -152,8 +153,8 @@ sub _relative {
# If there are the same number of components in both links, the link
# should be relative to the current directory. Otherwise, ascend to the
# common prefix and then descend to the dest link.
- if (@origin == 1 && @dest == 1) {
- return length($dest[0]) > 0 ? $dest[0] : q{./};
+ if (@origin == 1 && @dest <= 1) {
+ return (@dest && length($dest[0])) > 0 ? $dest[0] : q{./};
} else {
return ('../' x $#origin) . join(q{/}, @dest);
}
@@ -168,16 +169,21 @@ sub _relative {
# The relative URL and description may be undef if missing.
sub _page_links {
my ($self, $path) = @_;
- $path =~ s{ /index[.]html \z }{/}xms;
+ my $key;
+ if ($path->basename() eq 'index.html') {
+ $key = $path->parent() . q{/};
+ } else {
+ $key = "$path";
+ }
# If the page is not present in the sitemap, return nothing. There are
# also no meaningful links to generate for the top page.
- return () if ($path eq q{/} || !$self->{links}{$path});
+ return () if ($key eq q{/} || !$self->{links}{$key});
# Convert all the links to relative and add the page descriptions.
return
map { defined ? [_relative($path, $_), $self->{pagedesc}{$_}] : undef }
- $self->{links}{$path}->@*;
+ $self->{links}{$key}->@*;
}
##############################################################################
@@ -217,7 +223,7 @@ sub new {
bless($self, $class);
# Parse the file into the newly-created object.
- $self->_read_data($path);
+ $self->_read_data(path($path));
# Return the populated object.
return $self;
@@ -226,12 +232,12 @@ sub new {
# Return the <link> tags for a given output file, suitable for its <head>
# section.
#
-# $path - URL path to the output with leading slash
+# $path - Path to the output file relative to the top of the output tree
#
# Returns: List of lines to add to the <head> section
sub links {
my ($self, $path) = @_;
- my @links = $self->_page_links($path);
+ my @links = $self->_page_links(path($path));
return () if !@links;
# We only care about the first parent, not the rest of the chain to the
@@ -259,7 +265,7 @@ sub links {
}
# Add the link to the top-level page.
- my $url = _relative($path, q{/});
+ my $url = _relative($path, q{});
push(@output, qq{ <link rel="top" href="$url" />\n});
# Return the results.
@@ -268,12 +274,12 @@ sub links {
# Return the navigation bar for a given output file.
#
-# $path - URL path to the output with leading slash
+# $path - Path to the output file relative to the top of the output tree
#
# Returns: List of lines that create the navbar
sub navbar {
my ($self, $path) = @_;
- my ($prev, $next, @parents) = $self->_page_links($path);
+ my ($prev, $next, @parents) = $self->_page_links(path($path));
return () if !@parents;
# Construct the left and right links (previous and next).
@@ -329,7 +335,6 @@ sub sitemap {
# Build the sitemap as nested unordered lists.
for my $page ($self->{sitemap}->@*) {
my ($indent, $url, $desc) = $page->@*;
- $url =~ s{ \A / }{}xms;
# Skip the top page.
next if $indent == 0;
@@ -383,7 +388,8 @@ App::DocKnot::Spin::Sitemap - Generate page navigation links for spin
=head1 REQUIREMENTS
-Perl 5.24 or later and List::SomeUtils, which is available from CPAN.
+Perl 5.24 or later and the List::SomeUtils and Path::Tiny modules, both of
+which are available from CPAN.
=head1 DESCRIPTION
@@ -464,7 +470,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 1999-2000, 2002-2004, 2008, 2021 Russ Allbery <rra@cpan.org>
+Copyright 1999-2000, 2002-2004, 2008, 2021-2022 Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm
index 0d84bd7..1d1ef52 100644
--- a/lib/App/DocKnot/Spin/Thread.pm
+++ b/lib/App/DocKnot/Spin/Thread.pm
@@ -9,14 +9,15 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Thread 6.01;
+package App::DocKnot::Spin::Thread 7.00;
use 5.024;
use autodie;
-use warnings;
+use warnings FATAL => 'utf8';
use App::DocKnot;
use App::DocKnot::Util qw(print_fh);
+use Encode qw(decode);
use Git::Repository ();
use Image::Size qw(html_imgsize);
use Path::Tiny qw(path);
@@ -1079,7 +1080,7 @@ sub _cmd_heading {
# Add <link> tags based on the sitemap.
if ($self->{sitemap} && defined($page)) {
- my @links = $self->{sitemap}->links("/$page");
+ my @links = $self->{sitemap}->links($page);
if (@links) {
$output .= join(q{}, @links);
}
@@ -1098,7 +1099,7 @@ sub _cmd_heading {
# Add the <body> tag and the navbar (if we have a sitemap).
$output .= "\n<body>\n";
if ($self->{sitemap} && defined($page)) {
- my @navbar = $self->{sitemap}->navbar("/$page");
+ my @navbar = $self->{sitemap}->navbar($page);
if (@navbar) {
$output .= join(q{}, @navbar);
}
@@ -1278,7 +1279,7 @@ sub _cmd_signature {
# Add the end-of-page navbar if we have sitemap information.
if ($self->{sitemap} && $self->{output}) {
my $page = $self->{out_path}->relative($self->{output});
- $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n";
+ $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
}
# Figure out the modification dates. Use the Git repository if available.
@@ -1467,10 +1468,10 @@ sub new {
sub spin_thread {
my ($self, $thread, $input) = @_;
my $result;
- open(my $out_fh, '>', \$result);
+ open(my $out_fh, '>:raw:encoding(utf-8)', \$result);
$self->_parse_document($thread, $input, $out_fh, undef);
close($out_fh);
- return $result;
+ return decode('utf-8', $result);
}
# Spin a single file of thread to HTML.
@@ -1497,7 +1498,7 @@ sub spin_thread_file {
$output = path($output)->absolute();
$out_fh = $output->openw_utf8();
} else {
- open($out_fh, '>&', 'STDOUT');
+ open($out_fh, '>&:raw:encoding(utf-8)', 'STDOUT');
}
# Do the work.
@@ -1526,9 +1527,9 @@ sub spin_thread_output {
my $out_fh;
if (defined($output)) {
$output = path($output)->absolute();
- $out_fh = $output->filehandle('>');
+ $out_fh = $output->openw_utf8();
} else {
- open($out_fh, '>&', 'STDOUT');
+ open($out_fh, '>&:raw:encoding(utf-8)', 'STDOUT');
}
# Do the work.
@@ -2097,7 +2098,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 1999-2011, 2013, 2021 Russ Allbery <rra@cpan.org>
+Copyright 1999-2011, 2013, 2021-2022 Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm
index 1655ff0..2b6f759 100644
--- a/lib/App/DocKnot/Spin/Versions.pm
+++ b/lib/App/DocKnot/Spin/Versions.pm
@@ -12,7 +12,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Versions 6.01;
+package App::DocKnot::Spin::Versions 7.00;
use 5.024;
use autodie;
@@ -168,9 +168,7 @@ sub update_version {
# Edits the line for the package to replace the version and release date.
my $edit = sub {
- my $line = $_;
- my ($product, $old_version, $old_date, $old_time)
- = split(q{ }, $line);
+ my ($product, $old_version, $old_date, $old_time) = split(q{ });
return if $product ne $package;
# We're going to replace the old version with the new one, but we need
@@ -184,6 +182,7 @@ sub update_version {
}
# Make the replacement.
+ my $line = $_;
$line =~ s{ \Q$old_version\E }{$version_string}xms;
$line =~ s{ \Q$old_date\E }{$date}xms;
$line =~ s{ \Q$old_time\E }{$time}xms;
@@ -192,7 +191,7 @@ sub update_version {
# Apply that change to our versions file, and then re-read the contents to
# update the internal data structure.
- $self->{path}->edit_utf8($edit);
+ $self->{path}->edit_lines_utf8($edit);
$self->_read_data();
return;
}
diff --git a/lib/App/DocKnot/Update.pm b/lib/App/DocKnot/Update.pm
index 78b33de..4b43fba 100644
--- a/lib/App/DocKnot/Update.pm
+++ b/lib/App/DocKnot/Update.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Update 6.01;
+package App::DocKnot::Update 7.00;
use 5.024;
use autodie;
diff --git a/lib/App/DocKnot/Util.pm b/lib/App/DocKnot/Util.pm
index e5e4014..cffa4f5 100644
--- a/lib/App/DocKnot/Util.pm
+++ b/lib/App/DocKnot/Util.pm
@@ -9,15 +9,16 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Util 6.01;
+package App::DocKnot::Util 7.00;
use 5.024;
use autodie;
-use warnings;
+use warnings FATAL => 'utf8';
use Carp qw(croak);
use Exporter qw(import);
use List::SomeUtils qw(all);
+use Path::Tiny qw(path);
use Sort::Versions qw(versioncmp);
our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh);
@@ -34,9 +35,9 @@ our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh);
# Returns: True if $file exists and is newer than @others, false otherwise
sub is_newer {
my ($file, @others) = @_;
- return if !-e $file;
- my $file_mtime = (stat($file))[9];
- my @others_mtimes = map { (stat)[9] } @others;
+ return if !$file->exists();
+ my $file_mtime = $file->stat()->[9];
+ my @others_mtimes = map { $_->stat()->[9] } @others;
return all { $file_mtime >= $_ } @others_mtimes;
}
@@ -48,7 +49,6 @@ sub is_newer {
#
# Returns: Anonymous hash with the following keys:
# version - Latest version found
-# date - Date (in seconds since epoch) of oldest file
# files - Array of files for that version
# or undef if no matching files were found
# Throws: Text exception on any error
@@ -68,14 +68,10 @@ sub latest_tarball {
my $latest = $versions[0][0];
@files = map { $_->[1] } grep { $_->[0] eq $latest } @versions;
- # Find the timestamps of those files.
- my @times = sort(map { $path->child($_)->stat()->[9] } @files);
-
# Return the results.
#<<<
return {
version => $latest,
- date => $times[0],
files => \@files,
};
#<<<
@@ -126,19 +122,25 @@ App::DocKnot::Util - Shared utility functions for other DocKnot modules
=head1 SYNOPSIS
- use App::DocKnot::Util qw(is_newer print_checked print_fh);
+ use App::DocKnot::Util qw(
+ is_newer latest_tarball print_checked print_fh
+ );
+ use Path::Tiny qw(path);
print_checked('some stdout output');
- if (!is_newer('/output', '/input-1', '/input-2')) {
+ my @inputs = (path('/input-1'), path('/input-2'));
+ if (!is_newer(path('/output'), @inputs)) {
open(my $fh, '>', '/output');
print_fh($fh, '/output', 'some stuff');
close($fh);
}
+ my $latest_ref = latest_tarball(path('/archive'), 'App-Foo');
+
=head1 REQUIREMENTS
-Perl 5.24 or later and the modules List::SomeUtils and Sort::Versions,
-available from CPAN.
+Perl 5.24 or later and the modules List::SomeUtils, Path::Tiny, and
+Sort::Versions, available from CPAN.
=head1 DESCRIPTION
@@ -155,20 +157,18 @@ used if desired.
Returns a true value if FILE exists and has a last modified time that is newer
or equal to the last modified times of all SOURCE files, and otherwise returns
a false value. Used primarily to determine if a given output file is
-up-to-date with respect to its source files.
+up-to-date with respect to its source files. All paths must be Path::Tiny
+objects.
=item latest_tarball(PATH, NAME)
Returns data including a file list for the latest tarballs (by version number)
-for a given software package NAME in the directory PATH. Versions are compared
-using Sort::Versions. The return valid is a hash with the following keys:
+for a given software package NAME in the directory PATH (which must be a
+Path::Tiny object). Versions are compared using Sort::Versions. The return
+valid is a hash with the following keys:
=over 4
-=item date
-
-The timestamp of the oldest file for that version, in seconds since epoch.
-
=item files
The list of files found for that version.