summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2022-01-17 12:54:34 -0800
committerRuss Allbery <rra@cpan.org>2022-01-17 12:54:34 -0800
commit6842a49e21226e605c955676007973a209eef449 (patch)
tree7fee5f735b0adcc102d87ec70280bd6125e3404d
parentca30d64285e120b991f994808100ff866d7bdf7a (diff)
parent9c21cc6b8767be4d13893940ea59e1d1a0c33457 (diff)
New upstream version 7.00
-rw-r--r--Changes33
-rw-r--r--MANIFEST3
-rw-r--r--META.json30
-rw-r--r--META.yml30
-rw-r--r--README2
-rw-r--r--docs/docknot.yaml6
-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
-rw-r--r--share/templates/changes.tmpl12
-rw-r--r--share/templates/index.tmpl8
-rw-r--r--share/templates/rss.tmpl23
-rwxr-xr-xt/cli/generate.t44
-rwxr-xr-xt/cli/spin.t65
-rwxr-xr-xt/config/basic.t9
-rw-r--r--t/data/generate/docknot/output/thread1
-rwxr-xr-xt/data/regenerate-data39
-rw-r--r--t/data/spin/input/.rss6
-rw-r--r--t/data/spin/input/reviews/books/0-385-49362-2.th2
-rw-r--r--t/data/spin/output/changes.html144
-rw-r--r--t/data/spin/output/changes.rss22
-rw-r--r--t/data/spin/output/journal/debian.rss4
-rw-r--r--t/data/spin/output/journal/index.html6
-rw-r--r--t/data/spin/output/journal/index.rss8
-rw-r--r--t/data/spin/output/journal/reviews.rss8
-rw-r--r--t/data/spin/output/reviews/books/0-385-49362-2.html2
-rw-r--r--t/data/spin/output/software/docknot/api/app-docknot.html6
-rwxr-xr-xt/dist/basic.t92
-rwxr-xr-xt/dist/commands.t37
-rwxr-xr-xt/generate/basic.t16
-rwxr-xr-xt/generate/output.t40
-rwxr-xr-xt/generate/self.t8
-rw-r--r--t/lib/Test/DocKnot/Spin.pm91
-rwxr-xr-xt/release/basic.t20
-rwxr-xr-xt/spin/errors.t7
-rwxr-xr-xt/spin/file.t26
-rwxr-xr-xt/spin/markdown.t24
-rwxr-xr-xt/spin/sitemap.t34
-rwxr-xr-xt/spin/thread.t30
-rwxr-xr-xt/spin/tree.t104
-rwxr-xr-xt/spin/versions.t34
-rwxr-xr-xt/update/basic.t27
53 files changed, 859 insertions, 865 deletions
diff --git a/Changes b/Changes
index eebda3f..5c9722f 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,38 @@
Revision history for DocKnot
+7.00 - Not Released
+
+ - Various module APIs now take Path::Tiny objects instead of string
+ paths. Roughly, any module API that is not called directly by
+ App::DocKnot::Command now expects paths in the form of Path::Tiny
+ objects and does not always do an explicit conversion.
+
+ - Fix Unicode handling in App::DocKnot::Spin::Thread methods. Output to
+ files was handled correctly in most cases, but output to a scalar or to
+ standard output was not, nor was output to a file when the input was
+ generated by another program.
+
+ - Fix processing of old-style pointers in docknot spin.
+
+ - Fix import error when running docknot release.
+
+ - Fix .versions updating via docknot release when the package that needs
+ to be udpated is not the first line of the file.
+
+ - When spinning an input tree, process all .rss files first in a separate
+ pass. This ensurse the output files are seen when spinning the tree
+ into the output directory.
+
+ - Ensure docknot dist always regenerates the *.tar.xz file if necessary,
+ rather than reusing one left over from a previous build of the same
+ version.
+
+ - Always recreate GnuPG signatures when generating distribution tarballs
+ in docknot dist.
+
+ - When copying distribution files with docknot release, also copy the
+ modification timestamps of those files.
+
6.01 - 2022-01-15
- Add new docknot release command and corresponding App::DocKnot::Release
diff --git a/MANIFEST b/MANIFEST
index 466bdb9..d8d98af 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -30,9 +30,12 @@ share/schema/config.yaml
share/schema/docknot.yaml
share/schema/licenses.yaml
share/schema/pointer.yaml
+share/templates/changes.tmpl
share/templates/html.tmpl
+share/templates/index.tmpl
share/templates/readme-md.tmpl
share/templates/readme.tmpl
+share/templates/rss.tmpl
share/templates/thread.tmpl
t/cli/errors.t
t/cli/generate.t
diff --git a/META.json b/META.json
index 897c313..218c069 100644
--- a/META.json
+++ b/META.json
@@ -53,59 +53,59 @@
"provides" : {
"App::DocKnot" : {
"file" : "lib/App/DocKnot.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Command" : {
"file" : "lib/App/DocKnot/Command.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Config" : {
"file" : "lib/App/DocKnot/Config.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Dist" : {
"file" : "lib/App/DocKnot/Dist.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Generate" : {
"file" : "lib/App/DocKnot/Generate.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Release" : {
"file" : "lib/App/DocKnot/Release.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin" : {
"file" : "lib/App/DocKnot/Spin.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin::Pointer" : {
"file" : "lib/App/DocKnot/Spin/Pointer.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin::RSS" : {
"file" : "lib/App/DocKnot/Spin/RSS.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin::Sitemap" : {
"file" : "lib/App/DocKnot/Spin/Sitemap.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin::Thread" : {
"file" : "lib/App/DocKnot/Spin/Thread.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Spin::Versions" : {
"file" : "lib/App/DocKnot/Spin/Versions.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Update" : {
"file" : "lib/App/DocKnot/Update.pm",
- "version" : "6.01"
+ "version" : "7.00"
},
"App::DocKnot::Util" : {
"file" : "lib/App/DocKnot/Util.pm",
- "version" : "6.01"
+ "version" : "7.00"
}
},
"release_status" : "stable",
@@ -123,6 +123,6 @@
"web" : "https://github.com/rra/docknot"
}
},
- "version" : "6.01",
+ "version" : "7.00",
"x_serialization_backend" : "JSON::PP version 4.04"
}
diff --git a/META.yml b/META.yml
index 7f5a5c6..8acd006 100644
--- a/META.yml
+++ b/META.yml
@@ -17,46 +17,46 @@ name: App-DocKnot
provides:
App::DocKnot:
file: lib/App/DocKnot.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Command:
file: lib/App/DocKnot/Command.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Config:
file: lib/App/DocKnot/Config.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Dist:
file: lib/App/DocKnot/Dist.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Generate:
file: lib/App/DocKnot/Generate.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Release:
file: lib/App/DocKnot/Release.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin:
file: lib/App/DocKnot/Spin.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin::Pointer:
file: lib/App/DocKnot/Spin/Pointer.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin::RSS:
file: lib/App/DocKnot/Spin/RSS.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin::Sitemap:
file: lib/App/DocKnot/Spin/Sitemap.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin::Thread:
file: lib/App/DocKnot/Spin/Thread.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Spin::Versions:
file: lib/App/DocKnot/Spin/Versions.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Update:
file: lib/App/DocKnot/Update.pm
- version: '6.01'
+ version: '7.00'
App::DocKnot::Util:
file: lib/App/DocKnot/Util.pm
- version: '6.01'
+ version: '7.00'
requires:
Date::Parse: '0'
File::BaseDir: '0'
@@ -83,5 +83,5 @@ resources:
homepage: https://www.eyrie.org/~eagle/software/docknot
license: http://www.opensource.org/licenses/mit-license.php
repository: https://github.com/rra/docknot.git
-version: '6.01'
+version: '7.00'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/README b/README
index e0df429..b7cb45f 100644
--- a/README
+++ b/README
@@ -1,4 +1,4 @@
- DocKnot 6.01
+ DocKnot 7.00
(Static web site and documentation generator)
Maintained by Russ Allbery <rra@cpan.org>
diff --git a/docs/docknot.yaml b/docs/docknot.yaml
index 0103185..4f84fb9 100644
--- a/docs/docknot.yaml
+++ b/docs/docknot.yaml
@@ -7,7 +7,7 @@
#
# DocKnot is available from <https://www.eyrie.org/~eagle/software/docknot/>.
#
-# Copyright 2016, 2018-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2016, 2018-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -15,7 +15,7 @@ format: v1
name: DocKnot
maintainer: Russ Allbery <rra@cpan.org>
-version: '6.01'
+version: '7.00'
synopsis: Static web site and documentation generator
license:
@@ -65,6 +65,8 @@ docs:
title: App::DocKnot::Dist
- name: api/app-docknot-generate
title: App::DocKnot::Generate
+ - name: api/app-docknot-release
+ title: App::DocKnot::Release
- name: api/app-docknot-spin
title: App::DocKnot::Spin
- name: api/app-docknot-spin-pointer
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.
diff --git a/share/templates/changes.tmpl b/share/templates/changes.tmpl
new file mode 100644
index 0000000..fa610b1
--- /dev/null
+++ b/share/templates/changes.tmpl
@@ -0,0 +1,12 @@
+[% IF prefix %][% prefix %][% ELSE %]\heading[Recent Changes][indent]
+
+\h1[Recent Changes][% END %][% FOREACH month IN entries %]
+\h2[[% month.heading %]]
+[% FOREACH entry IN month.entries %]
+\desc[[% entry.date %] —
+ \link[[% entry.link %]]
+ [[% entry.title %]]][
+[% entry.description %]]
+[% END %]
+[% END %]
+\signature
diff --git a/share/templates/index.tmpl b/share/templates/index.tmpl
new file mode 100644
index 0000000..b62b7ed
--- /dev/null
+++ b/share/templates/index.tmpl
@@ -0,0 +1,8 @@
+[% prefix %][% FOREACH entry IN entries %]
+\h2[[% entry.day %]: [% entry.title %]]
+
+[% entry.text %]\class(footer)[[% entry.date %] —
+ \link[[% entry.link %]]
+ [Permanent link]]
+[% END %][% IF suffix %][% suffix %][% END %]
+\signature
diff --git a/share/templates/rss.tmpl b/share/templates/rss.tmpl
new file mode 100644
index 0000000..13a3ede
--- /dev/null
+++ b/share/templates/rss.tmpl
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title>[% title | xml %]</title>
+ <link>[% base | xml %]</link>
+ <description>[% description | xml %]</description>
+ <language>[% language | xml %]</language>
+ <pubDate>[% latest | xml %]</pubDate>
+ <lastBuildDate>[% now | xml %]</lastBuildDate>
+ <generator>DocKnot [% docknot_version | xml %]</generator>[% IF url %]
+ <atom:link href="[% url | url %]" rel="self"
+ type="application/rss+xml" />[% END %]
+
+[% FOREACH entry IN entries %] <item>
+ <title>[% entry.title | xml %]</title>
+ <link>[% entry.link | xml %]</link>
+ <description><![CDATA[
+[% entry.description %] ]]></description>
+ <pubDate>[% entry.date | xml %]</pubDate>
+ <guid[% IF !entry.guid.match('^http') %] isPermaLink="false"[% END %]>[% entry.guid | xml %]</guid>
+ </item>
+[% END %] </channel>
+</rss>
diff --git a/t/cli/generate.t b/t/cli/generate.t
index 87c8f68..f3d37f3 100755
--- a/t/cli/generate.t
+++ b/t/cli/generate.t
@@ -2,7 +2,7 @@
#
# Tests for the App::DocKnot command dispatch for generate.
#
-# Copyright 2018-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2018-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,9 +13,7 @@ use warnings;
use lib 't/lib';
use Cwd qw(getcwd);
-use File::Temp;
-use File::Spec;
-use Perl6::Slurp;
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More tests => 7;
@@ -32,48 +30,46 @@ my $docknot = App::DocKnot::Command->new();
isa_ok($docknot, 'App::DocKnot::Command');
# Create a temporary directory for test output.
-my $tempdir = File::Temp->newdir();
+my $tempdir = Path::Tiny->tempdir();
# Generate the package README file to a temporary file, read it into memory,
# and compare it to the actual README file. This duplicates part of the
# generate/self.t test, but via the command-line parser. Do this in a
# separate block so that $tempfile goes out of scope and will be cleaned up.
{
- my $tempfile = File::Temp->new(DIR => $tempdir);
- my $output_path = $tempfile->filename;
- $docknot->run('generate', 'readme', $output_path);
- my $output = slurp($output_path);
+ my $tempfile = $tempdir->tempfile();
+ $docknot->run('generate', 'readme', "$tempfile");
+ my $output = $tempfile->slurp_utf8();
is_file_contents($output, 'README', 'Generated README from argument list');
}
# Do the same thing again, but using arguments from @ARGV.
{
- my $tempfile = File::Temp->new(DIR => $tempdir);
- my $output_path = $tempfile->filename;
- local @ARGV = ('generate', 'readme-md', "$output_path");
+ my $tempfile = $tempdir->tempfile();
+ local @ARGV = ('generate', 'readme-md', "$tempfile");
$docknot->run();
- my $output = slurp($output_path);
+ my $output = $tempfile->slurp_utf8();
is_file_contents($output, 'README.md', 'Generated README.md from ARGV');
}
# Save the paths to various files in the source directory.
-my $readme_path = File::Spec->catfile(getcwd(), 'README');
-my $readme_md_path = File::Spec->catfile(getcwd(), 'README.md');
-my $metadata_path = File::Spec->catfile(getcwd(), 'docs', 'docknot.yaml');
+my $readme_path = path('README')->realpath();
+my $readme_md_path = path('README.md')->realpath();
+my $metadata_path = path('docs', 'docknot.yaml')->realpath();
# Generate all of the files using generate-all in a new temporary directory.
-my $tmpdir = File::Temp->newdir();
-chdir($tmpdir);
-$docknot->run('generate-all', '-m', $metadata_path);
-my $output = slurp('README');
+my $cwd = getcwd();
+chdir($tempdir);
+$docknot->run('generate-all', '-m', "$metadata_path");
+my $output = path('README')->slurp_utf8();
is_file_contents($output, $readme_path, 'README from generate_all');
-$output = slurp('README.md');
+$output = path('README.md')->slurp_utf8();
is_file_contents($output, $readme_md_path, 'README.md from generate_all');
# Ensure that generate works with a default argument.
-$docknot->run('generate', '-m', $metadata_path, 'readme');
-$output = slurp('README');
+$docknot->run('generate', '-m', "$metadata_path", 'readme');
+$output = path('README')->slurp_utf8();
is_file_contents($output, $readme_path, 'README from generate default args');
# Allow cleanup to delete our temporary directory.
-chdir(File::Spec->rootdir());
+chdir($cwd);
diff --git a/t/cli/spin.t b/t/cli/spin.t
index 2522eda..c1ba392 100755
--- a/t/cli/spin.t
+++ b/t/cli/spin.t
@@ -2,7 +2,7 @@
#
# Tests for the App::DocKnot command dispatch for spin and spin-file.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,12 +13,9 @@ use warnings;
use lib 't/lib';
use Capture::Tiny qw(capture capture_stdout);
-use Cwd qw(getcwd realpath);
use File::Copy::Recursive qw(dircopy);
-use File::Spec ();
-use File::Temp ();
+use Path::Tiny qw(path);
use POSIX qw(LC_ALL setlocale);
-use Test::RRA qw(is_file_contents);
use Test::DocKnot::Spin qw(is_spin_output is_spin_output_tree);
use Test::More;
@@ -39,64 +36,50 @@ my $docknot = App::DocKnot::Command->new();
isa_ok($docknot, 'App::DocKnot::Command');
# Create a temporary directory for test output.
-my $tempdir = File::Temp->newdir();
+my $tempdir = Path::Tiny->tempdir();
# Spin a single file.
-my $datadir = File::Spec->catfile('t', 'data', 'spin');
-my $input = File::Spec->catfile($datadir, 'input', 'index.th');
-my $expected = File::Spec->catfile($datadir, 'output', 'index.html');
-my $output = File::Spec->catfile($tempdir->dirname, 'index.html');
-$docknot->run('spin-thread', '-s', '/~eagle/styles', $input, $output);
+my $datadir = path('t', 'data', 'spin');
+my $input = $datadir->child('input', 'index.th');
+my $expected = $datadir->child('output', 'index.html');
+my $output = $tempdir->child('index.html');
+$docknot->run('spin-thread', '-s', '/~eagle/styles', "$input", "$output");
is_spin_output($output, $expected, 'spin-thread (output specified)');
# Spin a single file to standard output.
my $stdout = capture_stdout {
- $docknot->run('spin-thread', '-s', '/~eagle/styles', $input);
+ $docknot->run('spin-thread', '-s', '/~eagle/styles', "$input");
};
-open(my $output_fh, '>', $output);
-print {$output_fh} $stdout or BAIL_OUT("Cannot write to $output: $!");
-close($output_fh);
+$output->spew($stdout);
is_spin_output($output, $expected, 'spin-thread (standard output)');
# Copy the input tree to a new temporary directory since .rss files generate
# additional thread files. Replace the .spin pointer since it points to a
# relative path in the source tree.
-my $indir = File::Temp->newdir();
-$input = File::Spec->catfile($datadir, 'input');
-dircopy($input, $indir->dirname)
+my $indir = Path::Tiny->tempdir();
+$input = $datadir->child('input');
+dircopy($input, $indir)
or die "Cannot copy $input to $indir: $!\n";
-my $pod_source = File::Spec->catfile(getcwd(), 'lib', 'App', 'DocKnot.pm');
-my $pointer_path = File::Spec->catfile(
- $indir->dirname, 'software', 'docknot', 'api',
- 'app-docknot.spin',
+my $pod_source = path('lib', 'App', 'DocKnot.pm')->realpath();
+my $pointer_path = $indir->child(
+ 'software', 'docknot', 'api', 'app-docknot.spin',
);
-chmod(0644, $pointer_path);
-open(my $fh, '>', $pointer_path);
-print_fh($fh, $pointer_path, "format: pod\n");
-print_fh($fh, $pointer_path, "path: $pod_source\n");
-close($fh);
+$pointer_path->chmod(0644);
+$pointer_path->spew_utf8("format: pod\n", "path: $pod_source\n");
-# 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');
+# Spin a tree of files.
+$expected = $datadir->child('output');
capture_stdout {
- $docknot->run(
- 'spin', '-s', '/~eagle/styles', $indir->dirname,
- $tempdir->dirname,
- );
+ $docknot->run('spin', '-s', '/~eagle/styles', "$indir", "$tempdir");
};
-chdir($cwd);
-my $count = is_spin_output_tree($tempdir->dirname, $expected, 'spin');
+my $count = is_spin_output_tree($tempdir, $expected, 'spin');
# Spin a file with warnings. The specific warnings are checked in
# t/spin/errors.t; here, we just check the rewrite of the warning.
-my $errors = realpath(File::Spec->catfile($datadir, 'errors', 'errors.th'));
+my $errors = $datadir->child('errors', 'errors.th')->realpath();
my $stderr;
($stdout, $stderr) = capture {
- $docknot->run('spin-thread', $errors);
+ $docknot->run('spin-thread', "$errors");
};
like(
$stderr, qr{ \A \Q$0\E [ ] spin-thread : \Q$errors\E : 1 : }xms,
diff --git a/t/config/basic.t b/t/config/basic.t
index 87cd0c5..9c94325 100755
--- a/t/config/basic.t
+++ b/t/config/basic.t
@@ -2,7 +2,7 @@
#
# Tests for the App::DocKnot::Config module API.
#
-# Copyright 2019-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2019-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -11,7 +11,7 @@ use autodie;
use warnings;
use File::ShareDir qw(module_file);
-use File::Spec;
+use Path::Tiny qw(path);
use YAML::XS ();
use Test::More tests => 5;
@@ -24,11 +24,10 @@ local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
BEGIN { use_ok('App::DocKnot::Config') }
# Root of the test data.
-my $dataroot = File::Spec->catfile('t', 'data', 'generate');
+my $dataroot = path('t', 'data', 'generate');
# Load a test configuration and check a few inobvious pieces of it.
-my $metadata_path
- = File::Spec->catfile($dataroot, 'ansicolor', 'docknot.yaml');
+my $metadata_path = $dataroot->child('ansicolor', 'docknot.yaml');
my $config = App::DocKnot::Config->new({ metadata => $metadata_path });
isa_ok($config, 'App::DocKnot::Config');
my $data_ref = $config->config();
diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread
index d1d89a0..5f49a3e 100644
--- a/t/data/generate/docknot/output/thread
+++ b/t/data/generate/docknot/output/thread
@@ -182,6 +182,7 @@ development source].
\doc[api/app-docknot-config.html][App::DocKnot::Config]
\doc[api/app-docknot-dist.html][App::DocKnot::Dist]
\doc[api/app-docknot-generate.html][App::DocKnot::Generate]
+ \doc[api/app-docknot-release.html][App::DocKnot::Release]
\doc[api/app-docknot-spin.html][App::DocKnot::Spin]
\doc[api/app-docknot-spin-pointer.html][App::DocKnot::Spin::Pointer]
\doc[api/app-docknot-spin-rss.html][App::DocKnot::Spin::RSS]
diff --git a/t/data/regenerate-data b/t/data/regenerate-data
index 87ee82d..f796f8f 100755
--- a/t/data/regenerate-data
+++ b/t/data/regenerate-data
@@ -7,56 +7,51 @@
# output data using the local instance of App::DocKnot. It should be run from
# the root of the DocKnot distribution directory.
#
+# Copyright 2018-2022 Russ Allbery <rra@cpan.org>
+#
# SPDX-License-Identifier: MIT
use 5.018;
use autodie;
use warnings;
-use File::Spec;
-
use lib 'blib/lib';
use App::DocKnot;
use App::DocKnot::Generate;
use App::DocKnot::Spin;
+use Path::Tiny qw(path);
use Pod::Thread;
# For each subdirectory of t/data/generate, regenerate each file in the output
# subdirectory using the metadata subdirectory and the template matching the
# output name. Special-case the docknot subdirectory, which uses DocKnot's
# own metadata.
-my $data = File::Spec->catdir('t', 'data', 'generate');
-opendir(my $datadir, $data);
-my @packages = grep { -d File::Spec->catdir($data, $_) }
- File::Spec->no_upwards(readdir($datadir));
-closedir($datadir);
+my $data = path('t', 'data', 'generate');
+my @packages = map { $_->basename() } grep { $_->is_dir() } $data->children();
for my $package (@packages) {
my $metadata;
if ($package eq 'docknot') {
- $metadata = File::Spec->catdir('docs', 'docknot.yaml');
+ $metadata = path('docs', 'docknot.yaml');
} else {
- $metadata = File::Spec->catdir($data, $package, 'docknot.yaml');
+ $metadata = $data->child($package, 'docknot.yaml');
}
- my $output = File::Spec->catdir($data, $package, 'output');
- opendir(my $outputdir, $output);
- for my $template (File::Spec->no_upwards(readdir($outputdir))) {
+ my $output = $data->child($package, 'output');
+ for my $template (map { $_->basename() } $output->children()) {
my $docknot = App::DocKnot::Generate->new({ metadata => $metadata });
- my $outpath = File::Spec->catdir($output, $template);
- $docknot->generate_output($template, $outpath);
+ $docknot->generate_output($template, $output->child($template));
}
- closedir($outputdir);
}
# The test of spinning a tree of files uses a reference to App::DocKnot's own
# POD documentation. Regenerate the expected output in case the POD has
# changed.
-my $source = File::Spec->catdir('lib', 'App', 'DocKnot.pm');
+my $source = path('lib', 'App', 'DocKnot.pm');
my $podthread = Pod::Thread->new(navbar => 1);
my $spin = App::DocKnot::Spin::Thread->new();
my $thread;
$podthread->output_string(\$thread);
-$podthread->parse_file($source);
+$podthread->parse_file("$source");
my $html = $spin->spin_thread($thread);
# Add the additional metadata that should be added by spin.
@@ -91,10 +86,8 @@ $html =~ s{ (<body> \n) }{$1$navbar}xms;
$html =~ s{ (</body>) }{$navbar\n$address$1}xms;
# Replace the expected data file.
-my $output = File::Spec->catdir(
- 't', 'data', 'spin', 'output', 'software', 'docknot',
- 'api', 'app-docknot.html',
+my $output = path(
+ 't', 'data', 'spin', 'output', 'software', 'docknot', 'api',
+ 'app-docknot.html',
);
-open(my $fh, '>', $output);
-print {$fh} $html or die "Cannot write to $output: $!\n";
-close($fh);
+$output->spew_utf8($html);
diff --git a/t/data/spin/input/.rss b/t/data/spin/input/.rss
index c304c7a..bf75d9c 100644
--- a/t/data/spin/input/.rss
+++ b/t/data/spin/input/.rss
@@ -37,6 +37,12 @@ Thread-Prefix:
\link[changes/2011.html][2011], \link[changes/2010.html][2010],
\link[changes/2009.html][2009], and \link[changes/2008.html][2008].
+Date: 2021-08-30 22:30
+Title: Test UTF-8 entry
+Link: /
+Description:
+ Test entry — with UTF-8 dash.
+
Date: 2021-08-30 21:25
Title: kstart 4.3
Link: software/kstart/
diff --git a/t/data/spin/input/reviews/books/0-385-49362-2.th b/t/data/spin/input/reviews/books/0-385-49362-2.th
index 5df2e86..6fcc675 100644
--- a/t/data/spin/input/reviews/books/0-385-49362-2.th
+++ b/t/data/spin/input/reviews/books/0-385-49362-2.th
@@ -23,7 +23,7 @@ Fermat's Last Theorem is the infamous proposal that:
has no solutions for integer \italic[x, y, z, n] and \italic[n > 2]. It's
infamous for being very simple to state and understand, a variation on the
equation produced by the Pythagorean Theorem, but incredibly difficult to
-prove. It's also infamous for Pierre de Fermat's maddening marginal note:
+prove. It's also infamous for Pierre de Fermat's maddening marginal note —
"I have discovered a truly marvelous demonstration of this proposition
which this margin is too narrow to contain." 350 years after Fermat wrote
this, the theorem was still unproven in the general case, although the
diff --git a/t/data/spin/output/changes.html b/t/data/spin/output/changes.html
index 5e90dfe..acedc22 100644
--- a/t/data/spin/output/changes.html
+++ b/t/data/spin/output/changes.html
@@ -52,7 +52,13 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>August 2021</h2>
<dl>
-<dt>2021-08-30 &mdash;
+<dt>2021-08-30 —
+ <a href="https://www.eyrie.org/~eagle/">Test UTF-8 entry</a></dt>
+<dd><p>
+ Test entry — with UTF-8 dash.
+</p></dd>
+
+<dt>2021-08-30 —
<a href="https://www.eyrie.org/~eagle/software/kstart/">kstart 4.3</a></dt>
<dd><p>
Add support for kafs by continuing with -t behavior if kafs is present.
@@ -63,37 +69,37 @@ Also see changes from <a href="changes/2020.html">2020</a>,
updates.
</p></dd>
-<dt>2021-08-18 &mdash;
+<dt>2021-08-18 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-30112-2.html">Review: The Past is Red</a></dt>
<dd><p>
Review of The Past is Red by Catherynne M. Valente.
</p></dd>
-<dt>2021-08-16 &mdash;
+<dt>2021-08-16 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-5344-3769-X.html">Review: Black Sun</a></dt>
<dd><p>
Review of Black Sun by Rebecca Roanhorse.
</p></dd>
-<dt>2021-08-15 &mdash;
+<dt>2021-08-15 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-06-293605-0.html">Review: The Galaxy, and the Ground Within</a></dt>
<dd><p>
Review of The Galaxy, and the Ground Within by Becky Chambers.
</p></dd>
-<dt>2021-08-08 &mdash;
+<dt>2021-08-08 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044210-6.html">Review: The Last Battle</a></dt>
<dd><p>
Review of The Last Battle by C.S. Lewis.
</p></dd>
-<dt>2021-08-07 &mdash;
+<dt>2021-08-07 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-08-01 &mdash;
+<dt>2021-08-01 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-63557-564-8.html">Review: Piranesi</a></dt>
<dd><p>
Review of Piranesi by Susanna Clarke.
@@ -103,25 +109,25 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>July 2021</h2>
<dl>
-<dt>2021-07-31 &mdash;
+<dt>2021-07-31 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-76538-2.html">Review: Fugitive Telemetry</a></dt>
<dd><p>
Review of Fugitive Telemetry by Martha Wells.
</p></dd>
-<dt>2021-07-24 &mdash;
+<dt>2021-07-24 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add finalists for the World Fantasy Award</a></dt>
<dd><p>
Add the finalists for the 2021 World Fantasy Award for best novel.
</p></dd>
-<dt>2021-07-24 &mdash;
+<dt>2021-07-24 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-07-03 &mdash;
+<dt>2021-07-03 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add Clarke Award nominees and Locus Award Winners</a></dt>
<dd><p>
Add the nominees for the 2021 Arthur C. Clarke Award, and the winners of
@@ -134,14 +140,14 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>June 2021</h2>
<dl>
-<dt>2021-06-27 &mdash;
+<dt>2021-06-27 —
<a href="https://www.eyrie.org/~eagle/software/control-archive/">control-archive 1.9.1</a></dt>
<dd><p>
A data-only release that updates the Big Eight control signing key and
removes some obsolete information about net.*.
</p></dd>
-<dt>2021-06-27 &mdash;
+<dt>2021-06-27 —
<a href="https://www.eyrie.org/~eagle/big-8/">Added web page for Big Eight control information</a></dt>
<dd><p>
New web page summarizing control information for the Big Eight
@@ -149,32 +155,32 @@ Also see changes from <a href="changes/2020.html">2020</a>,
Moose key.
</p></dd>
-<dt>2021-06-20 &mdash;
+<dt>2021-06-20 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-345-36331-0.html">Review: Demon Lord of Karanda</a></dt>
<dd><p>
Review of Demon Lord of Karanda by David Eddings.
</p></dd>
-<dt>2021-06-19 &mdash;
+<dt>2021-06-19 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044230-0.html">Review: The Magician's Nephew</a></dt>
<dd><p>
Review of The Magician's Nephew by C.S. Lewis.
</p></dd>
-<dt>2021-06-06 &mdash;
+<dt>2021-06-06 —
<a href="https://www.eyrie.org/~eagle/reviews/books/stoneskin.html">Review: Stoneskin</a></dt>
<dd><p>
Review of Stoneskin by K.B. Spangler.
</p></dd>
-<dt>2021-06-05 &mdash;
+<dt>2021-06-05 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add winner of 2021 Nebula Award for best novel</a></dt>
<dd><p>
Add the winner of the 2021 Nebula Award for best novel (Network Effect by
Martha Wells).
</p></dd>
-<dt>2021-06-05 &mdash;
+<dt>2021-06-05 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
@@ -184,37 +190,37 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>May 2021</h2>
<dl>
-<dt>2021-05-31 &mdash;
+<dt>2021-05-31 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044200-9.html">Review: The Horse and His Boy</a></dt>
<dd><p>
Review of The Horse and His Boy by C.S. Lewis.
</p></dd>
-<dt>2021-05-30 &mdash;
+<dt>2021-05-30 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-23648-7.html">Review: The Relentless Moon</a></dt>
<dd><p>
Review of The Relentless Moon by Mary Robinette Kowal.
</p></dd>
-<dt>2021-05-29 &mdash;
+<dt>2021-05-29 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044250-5.html">Review: The Silver Chair</a></dt>
<dd><p>
Review of The Silver Chair by C.S. Lewis.
</p></dd>
-<dt>2021-05-29 &mdash;
+<dt>2021-05-29 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-05-15 &mdash;
+<dt>2021-05-15 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-18648-X.html">Review: A Desolation Called Peace</a></dt>
<dd><p>
Review of A Desolation Called Peace by Arkady Martine.
</p></dd>
-<dt>2021-05-02 &mdash;
+<dt>2021-05-02 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044260-2.html">Review: The Voyage of the Dawn Treader</a></dt>
<dd><p>
Review of The Voyage of the Dawn Treader by C.S. Lewis.
@@ -224,45 +230,45 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>April 2021</h2>
<dl>
-<dt>2021-04-27 &mdash;
+<dt>2021-04-27 —
<a href="https://www.eyrie.org/~eagle/reviews/books/beyond-shame.html">Review: Beyond Shame</a></dt>
<dd><p>
Review of Beyond Shame by Kit Rocha.
</p></dd>
-<dt>2021-04-24 &mdash;
+<dt>2021-04-24 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-4920-5172-1.html">Review: Learning React</a></dt>
<dd><p>
Review of Learning React by Alex Banks &amp; Eve Porcello.
</p></dd>
-<dt>2021-04-24 &mdash;
+<dt>2021-04-24 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add nominees for the 2021 Hugo Award</a></dt>
<dd><p>
Add the nominees for the 2021 Hugo Award for best novel.
</p></dd>
-<dt>2021-04-10 &mdash;
+<dt>2021-04-10 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-04-05 &mdash;
+<dt>2021-04-05 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add 2021 winner of the BSFA award</a></dt>
<dd><p>
Add 2021 winner of the BSFA award for best novel (The City We Became, by
N.K. Jemisin).
</p></dd>
-<dt>2021-04-05 &mdash;
+<dt>2021-04-05 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add 2021 winner of the Philip K. Dick Award</a></dt>
<dd><p>
Add 2021 winner of the Philip K. Dick Award for best novel (The Road out
of Winter, by Alison Stine).
</p></dd>
-<dt>2021-04-03 &mdash;
+<dt>2021-04-03 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044240-8.html">Review: Prince Caspian</a></dt>
<dd><p>
Review of Prince Caspian by C.S. Lewis.
@@ -272,33 +278,33 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>March 2021</h2>
<dl>
-<dt>2021-03-30 &mdash;
+<dt>2021-03-30 —
<a href="https://www.eyrie.org/~eagle/reviews/books/paladins-strength.html">Review: Paladin's Strength</a></dt>
<dd><p>
Review of Paladin's Strength by T. Kingfisher.
</p></dd>
-<dt>2021-03-29 &mdash;
+<dt>2021-03-29 —
<a href="https://www.eyrie.org/~eagle/reviews/books/paladins-grace.html">Add series information to Paladin's Grace</a></dt>
<dd><p>
Paladin's Grace by T. Kingfisher is the first of a series. Add the
series information and a mention of the sequel.
</p></dd>
-<dt>2021-03-28 &mdash;
+<dt>2021-03-28 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-4919-5202-4.html">Review: JavaScript: The Definitive Guide</a></dt>
<dd><p>
Review of JavaScript: The Definitive Guide by David Flanagan.
</p></dd>
-<dt>2021-03-28 &mdash;
+<dt>2021-03-28 —
<a href="https://www.eyrie.org/~eagle/software/inn/">Add sample INN init script and systemd unit file</a></dt>
<dd><p>
In the documentation pages for INN CURRENT, 2.6, and 2.5, add the sample
init script and systemd unit.
</p></dd>
-<dt>2021-03-28 &mdash;
+<dt>2021-03-28 —
<a href="https://www.eyrie.org/~eagle/software/pod-thread/">Pod::Thread 2.00</a></dt>
<dd><p>
Handle the navbar and table of contents internally in the module rather
@@ -307,32 +313,32 @@ Also see changes from <a href="changes/2020.html">2020</a>,
contain an underscore.
</p></dd>
-<dt>2021-03-21 &mdash;
+<dt>2021-03-21 —
<a href="https://www.eyrie.org/~eagle/software/web/">cvs2xhtml 1.15</a></dt>
<dd><p>
Convert to Python 3. Update my email address.
</p></dd>
-<dt>2021-03-21 &mdash;
+<dt>2021-03-21 —
<a href="https://www.eyrie.org/~eagle/software/web/">cl2xhtml 1.12</a></dt>
<dd><p>
Convert to Python 3. Update my email address.
</p></dd>
-<dt>2021-03-21 &mdash;
+<dt>2021-03-21 —
<a href="https://www.eyrie.org/~eagle/software/web/">faq2html 1.36</a></dt>
<dd><p>
Support formatting of dense bullet lists with line continuations but no
blank lines between bullets. Update my email address.
</p></dd>
-<dt>2021-03-21 &mdash;
+<dt>2021-03-21 —
<a href="https://www.eyrie.org/~eagle/software/debian.html">Remove DocKnot from my personal Debian repository list</a></dt>
<dd><p>
DocKnot has now been uploaded to Debian proper.
</p></dd>
-<dt>2021-03-21 &mdash;
+<dt>2021-03-21 —
<a href="https://www.eyrie.org/~eagle/software/pod-thread/">Add separate web pages for Pod::Thread</a></dt>
<dd><p>
As the first step in cleaning up my static site generator pages, and in
@@ -340,14 +346,14 @@ Also see changes from <a href="changes/2020.html">2020</a>,
its own separate web pages.
</p></dd>
-<dt>2021-03-20 &mdash;
+<dt>2021-03-20 —
<a href="https://www.eyrie.org/~eagle/software/pam-krb5/">pam-krb5 4.10</a></dt>
<dd><p>
Fix use-after-free if krb5_cc_get_principal fails on the newly-created
ticket cache.
</p></dd>
-<dt>2021-03-20 &mdash;
+<dt>2021-03-20 —
<a href="https://www.eyrie.org/~eagle/software/rra-c-util/">rra-c-util 9.0</a></dt>
<dd><p>
Rename SQLite Autoconf macros and their outputs from SQLITE to SQLITE3.
@@ -361,7 +367,7 @@ Also see changes from <a href="changes/2020.html">2020</a>,
perlcritic with YAML::XS.
</p></dd>
-<dt>2021-03-20 &mdash;
+<dt>2021-03-20 —
<a href="https://www.eyrie.org/~eagle/personal/contact.html">Clean GnuPG key</a></dt>
<dd><p>
Refresh the expiration on my signing key and re-export my key with the
@@ -369,27 +375,27 @@ Also see changes from <a href="changes/2020.html">2020</a>,
signatures.
</p></dd>
-<dt>2021-03-20 &mdash;
+<dt>2021-03-20 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add nominees for 2021 Nebula Award for best novel</a></dt>
<dd><p>
Add the nominees for the 2021 Nebula Award for best novel.
</p></dd>
-<dt>2021-03-14 &mdash;
+<dt>2021-03-14 —
<a href="https://www.eyrie.org/~eagle/journal/2021-03/001.html">New experimental Big Eight control message signing key</a></dt>
<dd><p>
Published a new experimental signing key for Big Eight control messages
that was generated with a modern GnuPG.
</p></dd>
-<dt>2021-03-13 &mdash;
+<dt>2021-03-13 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Add 2020 British Fantasy Award winner</a></dt>
<dd><p>
Add winner of the 2020 British Fantasy Robert Holdstock Award for best
fantasy novel (The Bone Ships, by RJ Barker).
</p></dd>
-<dt>2021-03-01 &mdash;
+<dt>2021-03-01 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-02-044220-3.html">Review: The Lion, the Witch and the Wardrobe</a></dt>
<dd><p>
Review of The Lion, the Witch and the Wardrobe by C.S. Lewis.
@@ -399,13 +405,13 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>February 2021</h2>
<dl>
-<dt>2021-02-28 &mdash;
+<dt>2021-02-28 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-21546-3.html">Review: Architects of Memory</a></dt>
<dd><p>
Review of Architects of Memory by Karen Osborne.
</p></dd>
-<dt>2021-02-27 &mdash;
+<dt>2021-02-27 —
<a href="https://www.eyrie.org/~eagle/software/docknot/">DocKnot 4.01</a></dt>
<dd><p>
Add support for a global user configuration file. Allow the distribution
@@ -415,50 +421,50 @@ Also see changes from <a href="changes/2020.html">2020</a>,
4.00 release.
</p></dd>
-<dt>2021-02-21 &mdash;
+<dt>2021-02-21 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-7564-1511-X.html">Review: Finder</a></dt>
<dd><p>
Review of Finder by Suzanne Palmer.
</p></dd>
-<dt>2021-02-20 &mdash;
+<dt>2021-02-20 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-7653-9893-1.html">Review: The Fated Sky</a></dt>
<dd><p>
Review of The Fated Sky by Mary Robinette Kowal.
</p></dd>
-<dt>2021-02-17 &mdash;
+<dt>2021-02-17 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-9821-5694-5.html">Review: Solutions and Other Problems</a></dt>
<dd><p>
Review of Solutions and Other Problems by Allie Brosh.
</p></dd>
-<dt>2021-02-14 &mdash;
+<dt>2021-02-14 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-4516-3937-6.html">Review: Spheres of Influence</a></dt>
<dd><p>
Review of Spheres of Influence by Ryk E. Spoor.
</p></dd>
-<dt>2021-02-14 &mdash;
+<dt>2021-02-14 —
<a href="https://www.eyrie.org/~eagle/reviews/mythopoeic.html">Add winner of 2020 Mythopoeic Award</a></dt>
<dd><p>
Add the winner of the (delayed) 2020 Mythopoeic Fantasy Award for Adult
Literature (Snow White Learns Witchcraft, by Theodora Goss).
</p></dd>
-<dt>2021-02-13 &mdash;
+<dt>2021-02-13 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-02-07 &mdash;
+<dt>2021-02-07 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-7653-9212-7.html">Review: The Future of Another Timeline</a></dt>
<dd><p>
Review of The Future of Another Timeline by Annalee Newitz.
</p></dd>
-<dt>2021-02-01 &mdash;
+<dt>2021-02-01 —
<a href="https://www.eyrie.org/~eagle/changes.html">Remove changes from December 2020</a></dt>
<dd><p>
Remove the changes from December 2020 from the recent changes
@@ -469,13 +475,13 @@ Also see changes from <a href="changes/2020.html">2020</a>,
<h2>January 2021</h2>
<dl>
-<dt>2021-01-30 &mdash;
+<dt>2021-01-30 —
<a href="https://www.eyrie.org/~eagle/">Broken link cleanup</a></dt>
<dd><p>
Another periodic cleanup of broken links.
</p></dd>
-<dt>2021-01-28 &mdash;
+<dt>2021-01-28 —
<a href="https://www.eyrie.org/~eagle/software/inn/">INN 2.6.4</a></dt>
<dd><p>
Added support for systemd notifications and socket activation. Adapt the
@@ -488,44 +494,44 @@ Also see changes from <a href="changes/2020.html">2020</a>,
hardening flags.
</p></dd>
-<dt>2021-01-26 &mdash;
+<dt>2021-01-26 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-593-12849-4.html">Review: A Deadly Education</a></dt>
<dd><p>
Review of A Deadly Education by Naomi Novik.
</p></dd>
-<dt>2021-01-25 &mdash;
+<dt>2021-01-25 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-316-50985-X.html">Review: The City We Became</a></dt>
<dd><p>
Review of The City We Became by N.K. Jemisin.
</p></dd>
-<dt>2021-01-24 &mdash;
+<dt>2021-01-24 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-9821-4013-5.html">Review: Laziness Does Not Exist</a></dt>
<dd><p>
Review of Laziness Does Not Exist by Devon Price.
</p></dd>
-<dt>2021-01-18 &mdash;
+<dt>2021-01-18 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-5098-4115-6.html">Review: The Secret Barrister</a></dt>
<dd><p>
Review of The Secret Barrister by The Secret Barrister.
</p></dd>
-<dt>2021-01-04 &mdash;
+<dt>2021-01-04 —
<a href="https://www.eyrie.org/~eagle/journal/">Update some blog roll links</a></dt>
<dd><p>
Update URL to Nicola Griffith's blog, and switch Karl Schroeder's blog to
https.
</p></dd>
-<dt>2021-01-03 &mdash;
+<dt>2021-01-03 —
<a href="https://www.eyrie.org/~eagle/reviews/books/0-316-42202-9.html">Review: The Once and Future Witches</a></dt>
<dd><p>
Review of The Once and Future Witches by Alix E. Harrow.
</p></dd>
-<dt>2021-01-01 &mdash;
+<dt>2021-01-01 —
<a href="https://www.eyrie.org/~eagle/reviews/awards.html">Update award winners</a></dt>
<dd><p>
Add links to all award pages that have good award web sites. Add notes
@@ -535,14 +541,14 @@ Also see changes from <a href="changes/2020.html">2020</a>,
by Silvia Moreno-Garcia).
</p></dd>
-<dt>2021-01-01 &mdash;
+<dt>2021-01-01 —
<a href="https://www.eyrie.org/~eagle/changes/2020.html">Rotate 2020 changes</a></dt>
<dd><p>
Move all web site changes from 2020 to a separate page and remove all
entries older than December of 2020 from the current changes page.
</p></dd>
-<dt>2021-01-01 &mdash;
+<dt>2021-01-01 —
<a href="https://www.eyrie.org/~eagle/reviews/year/2020.html">2020 reading in review</a></dt>
<dd><p>
Add an overview of my 2020 reading, main book recommendations, and
diff --git a/t/data/spin/output/changes.rss b/t/data/spin/output/changes.rss
index 1c2ee33..d4246cb 100644
--- a/t/data/spin/output/changes.rss
+++ b/t/data/spin/output/changes.rss
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
- <title>Changes to Russ Allbery's Web Pages</title>
+ <title>Changes to Russ Allbery&apos;s Web Pages</title>
<link>https://www.eyrie.org/~eagle/</link>
- <description>Recent changes to Russ Allbery's web pages.</description>
+ <description>Recent changes to Russ Allbery&apos;s web pages.</description>
<language>en-us</language>
<pubDate>%DATE%</pubDate>
<lastBuildDate>%DATE%</lastBuildDate>
@@ -12,6 +12,15 @@
type="application/rss+xml" />
<item>
+ <title>Test UTF-8 entry</title>
+ <link>https://www.eyrie.org/~eagle/</link>
+ <description><![CDATA[
+ <p>Test entry — with UTF-8 dash.</p>
+ ]]></description>
+ <pubDate>%DATE%</pubDate>
+ <guid isPermaLink="false">%DATE%</guid>
+ </item>
+ <item>
<title>kstart 4.3</title>
<link>https://www.eyrie.org/~eagle/software/kstart/</link>
<description><![CDATA[
@@ -148,14 +157,5 @@
<pubDate>%DATE%</pubDate>
<guid isPermaLink="false">%DATE%</guid>
</item>
- <item>
- <title>Review: The Magician's Nephew</title>
- <link>https://www.eyrie.org/~eagle/reviews/books/0-02-044230-0.html</link>
- <description><![CDATA[
- <p>Review of The Magician's Nephew by C.S. Lewis.</p>
- ]]></description>
- <pubDate>%DATE%</pubDate>
- <guid isPermaLink="false">%DATE%</guid>
- </item>
</channel>
</rss>
diff --git a/t/data/spin/output/journal/debian.rss b/t/data/spin/output/journal/debian.rss
index 10be70b..c4f0c86 100644
--- a/t/data/spin/output/journal/debian.rss
+++ b/t/data/spin/output/journal/debian.rss
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
- <title>Eagle's Path</title>
+ <title>Eagle&apos;s Path</title>
<link>https://www.eyrie.org/~eagle/</link>
- <description>"Passion and dispassion. Choose two." -- Larry Wall</description>
+ <description>&quot;Passion and dispassion. Choose two.&quot; -- Larry Wall</description>
<language>en-us</language>
<pubDate>%DATE%</pubDate>
<lastBuildDate>%DATE%</lastBuildDate>
diff --git a/t/data/spin/output/journal/index.html b/t/data/spin/output/journal/index.html
index ba16e82..f13cda8 100644
--- a/t/data/spin/output/journal/index.html
+++ b/t/data/spin/output/journal/index.html
@@ -651,7 +651,7 @@ things that I've not read but that are on my to-read list.
Christian faith to make any sense.</li>
</ol>
-<p class="footer">2011-08-13 00:09 &mdash;
+<p class="footer">2011-08-13 00:09 —
<a href="https://www.eyrie.org/~eagle/journal/2011-08/006.html">Permanent link</a></p>
<h2>2007-01-14: Review: Fermat's Enigma</h2>
@@ -703,7 +703,7 @@ Fermat's Last Theorem is the infamous proposal that:
has no solutions for integer <i>x, y, z, n</i> and <i>n &gt; 2</i>. It's
infamous for being very simple to state and understand, a variation on the
equation produced by the Pythagorean Theorem, but incredibly difficult to
-prove. It's also infamous for Pierre de Fermat's maddening marginal note:
+prove. It's also infamous for Pierre de Fermat's maddening marginal note —
"I have discovered a truly marvelous demonstration of this proposition
which this margin is too narrow to contain." 350 years after Fermat wrote
this, the theorem was still unproven in the general case, although the
@@ -796,7 +796,7 @@ includes several interesting nuggets of mathematical history.
Rating: 6 out of 10
</p>
-<p class="footer">2007-01-14 21:30 &mdash;
+<p class="footer">2007-01-14 21:30 —
<a href="https://www.eyrie.org/~eagle/reviews/books/1-250-30112-2.html">Permanent link</a></p></div>
<address>
diff --git a/t/data/spin/output/journal/index.rss b/t/data/spin/output/journal/index.rss
index 4dee08f..148989b 100644
--- a/t/data/spin/output/journal/index.rss
+++ b/t/data/spin/output/journal/index.rss
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
- <title>Eagle's Path</title>
+ <title>Eagle&apos;s Path</title>
<link>https://www.eyrie.org/~eagle/</link>
- <description>"Passion and dispassion. Choose two." -- Larry Wall</description>
+ <description>&quot;Passion and dispassion. Choose two.&quot; -- Larry Wall</description>
<language>en-us</language>
<pubDate>%DATE%</pubDate>
<lastBuildDate>%DATE%</lastBuildDate>
@@ -378,7 +378,7 @@ things that I've not read but that are on my to-read list.
<guid>https://www.eyrie.org/~eagle/journal/2011-08/006.html</guid>
</item>
<item>
- <title>Review: Fermat's Enigma</title>
+ <title>Review: Fermat&apos;s Enigma</title>
<link>https://www.eyrie.org/~eagle/reviews/books/1-250-30112-2.html</link>
<description><![CDATA[
<p>Review: <cite>Fermat's Enigma</cite>, by Simon Singh</p>
@@ -421,7 +421,7 @@ Fermat's Last Theorem is the infamous proposal that:
has no solutions for integer <i>x, y, z, n</i> and <i>n &gt; 2</i>. It's
infamous for being very simple to state and understand, a variation on the
equation produced by the Pythagorean Theorem, but incredibly difficult to
-prove. It's also infamous for Pierre de Fermat's maddening marginal note:
+prove. It's also infamous for Pierre de Fermat's maddening marginal note —
"I have discovered a truly marvelous demonstration of this proposition
which this margin is too narrow to contain." 350 years after Fermat wrote
this, the theorem was still unproven in the general case, although the
diff --git a/t/data/spin/output/journal/reviews.rss b/t/data/spin/output/journal/reviews.rss
index 5632834..ca070d6 100644
--- a/t/data/spin/output/journal/reviews.rss
+++ b/t/data/spin/output/journal/reviews.rss
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
- <title>Eagle's Path</title>
+ <title>Eagle&apos;s Path</title>
<link>https://www.eyrie.org/~eagle/</link>
- <description>"Passion and dispassion. Choose two." -- Larry Wall</description>
+ <description>&quot;Passion and dispassion. Choose two.&quot; -- Larry Wall</description>
<language>en-us</language>
<pubDate>%DATE%</pubDate>
<lastBuildDate>%DATE%</lastBuildDate>
@@ -12,7 +12,7 @@
type="application/rss+xml" />
<item>
- <title>Review: Fermat's Enigma</title>
+ <title>Review: Fermat&apos;s Enigma</title>
<link>https://www.eyrie.org/~eagle/reviews/books/1-250-30112-2.html</link>
<description><![CDATA[
<p>Review: <cite>Fermat's Enigma</cite>, by Simon Singh</p>
@@ -55,7 +55,7 @@ Fermat's Last Theorem is the infamous proposal that:
has no solutions for integer <i>x, y, z, n</i> and <i>n &gt; 2</i>. It's
infamous for being very simple to state and understand, a variation on the
equation produced by the Pythagorean Theorem, but incredibly difficult to
-prove. It's also infamous for Pierre de Fermat's maddening marginal note:
+prove. It's also infamous for Pierre de Fermat's maddening marginal note —
"I have discovered a truly marvelous demonstration of this proposition
which this margin is too narrow to contain." 350 years after Fermat wrote
this, the theorem was still unproven in the general case, although the
diff --git a/t/data/spin/output/reviews/books/0-385-49362-2.html b/t/data/spin/output/reviews/books/0-385-49362-2.html
index 56f6900..e70d263 100644
--- a/t/data/spin/output/reviews/books/0-385-49362-2.html
+++ b/t/data/spin/output/reviews/books/0-385-49362-2.html
@@ -65,7 +65,7 @@ Fermat's Last Theorem is the infamous proposal that:
has no solutions for integer <i>x, y, z, n</i> and <i>n &gt; 2</i>. It's
infamous for being very simple to state and understand, a variation on the
equation produced by the Pythagorean Theorem, but incredibly difficult to
-prove. It's also infamous for Pierre de Fermat's maddening marginal note:
+prove. It's also infamous for Pierre de Fermat's maddening marginal note —
"I have discovered a truly marvelous demonstration of this proposition
which this margin is too narrow to contain." 350 years after Fermat wrote
this, the theorem was still unproven in the general case, although the
diff --git a/t/data/spin/output/software/docknot/api/app-docknot.html b/t/data/spin/output/software/docknot/api/app-docknot.html
index 85f242f..a64d7c3 100644
--- a/t/data/spin/output/software/docknot/api/app-docknot.html
+++ b/t/data/spin/output/software/docknot/api/app-docknot.html
@@ -44,8 +44,8 @@
<h2 id="S1"><a name="S1">REQUIREMENTS</a></h2>
<p>
-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.
</p>
<h2 id="S2"><a name="S2">DESCRIPTION</a></h2>
@@ -95,7 +95,7 @@ Russ Allbery &lt;rra@cpan.org&gt;
<h2 id="S5"><a name="S5">COPYRIGHT AND LICENSE</a></h2>
<p>
-Copyright 2013-2021 Russ Allbery &lt;rra@cpan.org&gt;
+Copyright 2013-2022 Russ Allbery &lt;rra@cpan.org&gt;
</p>
<p>
diff --git a/t/dist/basic.t b/t/dist/basic.t
index dcc6b1b..3796a81 100755
--- a/t/dist/basic.t
+++ b/t/dist/basic.t
@@ -2,7 +2,7 @@
#
# Basic tests for App::DocKnot::Dist.
#
-# Copyright 2019-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2019-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,14 +13,12 @@ use warnings;
use lib 't/lib';
use Capture::Tiny qw(capture_stdout);
-use Cwd qw(getcwd);
use File::Copy::Recursive qw(dircopy);
-use File::Spec;
-use File::Temp;
use Git::Repository;
use IPC::Run qw(run);
use IPC::System::Simple qw(capturex systemx);
use List::Util qw(first);
+use Path::Tiny qw(path);
use Test::More;
@@ -29,24 +27,24 @@ local $ENV{XDG_CONFIG_HOME} = '/nonexistent';
local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
# Find the full path to the test data.
-my $cwd = getcwd() or die "$0: cannot get working directory: $!\n";
-my $dataroot = File::Spec->catfile($cwd, 't', 'data', 'dist', 'package');
-my $gpg_path = File::Spec->catfile($cwd, 't', 'data', 'dist', 'fake-gpg');
+my $cwd = Path::Tiny->cwd();
+my $dataroot = $cwd->child('t', 'data', 'dist', 'package');
+my $gpg_path = $cwd->child('t', 'data', 'dist', 'fake-gpg');
# Set up a temporary directory.
-my $dir = File::Temp->newdir();
-my $sourcedir = File::Spec->catfile($dir, 'source');
-my $distdir = File::Spec->catfile($dir, 'dist');
+my $dir = Path::Tiny->tempdir();
+my $sourcedir = $dir->child('source');
+my $distdir = $dir->child('dist');
# Create a new repository, copy all files from the data directory, and commit
# them. We have to rename the test while we copy it to avoid having it picked
# up by the main package test suite.
dircopy($dataroot, $sourcedir)
or die "$0: cannot copy $dataroot to $sourcedir: $!\n";
-my $testpath = File::Spec->catfile($sourcedir, 't', 'api', 'empty.t');
-rename($testpath . '.in', $testpath);
-Git::Repository->run('init', { cwd => $sourcedir, quiet => 1 });
-my $repo = Git::Repository->new(work_tree => $sourcedir);
+my $testpath = $sourcedir->child('t', 'api', 'empty.t');
+$testpath->sibling('empty.t.in')->move($testpath);
+Git::Repository->run('init', { cwd => "$sourcedir", quiet => 1 });
+my $repo = Git::Repository->new(work_tree => "$sourcedir");
$repo->run(config => '--add', 'user.name', 'Test');
$repo->run(config => '--add', 'user.email', 'test@example.com');
$repo->run(add => '-A', q{.});
@@ -76,11 +74,8 @@ require_ok('App::DocKnot::Dist');
# Put some existing files in the directory that are marked read-only. These
# should be cleaned up automatically.
-mkdir($distdir);
-mkdir(File::Spec->catfile($distdir, 'Empty'));
-open(my $fh, '>', File::Spec->catfile($distdir, 'Empty', 'Build.PL'));
-close($fh);
-chmod(0000, File::Spec->catfile($distdir, 'Empty', 'Build.PL'));
+$distdir->child('Empty')->mkpath();
+$distdir->child('Empty', 'Build.PL')->touch()->chmod(0000);
# Setup finished. Now we can create a distribution tarball.
chdir($sourcedir);
@@ -88,49 +83,50 @@ my $dist = App::DocKnot::Dist->new({ distdir => $distdir, perl => $^X });
capture_stdout {
eval { $dist->make_distribution() };
};
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.gz'), 'dist exists');
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.xz'), 'xz dist exists');
-ok(!-e File::Spec->catfile($distdir, 'Empty-1.00.tar'), 'tarball missing');
-ok(!-e File::Spec->catfile($distdir, 'Empty-1.00.tar.gz.asc'), 'no signature');
-ok(!-e File::Spec->catfile($distdir, 'Empty-1.00.tar.xz.asc'), 'no signature');
+ok($distdir->child('Empty-1.00.tar.gz')->exists(), 'dist exists');
+ok($distdir->child('Empty-1.00.tar.xz')->exists(), 'xz dist exists');
+ok(!$distdir->child('Empty-1.00.tar')->exists(), 'tarball missing');
+ok(!$distdir->child('Empty-1.00.tar.gz.asc')->exists(), 'no signature');
+ok(!$distdir->child('Empty-1.00.tar.xz.asc')->exists(), 'no signature');
is($@, q{}, 'no errors');
# Switch to using a configuration file and enable signing.
-unlink(File::Spec->catfile($distdir, 'Empty-1.00.tar.gz'));
-unlink(File::Spec->catfile($distdir, 'Empty-1.00.tar.xz'));
-mkdir(File::Spec->catfile($dir, 'docknot'));
-open($fh, '>', File::Spec->catfile($dir, 'docknot', 'config.yaml'));
-print {$fh} "distdir: $distdir\npgp_key: some-pgp-key\n"
- or die "cannot write to config.yaml: $!\n";
-close($fh);
-local $ENV{XDG_CONFIG_HOME} = $dir;
+$distdir->child('Empty-1.00.tar.gz')->remove();
+$distdir->child('Empty-1.00.tar.xz')->remove();
+$dir->child('docknot')->mkpath();
+$dir->child('docknot', 'config.yaml')->spew_utf8(
+ "distdir: $distdir\n",
+ "pgp_key: some-pgp-key\n",
+);
+local $ENV{XDG_CONFIG_HOME} = "$dir";
$dist = App::DocKnot::Dist->new({ gpg => $gpg_path, perl => $^X });
+# Create a dummy signature, which should be overwritten.
+$distdir->child('Empty-1.00.tar.gz.asc')->spew_utf8("bogus signature\n");
+
# If we add an ignored file to the source tree, this should not trigger any
# errors.
-open($fh, '>', 'ignored-file');
-print {$fh} "Some data\n" or die "cannot write to some-file: $!\n";
-close($fh);
+$sourcedir->child('ignored-file')->spew_utf8("Some data\n");
capture_stdout {
eval { $dist->make_distribution() };
};
is($@, q{}, 'no errors with ignored file');
# And now there should be signatures.
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.gz'), 'dist exists');
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.xz'), 'xz dist exists');
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.gz.asc'), 'gz signature');
-ok(-e File::Spec->catfile($distdir, 'Empty-1.00.tar.xz.asc'), 'xz signature');
-open($fh, '<', File::Spec->catfile($distdir, 'Empty-1.00.tar.gz.asc'));
-is("some signature\n", <$fh>, 'fake-gpg was run');
-close($fh);
+ok($distdir->child('Empty-1.00.tar.gz')->exists(), 'dist exists');
+ok($distdir->child('Empty-1.00.tar.xz')->exists(), 'xz dist exists');
+ok($distdir->child('Empty-1.00.tar.gz.asc')->exists(), 'gz signature');
+ok($distdir->child('Empty-1.00.tar.xz.asc')->exists(), 'xz signature');
+is(
+ "some signature\n",
+ $distdir->child('Empty-1.00.tar.gz.asc')->slurp_utf8(),
+ 'fake-gpg was run',
+);
# If we add a new file to the source tree and run make_distribution() again,
# it should fail, and the output should contain an error message about an
# unknown file.
-open($fh, '>', 'some-file');
-print {$fh} "Some data\n" or die "cannot write to some-file: $!\n";
-close($fh);
+$sourcedir->child('some-file')->spew_utf8("Some data\n");
my $stdout = capture_stdout {
eval { $dist->make_distribution() };
};
@@ -138,14 +134,12 @@ is($@, "1 file missing from distribution\n", 'correct error for extra file');
like($stdout, qr{ some-file }xms, 'output mentions the right file');
# Verify that check_dist produces the same output.
-my $tarball = File::Spec->catfile($distdir, 'Empty-1.00.tar.gz');
+my $tarball = $distdir->child('Empty-1.00.tar.gz');
my @missing = $dist->check_dist($sourcedir, $tarball);
is_deeply(['some-file'], \@missing, 'check_dist matches');
# Another missing file should produce different formatting.
-open($fh, '>', 'another-file');
-print {$fh} "Some data\n" or die "cannot write to some-file: $!\n";
-close($fh);
+$sourcedir->child('another-file')->spew_utf8("Some data\n");
$stdout = capture_stdout {
eval { $dist->make_distribution() };
};
diff --git a/t/dist/commands.t b/t/dist/commands.t
index 9ca697c..a2a0597 100755
--- a/t/dist/commands.t
+++ b/t/dist/commands.t
@@ -2,7 +2,7 @@
#
# Tests for App::DocKnot::Dist command selection to generate a distribution.
#
-# Copyright 2019-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2019-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -10,7 +10,7 @@ use 5.024;
use autodie;
use warnings;
-use File::Spec;
+use Path::Tiny qw(path);
use Test::More tests => 7;
@@ -23,7 +23,7 @@ BEGIN { use_ok('App::DocKnot::Dist') }
# Use the same test cases that we use for generate, since they represent the
# same variety of build systems.
-my $dataroot = File::Spec->catfile('t', 'data', 'generate');
+my $dataroot = path('t', 'data', 'generate');
# Module::Build distribution (use App::DocKnot itself and default paths).
my $docknot = App::DocKnot::Dist->new({ distdir => q{.} });
@@ -50,10 +50,10 @@ $docknot = App::DocKnot::Dist->new({ distdir => q{.}, perl => '/a/perl' });
is_deeply(\@seen, \@expected, 'Module::Build');
# ExtUtils::MakeMaker distribution.
-my $metadata_path
- = File::Spec->catfile($dataroot, 'ansicolor', 'docknot.yaml');
-$docknot
- = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+my $metadata_path = $dataroot->child('ansicolor', 'docknot.yaml');
+$docknot = App::DocKnot::Dist->new(
+ { distdir => q{.}, metadata => "$metadata_path" },
+);
#<<<
@expected = (
['perl', 'Makefile.PL'],
@@ -65,9 +65,10 @@ $docknot
is_deeply(\@seen, \@expected, 'ExtUtils::MakeMaker');
# Autoconf distribution.
-$metadata_path = File::Spec->catfile($dataroot, 'lbcd', 'docknot.yaml');
-$docknot
- = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+$metadata_path = $dataroot->child('lbcd', 'docknot.yaml');
+$docknot = App::DocKnot::Dist->new(
+ { distdir => q{.}, metadata => "$metadata_path" },
+);
#<<<
@expected = (
['./bootstrap'],
@@ -87,10 +88,10 @@ $docknot
is_deeply(\@seen, \@expected, 'Autoconf');
# Autoconf distribution with C++ and valgrind.
-$metadata_path
- = File::Spec->catfile($dataroot, 'c-tap-harness', 'docknot.yaml');
-$docknot
- = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+$metadata_path = $dataroot->child('c-tap-harness', 'docknot.yaml');
+$docknot = App::DocKnot::Dist->new(
+ { distdir => q{.}, metadata => "$metadata_path" },
+);
#<<<
@expected = (
['./bootstrap'],
@@ -114,10 +115,10 @@ $docknot
is_deeply(\@seen, \@expected, 'Autoconf with C++');
# Makefile only distribution (make).
-$metadata_path
- = File::Spec->catfile($dataroot, 'control-archive', 'docknot.yaml');
-$docknot
- = App::DocKnot::Dist->new({ distdir => q{.}, metadata => $metadata_path });
+$metadata_path = $dataroot->child('control-archive', 'docknot.yaml');
+$docknot = App::DocKnot::Dist->new(
+ { distdir => q{.}, metadata => "$metadata_path" },
+);
@expected = (['make', 'dist']);
@seen = $docknot->commands();
is_deeply(\@seen, \@expected, 'make');
diff --git a/t/generate/basic.t b/t/generate/basic.t
index 20af8b0..ca5762b 100755
--- a/t/generate/basic.t
+++ b/t/generate/basic.t
@@ -2,7 +2,7 @@
#
# Tests for the App::DocKnot::Generate module API.
#
-# Copyright 2013, 2016-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2013, 2016-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,7 +13,7 @@ use warnings;
use lib 't/lib';
use Encode qw(encode);
-use File::Spec;
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More;
@@ -27,24 +27,22 @@ BEGIN { use_ok('App::DocKnot::Generate') }
# We have a set of test cases in the data directory. Each of them contains
# metadata and output directories.
-my $dataroot = File::Spec->catfile('t', 'data', 'generate');
-opendir(my $tests, $dataroot);
-my @tests = File::Spec->no_upwards(readdir($tests));
-closedir($tests);
-@tests = grep { -e File::Spec->catfile($dataroot, $_, 'docknot.yaml') } @tests;
+my $dataroot = path('t', 'data', 'generate');
+my @tests = grep { $_->child('docknot.yaml')->exists() } $dataroot->children();
+@tests = map { $_->basename() } @tests;
# For each of those cases, initialize an object from the metadata directory,
# generate file from known templates, and compare that with the corresponding
# output file.
for my $test (@tests) {
- my $metadata_path = File::Spec->catfile($dataroot, $test, 'docknot.yaml');
+ my $metadata_path = $dataroot->child($test, 'docknot.yaml');
my $docknot = App::DocKnot::Generate->new({ metadata => $metadata_path });
isa_ok($docknot, 'App::DocKnot::Generate', "for $test");
# Loop through the possible templates.
for my $template (qw(readme readme-md thread)) {
my $got = encode('utf-8', $docknot->generate($template));
- my $path = File::Spec->catfile($dataroot, $test, 'output', $template);
+ my $path = $dataroot->child($test, 'output', $template);
is_file_contents($got, $path, "$template for $test");
}
}
diff --git a/t/generate/output.t b/t/generate/output.t
index 2a66451..bb2c272 100755
--- a/t/generate/output.t
+++ b/t/generate/output.t
@@ -3,7 +3,7 @@
# Test the generate_output method. This doubles as a test for whether the
# package metadata is consistent with the files currently in the distribution.
#
-# Copyright 2016, 2018-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2016, 2018-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -14,9 +14,7 @@ use warnings;
use lib 't/lib';
use Cwd qw(getcwd);
-use File::Spec;
-use File::Temp;
-use Perl6::Slurp;
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More tests => 7;
@@ -29,39 +27,39 @@ local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
BEGIN { use_ok('App::DocKnot::Generate') }
# Initialize the App::DocKnot object using the default metadata path.
-my $metadata_path = File::Spec->catfile(getcwd(), 'docs', 'docknot.yaml');
+my $metadata_path = path('docs', 'docknot.yaml')->realpath();
my $docknot = App::DocKnot::Generate->new({ metadata => $metadata_path });
isa_ok($docknot, 'App::DocKnot::Generate');
# Save the paths to the real README and README.md files.
-my $readme_path = File::Spec->catfile(getcwd(), 'README');
-my $readme_md_path = File::Spec->catfile(getcwd(), 'README.md');
+my $readme_path = Path::Tiny->cwd()->child('README');
+my $readme_md_path = Path::Tiny->cwd()->child('README.md');
# Write the README output for the DocKnot package to a temporary file.
-my $tmp = File::Temp->new();
-my $tmpname = $tmp->filename;
-$docknot->generate_output('readme', $tmpname);
-my $output = slurp($tmpname);
+my $tmp = Path::Tiny->tempfile();
+$docknot->generate_output('readme', "$tmp");
+my $output = $tmp->slurp();
is_file_contents($output, 'README', 'README in package');
-$docknot->generate_output('readme-md', $tmpname);
-$output = slurp($tmpname);
+$docknot->generate_output('readme-md', "$tmp");
+$output = $tmp->slurp();
is_file_contents($output, 'README.md', 'README.md in package');
# Test default output destinations by creating a temporary directory and then
# generating the README file without an explicit output location.
-my $tmpdir = File::Temp->newdir();
+my $tmpdir = Path::Tiny->tempdir();
+my $cwd = getcwd();
chdir($tmpdir);
$docknot->generate_output('readme');
-$output = slurp('README');
-is_file_contents($output, $readme_path, 'README using default filename');
+$output = path('README')->slurp();
+is_file_contents($output, "$readme_path", 'README using default filename');
# Use generate_all to generate all the metadata with default output paths.
unlink('README');
$docknot->generate_all();
-$output = slurp('README');
-is_file_contents($output, $readme_path, 'README from generate_all');
-$output = slurp('README.md');
-is_file_contents($output, $readme_md_path, 'README.md from generate_all');
+$output = path('README')->slurp();
+is_file_contents($output, "$readme_path", 'README from generate_all');
+$output = path('README.md')->slurp();
+is_file_contents($output, "$readme_md_path", 'README.md from generate_all');
# Allow cleanup to delete our temporary directory.
-chdir(File::Spec->rootdir());
+chdir($cwd);
diff --git a/t/generate/self.t b/t/generate/self.t
index 49145ff..8e40949 100755
--- a/t/generate/self.t
+++ b/t/generate/self.t
@@ -2,7 +2,7 @@
#
# Test generated files against the files included in the package.
#
-# Copyright 2016, 2018-2019, 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2016, 2018-2019, 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -12,7 +12,7 @@ use warnings;
use lib 't/lib';
-use File::Spec;
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More tests => 5;
@@ -34,6 +34,6 @@ is_file_contents($output, 'README', 'README in package');
$output = $docknot->generate('readme-md');
is_file_contents($output, 'README.md', 'README.md in package');
$output = $docknot->generate('thread');
-my $dataroot = File::Spec->catfile('t', 'data', 'generate');
-my $expected = File::Spec->catfile($dataroot, 'docknot', 'output', 'thread');
+my $dataroot = path('t', 'data', 'generate');
+my $expected = $dataroot->child('docknot', 'output', 'thread');
is_file_contents($output, $expected, 'Thread output for package');
diff --git a/t/lib/Test/DocKnot/Spin.pm b/t/lib/Test/DocKnot/Spin.pm
index 2c520a2..8cc461f 100644
--- a/t/lib/Test/DocKnot/Spin.pm
+++ b/t/lib/Test/DocKnot/Spin.pm
@@ -6,17 +6,19 @@
# Modules and declarations
##############################################################################
-package Test::DocKnot::Spin 1.00;
+package Test::DocKnot::Spin 2.00;
use 5.024;
use autodie;
use warnings;
use Cwd qw(getcwd);
+use Encode qw(encode);
use Exporter qw(import);
use File::Compare qw(compare);
use File::Find qw(find);
-use Perl6::Slurp qw(slurp);
+use Path::Iterator::Rule ();
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More;
@@ -36,7 +38,7 @@ our @EXPORT_OK = qw(is_spin_output is_spin_output_tree);
# $message - The descriptive message of the test
sub is_spin_output {
my ($output_file, $expected, $message) = @_;
- my $results = slurp($output_file);
+ my $results = path($output_file)->slurp_utf8();
# Map dates to %DATE% and ignore the different output when the
# modification date is the same as the generation date.
@@ -60,7 +62,7 @@ sub is_spin_output {
$results =~ s{ DocKnot [ ] \d+ [.] \d+ }{DocKnot %VERSION%}xms;
# Check the results against the expected file.
- is_file_contents($results, $expected, $message);
+ is_file_contents(encode('utf-8', $results), $expected, $message);
return;
}
@@ -74,61 +76,38 @@ sub is_spin_output {
# Returns: The number of tests run.
sub is_spin_output_tree {
my ($output, $expected, $message) = @_;
- my $cwd = getcwd();
- my %seen;
- my @missing;
-
- # Function that compares each of the output files in the tree, called from
- # 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.
- my $path = File::Spec->abs2rel($File::Find::name, $output);
- $seen{$path} = 1;
+ my (%seen, @missing);
- # Find the corresponding expected file.
- my $expected_file
- = File::Spec->rel2abs(File::Spec->catfile($expected, $path), $cwd);
+ # Compare each of the output files in the tree.
+ my $rule = Path::Iterator::Rule->new()->skip_dirs('.git')->file();
+ my $iter = $rule->iter("$output", { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ my $path = path($file)->relative($output);
+ $seen{"$path"} = 1;
+ my $expected_file = $path->absolute($expected);
# Compare HTML output using is_spin_output and all other files as
# copies.
- if ($file =~ m{ [.] (?: html | rss ) \z }xms) {
+ if ($path->basename() =~ m{ [.] (?: html | rss ) \z }xms) {
is_spin_output($file, $expected_file, "$message ($path)");
} else {
is(compare($file, $expected_file), 0, "$message ($path)");
}
- return;
- };
-
- # Function that checks that every file in the expected output tree was
- # seen in the generated output tree, called from File::Find on the
- # expected directory.
- my $check_files = sub {
- my $file = $_;
- return if -d $file;
-
- # Determine the relative path and make sure it was in the %seen hash.
- my $path = File::Spec->abs2rel($File::Find::name, $expected);
- if ($seen{$path}) {
- delete $seen{$path};
+ }
+ my $count = keys(%seen);
+
+ # Check every file in the expected output tree was seen in the generated
+ # output tree.
+ $rule = Path::Iterator::Rule->new()->skip_dirs('.git')->file();
+ $iter = $rule->iter("$expected", { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ my $path = path($file)->relative($expected);
+ if ($seen{"$path"}) {
+ delete $seen{"$path"};
} else {
push(@missing, $path);
}
- return;
- };
-
- # Compare the output.
- find($check_output, $output);
- my $count = keys(%seen);
-
- # Check that there aren't any missing files.
- find($check_files, $expected);
+ }
is_deeply(\@missing, [], 'All expected files generated');
# Return the count of tests.
@@ -143,7 +122,7 @@ sub is_spin_output_tree {
__END__
=for stopwords
-Allbery Allbery sublicense MERCHANTABILITY NONINFRINGEMENT DocKnot
+Allbery Allbery sublicense MERCHANTABILITY NONINFRINGEMENT DocKnot RSS
=head1 NAME
@@ -176,16 +155,18 @@ should be explicitly imported.
=item is_spin_output(OUTPUT, EXPECTED, MESSAGE)
-Given OUTPUT, which should be the path to a file generated by
+Given OUTPUT, which should be a Path::Tiny object pointing to the output from
App::DocKnot::Spin, compare it to the expected output in the file named
-EXPECTED. MESSAGE is the message to print with the test results for easy
-identification.
+EXPECTED (also a Path::Tiny object). MESSAGE is the message to print with the
+test results for easy identification.
=item is_spin_output_tree(OUTPUT, EXPECTED, MESSAGE)
-Compare the output tree at OUTPUT with the expected output tree at EXPECTED,
-using the same comparison algorithm as is_spin_output(). MESSAGE with the
-message to print with the test results for easy identification.
+Compare the output tree at OUTPUT with the expected output tree at EXPECTED
+(both Path::Tiny objects), using the same comparison algorithm as
+is_spin_output() for HTML and RSS files and a straight content comparison for
+all other files. MESSAGE with the message to print with the test results for
+easy identification.
=back
diff --git a/t/release/basic.t b/t/release/basic.t
index 4379136..ee67ccd 100755
--- a/t/release/basic.t
+++ b/t/release/basic.t
@@ -15,7 +15,7 @@ use lib 't/lib';
use Git::Repository ();
use Path::Tiny qw(path);
-use Test::More tests => 30;
+use Test::More tests => 34;
# Isolate from the environment.
local $ENV{XDG_CONFIG_HOME} = '/nonexistent';
@@ -35,7 +35,10 @@ $dist_path->mkpath();
# Make a release when there are no existing files.
my @extensions = qw(tar.gz tar.gz.asc tar.xz tar.xz.asc);
for my $ext (@extensions) {
- $dist_path->child('Empty-1.9.' . $ext)->touch();
+ my $path = $dist_path->child('Empty-1.9.' . $ext);
+ $path->touch();
+ utime(time() - 5, time() - 5, $path)
+ or die "Cannot reset timestamps for $path: $!\n";
}
my $metadata = path('t', 'data', 'dist', 'package', 'docs', 'docknot.yaml');
my %options = (
@@ -49,7 +52,13 @@ $release->release();
# Check that the files were copied correctly and the symlinks were created.
for my $ext (@extensions) {
my $file = 'Empty-1.9.' . $ext;
- ok($archive_path->child('devel', $file)->is_file(), "Copied $file");
+ my $file_path = $archive_path->child('devel', $file);
+ ok($file_path->is_file(), "Copied $file");
+ is(
+ $dist_path->child($file)->stat()->[9],
+ $file_path->stat()->[9],
+ "Timestamp set on $file",
+ );
my $link = 'Empty.' . $ext;
is(readlink($archive_path->child('devel', $link)), $file, "Linked $link");
}
@@ -59,6 +68,7 @@ my $spin_path = $tempdir->child('spin');
$spin_path->mkpath();
my $versions_path = $spin_path->child('.versions');
$versions_path->spew_utf8(
+ "foo 1.0 2021-12-14 17:31:32 software/foo/index.th\n",
"empty 1.9 2022-01-01 16:00:00 software/empty/index.th\n",
);
Git::Repository->run('init', { cwd => "$spin_path", quiet => 1 });
@@ -105,7 +115,9 @@ for my $ext (@extensions) {
}
# Check that the version file was updated.
-my @versions = split(q{ }, $versions_path->slurp_utf8());
+my $versions_line;
+(undef, $versions_line) = $versions_path->lines_utf8();
+my @versions = split(q{ }, $versions_line);
is($versions[0], 'empty', '.versions line');
is($versions[1], '1.10', '...version updated');
isnt(join(q{ }, @versions[2, 3]), '2022-01-01 16:00:00', '...date updated');
diff --git a/t/spin/errors.t b/t/spin/errors.t
index c6e1ce6..c35669b 100755
--- a/t/spin/errors.t
+++ b/t/spin/errors.t
@@ -2,7 +2,7 @@
#
# Test errors generated by App::DocKnot::Spin::Thread.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -11,8 +11,7 @@ use autodie;
use warnings;
use Capture::Tiny qw(capture);
-use File::Spec;
-use File::Temp;
+use Path::Tiny qw(path);
use Test::More tests => 2;
@@ -35,7 +34,7 @@ ERRORS
require_ok('App::DocKnot::Spin::Thread');
# Spin the errors file with output captured.
-my $input = File::Spec->catfile('t', 'data', 'spin', 'errors', 'errors.th');
+my $input = path('t', 'data', 'spin', 'errors', 'errors.th');
my $spin = App::DocKnot::Spin::Thread->new();
my ($stdout, $stderr) = capture {
$spin->spin_thread_file($input);
diff --git a/t/spin/file.t b/t/spin/file.t
index 2f645e5..260b03e 100755
--- a/t/spin/file.t
+++ b/t/spin/file.t
@@ -2,7 +2,7 @@
#
# Test running spin on a single file.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,11 +13,8 @@ use warnings;
use lib 't/lib';
use Capture::Tiny qw(capture_stdout);
-use Cwd qw(getcwd);
use Fcntl qw(SEEK_SET);
-use File::Spec;
-use File::Temp;
-use Perl6::Slurp qw(slurp);
+use Path::Tiny qw(path);
use Test::DocKnot::Spin qw(is_spin_output);
use Test::More tests => 3;
@@ -25,22 +22,19 @@ use Test::More tests => 3;
require_ok('App::DocKnot::Spin::Thread');
# Spin a single file.
-my $tempfile = File::Temp->new();
-my $datadir = File::Spec->catfile('t', 'data', 'spin');
-my $inputdir = File::Spec->catfile($datadir, 'input');
-my $input = File::Spec->catfile($inputdir, 'index.th');
-my $expected = File::Spec->catfile($datadir, 'output', 'index.html');
+my $tempfile = Path::Tiny->tempfile();
+my $datadir = path('t', 'data', 'spin');
+my $inputdir = $datadir->child('input');
+my $input = $inputdir->child('index.th');
+my $expected = $datadir->child('output', 'index.html');
my $spin
= App::DocKnot::Spin::Thread->new({ 'style-url' => '/~eagle/styles/' });
-$spin->spin_thread_file($input, $tempfile->filename);
+$spin->spin_thread_file($input, $tempfile);
is_spin_output($tempfile, $expected, 'spin_thread_file with output path');
# The same but spin to standard output.
my $html = capture_stdout {
$spin->spin_thread_file($input);
};
-$tempfile->seek(0, SEEK_SET);
-$tempfile->truncate(0);
-print {$tempfile} $html or die "Cannot write to $tempfile: $!\n";
-$tempfile->flush();
-is_spin_output($tempfile->filename, $expected, 'spin_thread_file to stdout');
+$tempfile->spew($html);
+is_spin_output($tempfile, $expected, 'spin_thread_file to stdout');
diff --git a/t/spin/markdown.t b/t/spin/markdown.t
index bf8de61..7b36e9f 100755
--- a/t/spin/markdown.t
+++ b/t/spin/markdown.t
@@ -2,7 +2,7 @@
#
# Test Markdown conversion.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -14,10 +14,8 @@ use lib 't/lib';
use Capture::Tiny qw(capture_stdout);
use Carp qw(croak);
-use Cwd qw(getcwd);
-use File::Copy::Recursive qw(dircopy);
-use File::Temp ();
use IPC::Cmd qw(can_run);
+use Path::Tiny qw(path);
use Test::DocKnot::Spin qw(is_spin_output_tree);
use Template ();
@@ -35,21 +33,13 @@ local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
require_ok('App::DocKnot::Spin');
require_ok('App::DocKnot::Spin::Pointer');
-# Ensure Devel::Cover has loaded the HTML template before we start changing
-# the working directory with File::Find. (This is a dumb workaround, but I
-# can't find a better one; +ignore doesn't work.)
-my $pointer = App::DocKnot::Spin::Pointer->new();
-my $template = $pointer->appdata_path('templates', 'html.tmpl');
-my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error());
-$tt->process($template, {}, \my $result);
-
# Spin the tree of files and check the result.
-my $datadir = File::Spec->catfile('t', 'data', 'spin', 'markdown');
-my $input = File::Spec->catfile($datadir, 'input');
-my $output = File::Temp->newdir();
-my $expected = File::Spec->catfile($datadir, 'output');
+my $datadir = path('t', 'data', 'spin', 'markdown');
+my $input = $datadir->child('input');
+my $output = Path::Tiny->tempdir();
+my $expected = $datadir->child('output');
my $spin = App::DocKnot::Spin->new({ 'style-url' => '/~eagle/styles/' });
-my $stdout = capture_stdout { $spin->spin($input, $output->dirname) };
+my $stdout = capture_stdout { $spin->spin($input, $output) };
my $count = is_spin_output_tree($output, $expected, 'spin');
# Report the end of testing.
diff --git a/t/spin/sitemap.t b/t/spin/sitemap.t
index b51d675..d6a66d7 100755
--- a/t/spin/sitemap.t
+++ b/t/spin/sitemap.t
@@ -2,7 +2,7 @@
#
# Tests for App::DocKnot::Spin::Sitemap (.sitemap file handling).
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -12,7 +12,7 @@ use warnings;
use lib 't/lib';
-use File::Spec;
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More tests => 10;
@@ -20,25 +20,24 @@ use Test::More tests => 10;
require_ok('App::DocKnot::Spin::Sitemap');
# Parse a complex .sitemap file.
-my $datadir = File::Spec->catfile('t', 'data', 'spin', 'sitemap');
-my $path = File::Spec->catfile($datadir, 'complex');
-my $sitemap = App::DocKnot::Spin::Sitemap->new($path);
+my $datadir = path('t', 'data', 'spin', 'sitemap');
+my $sitemap = App::DocKnot::Spin::Sitemap->new($datadir->child('complex'));
isa_ok($sitemap, 'App::DocKnot::Spin::Sitemap');
# Check the generated sitemap.
my $output = join(q{}, $sitemap->sitemap());
-my $expected = File::Spec->catfile($datadir, 'complex.html');
+my $expected = $datadir->child('complex.html');
is_file_contents($output, $expected, 'sitemap output');
# Unknown page.
-my @links = $sitemap->links('/unknown');
+my @links = $sitemap->links('unknown');
is_deeply(\@links, [], 'links for unknown page');
-my @navbar = $sitemap->navbar('/unknown');
+my @navbar = $sitemap->navbar('unknown');
is_deeply(\@navbar, [], 'navbar for unknown page');
# Check links and navbar for a page near a --- boundary, which may not be
# exercised by the test of spinning a tree of files.
-@links = $sitemap->links('/faqs/soundness-inn.html');
+@links = $sitemap->links('faqs/soundness-inn.html');
my @expected = (
q{ <link rel="next" href="soundness-cnews.html"}
. qq{ title="Soundness for C News" />\n},
@@ -46,7 +45,7 @@ my @expected = (
qq{ <link rel="top" href="../" />\n},
);
is_deeply(\@links, \@expected, 'links output');
-@navbar = $sitemap->navbar('/faqs/soundness-inn.html');
+@navbar = $sitemap->navbar('faqs/soundness-inn.html');
@expected = (
qq{<table class="navbar"><tr>\n},
qq{ <td class="navleft"></td>\n},
@@ -61,7 +60,7 @@ is_deeply(\@links, \@expected, 'links output');
is_deeply(\@navbar, \@expected, 'navbar output');
# Check links for a page with long adjacent titles to test the wrapping.
-@links = $sitemap->links('/notes/cvs/basic-usage.html');
+@links = $sitemap->links('notes/cvs/basic-usage.html');
@expected = (
qq{ <link rel="previous" href="why.html"\n},
qq{ title="Why put a set of files into CVS?" />\n},
@@ -73,16 +72,11 @@ is_deeply(\@navbar, \@expected, 'navbar output');
is_deeply(\@links, \@expected, 'links output with wrapping');
# Check error handling.
-eval {
- $path = File::Spec->catfile($datadir, 'invalid');
- App::DocKnot::Spin::Sitemap->new($path);
-};
+my $path = $datadir->child('invalid');
+eval { App::DocKnot::Spin::Sitemap->new($path) };
is($@, "invalid line 3 in $path\n", 'invalid sitemap file');
-# Check error handling.
-eval {
- $path = File::Spec->catfile($datadir, 'duplicate');
- App::DocKnot::Spin::Sitemap->new($path);
-};
+$path = $datadir->child('duplicate');
+eval { App::DocKnot::Spin::Sitemap->new($path) };
is(
$@,
"duplicate entry for /faqs/comments.html in $path (line 4)\n",
diff --git a/t/spin/thread.t b/t/spin/thread.t
index c18a743..edacbe5 100755
--- a/t/spin/thread.t
+++ b/t/spin/thread.t
@@ -2,7 +2,7 @@
#
# Test running spin on a scalar containing thread source.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,9 +13,7 @@ use warnings;
use lib 't/lib';
use Cwd qw(getcwd);
-use File::Spec;
-use File::Temp;
-use Perl6::Slurp qw(slurp);
+use Path::Tiny qw(path);
use Test::DocKnot::Spin qw(is_spin_output);
use Test::More tests => 2;
@@ -23,31 +21,29 @@ use Test::More tests => 2;
require_ok('App::DocKnot::Spin::Thread');
# Test data file paths.
-my $datadir = File::Spec->catfile('t', 'data', 'spin');
-my $inputdir = File::Spec->catfile($datadir, 'input');
-my $input = File::Spec->catfile($inputdir, 'index.th');
-my $expected = File::Spec->catfile($datadir, 'output', 'index.html');
+my $datadir = path('t', 'data', 'spin');
+my $inputdir = $datadir->child('input');
+my $input = $inputdir->child('index.th');
+my $expected = $datadir->child('output', 'index.html');
# The expected output is a bit different since we won't add timestamp
# information or the filename to the comment, so we have to generate our
# expected output file.
-my $tempfile = File::Temp->new();
-my $output = slurp($expected);
+my $tempfile = Path::Tiny->tempfile();
+my $output = $expected->slurp_utf8();
$output =~ s{ from [ ] index[.]th [ ] }{}xms;
$output =~ s{ <address> .* </address> \n }{}xms;
-print {$tempfile} $output or die "Cannot write to $tempfile: $!\n";
-$tempfile->flush();
+$tempfile->spew_utf8($output);
# Spin the file using the spin_thread() API, using the right working directory
# to expand \image and the like.
my $spin
= App::DocKnot::Spin::Thread->new({ 'style-url' => '/~eagle/styles/' });
-my $thread = slurp($input);
+my $thread = $input->slurp_utf8();
my $cwd = getcwd();
chdir($inputdir);
my $html = $spin->spin_thread($thread);
chdir($cwd);
-my $outfile = File::Temp->new();
-print {$outfile} $html or die "Cannot write to $outfile: $!\n";
-$outfile->flush();
-is_spin_output($outfile->filename, $tempfile->filename, 'spin_thread');
+my $outfile = Path::Tiny->tempfile();
+$outfile->spew_utf8($html);
+is_spin_output($outfile, $tempfile, 'spin_thread');
diff --git a/t/spin/tree.t b/t/spin/tree.t
index 700f961..c57c413 100755
--- a/t/spin/tree.t
+++ b/t/spin/tree.t
@@ -2,7 +2,7 @@
#
# Test running spin on a tree of files.
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -13,11 +13,8 @@ use warnings;
use lib 't/lib';
use Capture::Tiny qw(capture_stdout);
-use Cwd qw(getcwd);
use File::Copy::Recursive qw(dircopy);
-use File::Spec ();
-use File::Temp ();
-use Perl6::Slurp qw(slurp);
+use Path::Tiny qw(path);
use POSIX qw(LC_ALL setlocale strftime);
use Test::DocKnot::Spin qw(is_spin_output_tree);
@@ -32,14 +29,14 @@ setlocale(LC_ALL, 'C');
my $EXPECTED_OUTPUT = <<'OUTPUT';
Generating thread file .../changes.th
Generating RSS file .../changes.rss
-Updating .../changes.rss
-Spinning .../changes.html
-Spinning .../index.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 .../changes.rss
+Spinning .../changes.html
+Spinning .../index.html
+Creating .../journal
Updating .../names.png
Spinning .../random.html
Creating .../reviews
@@ -74,87 +71,72 @@ require_ok('App::DocKnot::Spin');
# additional thread files. Replace the POD pointer since it points to a
# relative path in the source tree, but change its modification timestamp to
# something in the past.
-my $tmpdir = File::Temp->newdir();
-my $datadir = File::Spec->catfile('t', 'data', 'spin');
-my $input = File::Spec->catfile($datadir, 'input');
-dircopy($input, $tmpdir->dirname)
- or die "Cannot copy $input to $tmpdir: $!\n";
-my $pod_source = File::Spec->catfile(getcwd(), 'lib', 'App', 'DocKnot.pm');
-my $pointer_path = File::Spec->catfile(
- $tmpdir->dirname, 'software', 'docknot', 'api',
- 'app-docknot.spin',
+my $tmpdir = Path::Tiny->tempdir();
+my $datadir = path('t', 'data', 'spin');
+my $input = $datadir->child('input');
+dircopy($input, $tmpdir) or die "Cannot copy $input to $tmpdir: $!\n";
+my $pod_source = path('lib', 'App', 'DocKnot.pm')->realpath();
+my $pointer_path = $tmpdir->path(
+ 'software', 'docknot', 'api', 'app-docknot.spin',
);
-chmod(0644, $pointer_path);
-open(my $fh, '>', $pointer_path);
-print_fh($fh, $pointer_path, "format: pod\n");
-print_fh($fh, $pointer_path, "path: $pod_source\n");
-close($fh);
+$pointer_path->spew_utf8("format: pod\n", "path: $pod_source\n");
my $old_timestamp = time() - 10;
# Spin a tree of files.
-my $output = File::Temp->newdir();
-my $expected = File::Spec->catfile($datadir, 'output');
+my $output = Path::Tiny->tempdir();
+my $expected = $datadir->child('output');
my $spin = App::DocKnot::Spin->new({ 'style-url' => '/~eagle/styles/' });
-my $stdout = capture_stdout {
- $spin->spin($tmpdir->dirname, $output->dirname);
-};
+my $stdout = capture_stdout { $spin->spin($tmpdir, $output) };
my $count = is_spin_output_tree($output, $expected, 'spin');
is($stdout, $EXPECTED_OUTPUT, 'Expected spin output');
# Create a bogus file in the output tree.
-my $bogus = File::Spec->catfile($output->dirname, 'bogus');
-my $bogus_file = File::Spec->catfile($bogus, 'some-file');
-mkdir($bogus);
-open($fh, '>', $bogus_file);
-print {$fh} "Some stuff\n" or die "Cannot write to $bogus_file: $!\n";
-close($fh);
+my $bogus = $output->child('bogus');
+$bogus->mkpath();
+$bogus->child('some-file')->spew_utf8("Some stuff\n");
# Spinning the same tree of files again should do nothing because of the
# modification timestamps.
-$stdout = capture_stdout {
- $spin->spin($tmpdir->dirname, $output->dirname);
-};
+$stdout = capture_stdout { $spin->spin($tmpdir, $output) };
is($stdout, q{}, 'Spinning again does nothing');
# The extra file shouldn't be deleted.
-ok(-d $bogus, 'Stray file and directory not deleted');
+ok($bogus->is_dir(), 'Stray file and directory not deleted');
# Reconfigure spin to enable deletion, and run it again. The only action
# taken should be to delete the stray file.
$spin
= App::DocKnot::Spin->new({ delete => 1, 'style-url' => '/~eagle/styles/' });
-$stdout = capture_stdout {
- $spin->spin($tmpdir->dirname, $output->dirname);
-};
+$stdout = capture_stdout { $spin->spin($tmpdir, $output) };
is(
$stdout,
"Deleting .../bogus/some-file\nDeleting .../bogus\n",
'Spinning with delete option cleans up',
);
-ok(!-e $bogus, 'Stray file and directory was deleted');
+ok(!$bogus->exists(), 'Stray file and directory was deleted');
# Override the title of the POD document and request a contents section. Set
# the modification timestamp in the future to force a repsin.
-open($fh, '>>', $pointer_path);
-print_fh($fh, $pointer_path, "format: pod\n");
-print_fh($fh, $pointer_path, "path: $pod_source\n");
-print_fh($fh, $pointer_path, "options:\n contents: true\n navbar: false\n");
-print_fh($fh, $pointer_path, "title: 'New Title'\n");
-close($fh);
+$pointer_path->spew_utf8(
+ "format: pod\n",
+ "path: $pod_source\n",
+ "options:\n",
+ " contents: true\n",
+ " navbar: false\n",
+ "title: 'New Title'\n",
+);
utime(time() + 5, time() + 5, $pointer_path)
or die "Cannot reset timestamps of $pointer_path: $!\n";
-$stdout = capture_stdout {
- $spin->spin($tmpdir->dirname, $output->dirname);
-};
+$stdout = capture_stdout { $spin->spin($tmpdir, $output) };
is(
$stdout,
"Converting .../software/docknot/api/app-docknot.html\n",
'Spinning again regenerates the App::DocKnot page',
);
-my $output_path = File::Spec->catfile(
- $output->dirname, 'software', 'docknot', 'api', 'app-docknot.html',
+my $output_path = $output->child(
+ 'software', 'docknot', 'api', 'app-docknot.html',
);
-my $page = slurp($output_path);
+my $page = $output_path->slurp_utf8();
like(
$page,
qr{ <title> New [ ] Title </title> }xms,
@@ -170,17 +152,13 @@ utime(time() - 5, time() - 5, $pointer_path)
# Now, update the .versions file at the top of the input tree to change the
# timestamp to ten seconds into the future. This should force regeneration of
# only the software/docknot/index.html file.
-my $versions_path = File::Spec->catfile($tmpdir->dirname, '.versions');
-my $versions = slurp($versions_path);
+my $versions_path = $tmpdir->child('.versions');
+my $versions = $versions_path->slurp_utf8();
my $new_date = strftime('%Y-%m-%d %T', localtime(time() + 10));
$versions =~ s{ \d{4}-\d\d-\d\d [ ] [\d:]+ }{$new_date}xms;
-chmod(0644, $versions_path);
-open(my $versions_fh, '>', $versions_path);
-print {$versions_fh} $versions or die "Cannot write to $versions_path: $!\n";
-close($versions_fh);
-$stdout = capture_stdout {
- $spin->spin($tmpdir->dirname, $output->dirname);
-};
+$versions_path->chmod(0644);
+$versions_path->spew_utf8($versions);
+$stdout = capture_stdout { $spin->spin($tmpdir, $output) };
is(
$stdout,
"Spinning .../software/docknot/index.html\n",
diff --git a/t/spin/versions.t b/t/spin/versions.t
index 7f1e62b..ba508bb 100755
--- a/t/spin/versions.t
+++ b/t/spin/versions.t
@@ -2,7 +2,7 @@
#
# Tests for App::DocKnot::Spin::Versions (.versions file handling).
#
-# Copyright 2021 Russ Allbery <rra@cpan.org>
+# Copyright 2021-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -12,7 +12,7 @@ use warnings;
use lib 't/lib';
-use File::Spec;
+use Path::Tiny qw(path);
use POSIX qw(tzset);
use Test::More tests => 20;
@@ -26,7 +26,7 @@ local $ENV{TZ} = 'PST8PDT,M3.2.0,M11.1.0';
tzset();
# Parse the file.
-my $path = File::Spec->catfile('t', 'data', 'spin', 'input', '.versions');
+my $path = path('t', 'data', 'spin', 'input', '.versions');
my $versions = App::DocKnot::Spin::Versions->new($path);
isa_ok($versions, 'App::DocKnot::Spin::Versions');
@@ -44,8 +44,8 @@ is($versions->release_date('unknown'), undef, 'unknown release date');
is($versions->latest_release('index.th'), 0, 'unknown file index.th');
# Check continuation handling and a line without dependencies.
-my $inputdir = File::Spec->catfile('t', 'data', 'spin', 'versions');
-$path = File::Spec->catfile($inputdir, 'continuation');
+my $inputdir = path('t', 'data', 'spin', 'versions');
+$path = $inputdir->child('continuation');
$versions = App::DocKnot::Spin::Versions->new($path);
is($versions->version('docknot'), '4.01', 'docknot version');
is($versions->release_date('docknot'), '2021-02-27', 'docknot release date');
@@ -72,26 +72,18 @@ is(
);
# Check error handling.
-eval {
- $path = File::Spec->catfile($inputdir, 'invalid-continuation');
- App::DocKnot::Spin::Versions->new($path);
-};
+$path = $inputdir->child('invalid-continuation');
+eval { App::DocKnot::Spin::Versions->new($path) };
is(
$@, "continuation without previous entry in $path\n",
'invalid continuation',
);
-eval {
- $path = File::Spec->catfile($inputdir, 'invalid-date');
- App::DocKnot::Spin::Versions->new($path);
-};
+$path = $inputdir->child('invalid-date');
+eval { App::DocKnot::Spin::Versions->new($path) };
is($@, qq(invalid date "20-02-27" in $path\n), 'invalid date');
-eval {
- $path = File::Spec->catfile($inputdir, 'invalid-time');
- App::DocKnot::Spin::Versions->new($path);
-};
+$path = $inputdir->child('invalid-time');
+eval { App::DocKnot::Spin::Versions->new($path) };
is($@, qq(invalid time "13:08" in $path\n), 'invalid time');
-eval {
- $path = File::Spec->catfile($inputdir, 'invalid-line');
- App::DocKnot::Spin::Versions->new($path);
-};
+$path = $inputdir->child('invalid-line');
+eval { App::DocKnot::Spin::Versions->new($path) };
is($@, "invalid line 2 in $path\n", 'invalid line');
diff --git a/t/update/basic.t b/t/update/basic.t
index 5dc2be1..30049bf 100755
--- a/t/update/basic.t
+++ b/t/update/basic.t
@@ -2,7 +2,7 @@
#
# Tests for the App::DocKnot::Update module API.
#
-# Copyright 2020-2021 Russ Allbery <rra@cpan.org>
+# Copyright 2020-2022 Russ Allbery <rra@cpan.org>
#
# SPDX-License-Identifier: MIT
@@ -12,9 +12,7 @@ use warnings;
use lib 't/lib';
-use File::Temp;
-use File::Spec;
-use Perl6::Slurp qw(slurp);
+use Path::Tiny qw(path);
use Test::RRA qw(is_file_contents);
use Test::More;
@@ -28,27 +26,22 @@ BEGIN { use_ok('App::DocKnot::Update') }
# We have a set of test cases in the data directory. Each of them contains
# an old directory for the old files and a docknot.yaml file for the results.
-my $dataroot = File::Spec->catfile('t', 'data', 'update');
-opendir(my $tests, $dataroot);
-my @tests = File::Spec->no_upwards(readdir($tests));
-closedir($tests);
+my $dataroot = path('t', 'data', 'update');
+my @tests = map { $_->basename() } $dataroot->children();
# For each of those cases, initialize an object, generate the updated
# configuration, and compare it with the test output file.
-my $tempdir = File::Temp->newdir();
+my $tempdir = Path::Tiny->tempdir();
for my $test (@tests) {
- my $metadata_path = File::Spec->catfile($dataroot, $test, 'old');
- my $expected_path = File::Spec->catfile($dataroot, $test, 'docknot.yaml');
- my $output_path = File::Spec->catfile($tempdir, "$test.yaml");
+ my $metadata_path = $dataroot->child($test, 'old');
+ my $expected_path = $dataroot->child($test, 'docknot.yaml');
+ my $output_path = $tempdir->child("$test.yaml");
my $docknot = App::DocKnot::Update->new(
- {
- metadata => $metadata_path,
- output => $output_path,
- },
+ { metadata => $metadata_path, output => $output_path },
);
isa_ok($docknot, 'App::DocKnot::Update', "for $test");
$docknot->update();
- my $got = slurp($output_path);
+ my $got = $output_path->slurp();
is_file_contents($got, $expected_path, "output for $test");
}