summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2022-01-02 13:23:07 -0800
committerRuss Allbery <rra@cpan.org>2022-01-02 13:23:07 -0800
commit7c2fb0c0588ca7cb7e54695935afd965c22cab27 (patch)
tree62861892e985558fdf6867a0db77f01afab1d664
parentc00d3652d51a440d6c5378631af4021c39afa644 (diff)
Add new docknot update-spin command
Add new docknot update-spin command and corresponding update_spin method in App::DocKnot::Update to update a spin input tree to the latest expectations. Currently, all this does is convert *.rpod pointer files to *.spin pointer files.
-rw-r--r--Changes5
-rwxr-xr-xbin/docknot21
-rw-r--r--lib/App/DocKnot/Command.pm7
-rw-r--r--lib/App/DocKnot/Update.pm147
-rw-r--r--t/data/spin/update/input/module.rpod1
-rw-r--r--t/data/spin/update/input/readme.rpod2
-rw-r--r--t/data/spin/update/input/script.rpod3
-rw-r--r--t/data/spin/update/output/module.spin2
-rw-r--r--t/data/spin/update/output/readme.spin6
-rw-r--r--t/data/spin/update/output/script.spin3
-rw-r--r--t/lib/Test/DocKnot/Spin.pm6
-rwxr-xr-xt/update/spin.t65
12 files changed, 250 insertions, 18 deletions
diff --git a/Changes b/Changes
index ceb8d09..adf5cfc 100644
--- a/Changes
+++ b/Changes
@@ -2,6 +2,11 @@
6.01 - Not Released
+ - Add new docknot update-spin command and corresponding update_spin
+ method in App::DocKnot::Update to update a spin input tree to the
+ latest expectations. Currently, all this does is convert *.rpod
+ pointer files to *.spin pointer files.
+
- docknot spin now uses Path::Iterator::Rule and Path::Tiny to construct
its paths, which eliminates the need to change the working directory
while processing input files.
diff --git a/bin/docknot b/bin/docknot
index c9e3f12..3de49a9 100755
--- a/bin/docknot
+++ b/bin/docknot
@@ -47,6 +47,8 @@ B<docknot> spin-thread [B<-f>] [B<-s> I<url>] [I<source> [I<output>]]
B<docknot> update [B<-m> I<metadata>] [B<-o> I<output>]
+B<docknot> update-spin [I<path>]
+
=head1 DESCRIPTION
B<docknot> is a static web site generator with special support for managing
@@ -97,6 +99,12 @@ Like C<spin>, but convert a single file written in thread to HTML.
Update the DocKnot package configuration from an older format.
+=item update-spin
+
+Update an input tree for C<spin> to the latest expectations. This will, for
+example, convert old-style F<*.rpod> pointer files to new-style F<*.spin>
+pointer files.
+
=back
=head1 OPTIONS
@@ -285,6 +293,17 @@ recommended metadata path for a project).
=back
+=head2 update-spin
+
+=over 4
+
+=item I<path>
+
+The path to the spin input tree to update. If not given, defaults to the
+current directory.
+
+=back
+
=head1 DIAGNOSTICS
If B<docknot> fails with errors, see the underlying module for that subcommand
@@ -320,7 +339,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2016, 2018-2021 Russ Allbery <rra@cpan.org>
+Copyright 2016, 2018-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 fdbc184..48c7240 100644
--- a/lib/App/DocKnot/Command.pm
+++ b/lib/App/DocKnot/Command.pm
@@ -104,6 +104,11 @@ our %COMMANDS = (
options => ['metadata|m=s', 'output|o=s'],
maximum => 0,
},
+ 'update-spin' => {
+ method => 'update_spin',
+ module => 'App::DocKnot::Update',
+ maximum => 1,
+ },
);
##############################################################################
@@ -338,7 +343,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2018-2021 Russ Allbery <rra@cpan.org>
+Copyright 2018-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/Update.pm b/lib/App/DocKnot/Update.pm
index 5c6a999..e14a726 100644
--- a/lib/App/DocKnot/Update.pm
+++ b/lib/App/DocKnot/Update.pm
@@ -20,6 +20,8 @@ use Carp qw(croak);
use File::Spec;
use JSON::MaybeXS qw(JSON);
use Kwalify qw(validate);
+use Path::Iterator::Rule;
+use Path::Tiny qw(path);
use Perl6::Slurp;
use YAML::XS ();
@@ -158,6 +160,90 @@ sub _config_from_json {
}
##############################################################################
+# Spin helper methods
+##############################################################################
+
+# Given an old-format *.rpod pointer file, read the master file name and any
+# options. Return them in the structure used for *.spin pointer files.
+#
+# $path - Path::Tiny for the file to read
+#
+# Returns: Hash in the format of a *.spin pointer file
+# Throws: Text exception if no master file is present in the pointer
+# autodie exception if the pointer file could not be read
+sub _read_rpod_pointer {
+ my ($self, $path) = @_;
+
+ # Read the pointer file.
+ my ($master, $options, $style) = $path->lines_utf8();
+ if (!$master) {
+ die "no master file specified in $path\n";
+ }
+ chomp($master);
+
+ # Put the results into the correct format.
+ my %results = (format => 'pod', path => $master);
+ if (defined($style)) {
+ chomp($style);
+ $results{style} = $style;
+ }
+ if (defined($options)) {
+ if ($options =~ m{ -c ( \s | \z ) }xms) {
+ $results{options} = {
+ contents => JSON::MaybeXS::true,
+ navbar => JSON::MaybeXS::false,
+ };
+ }
+ if ($options =~ m{ -t \s+ (?: '(.*)' | ( [^\'] \S+ ) ) }xms) {
+ $results{title} = $1 || $2;
+ }
+ }
+
+ # Return the parsed file.
+ return \%results;
+}
+
+# Given its representation as a hash, write out a new-style *.spin file.
+#
+# $data_ref - Hash of data for the file
+# $path - Path to output file
+sub _write_spin_pointer {
+ my ($self, $data_ref, $path) = @_;
+
+ # Generate the YAML output and strip off the leading document separator.
+ local $YAML::XS::Boolean = 'JSON::PP';
+ my $yaml = YAML::XS::Dump($data_ref);
+ $yaml =~ s{ \A --- \n }{}xms;
+
+ # Write the output.
+ $path->spew_utf8($yaml);
+ return;
+}
+
+# Convert an *.rpod file to a *.spin file. Intended to be run via
+# Path::Iterator::Rule.
+#
+# $rpod_path - Path to *.rpod file
+# $repo - Optional Git::Repository object for input tree
+sub _convert_rpod_pointer {
+ my ($self, $rpod_path, $repo) = @_;
+
+ # Convert the file.
+ my $data_ref = $self->_read_rpod_pointer($rpod_path);
+ my $basename = $rpod_path->basename('.rpod');
+ my $spin_path = $rpod_path->sibling($basename . '.spin');
+ $self->_write_spin_pointer($data_ref, $spin_path);
+
+ # If we have a Git repository, update Git.
+ if (defined($repo)) {
+ my $root = path($repo->work_tree());
+ $repo->run('add', $spin_path->relative($root)->stringify());
+ $repo->run('rm', $rpod_path->relative($root)->stringify());
+ }
+ return;
+}
+
+##############################################################################
# Public Interface
##############################################################################
@@ -172,16 +258,8 @@ sub _config_from_json {
# Throws: Text exceptions on invalid metadata directory path
sub new {
my ($class, $args_ref) = @_;
-
- # Ensure we were given a valid metadata argument.
- my $metadata = $args_ref->{metadata} // 'docs/metadata';
- if (!-d $metadata) {
- croak("metadata path $metadata does not exist or is not a directory");
- }
-
- # Create and return the object.
my $self = {
- metadata => $metadata,
+ metadata => $args_ref->{metadata} // 'docs/metadata',
output => $args_ref->{output} // 'docs/docknot.yaml',
};
bless($self, $class);
@@ -197,6 +275,12 @@ sub new {
sub update {
my ($self) = @_;
+ # Ensure we were given a valid metadata argument.
+ if (!-d $self->{metadata}) {
+ my $metadata = $self->{metadata};
+ croak("metadata path $metadata does not exist or is not a directory");
+ }
+
# Tell YAML::XS that we'll be feeding it JSON::PP booleans.
local $YAML::XS::Boolean = 'JSON::PP';
@@ -278,6 +362,28 @@ sub update {
return;
}
+# Update an input tree for spin to the current format.
+#
+# $path - Optional path to the spin input tree, defaults to current directory
+#
+# Raises: Text exception on failure
+sub update_spin {
+ my ($self, $path) = @_;
+ $path = defined($path) ? path($path) : path(q{.});
+ my $repo;
+ if ($path->child('.git')->is_dir()) {
+ $repo = Git::Repository->new(work_tree => "$path");
+ }
+
+ # Convert all *.rpod files to *.spin files.
+ my $rule = Path::Iterator::Rule->new()->name(qr{ [.] rpod \z }xms);
+ my $iter = $rule->iter($path, { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ $self->_convert_rpod_pointer(path($file), $repo);
+ }
+ return;
+}
+
##############################################################################
# Module return value and documentation
##############################################################################
@@ -290,23 +396,27 @@ Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG
=head1 NAME
-App::DocKnot::Update - Update DocKnot package configuration for new formats
+App::DocKnot::Update - Update DocKnot input or package configuration
=head1 SYNOPSIS
use App::DocKnot::Update;
- my $reader = App::DocKnot::Update->new(
+
+ my $update = App::DocKnot::Update->new(
{
metadata => 'docs/metadata',
output => 'docs/docknot.yaml',
}
);
- my $config = $reader->update();
+ $update->update();
+
+ $update->update_spin('/path/to/spin/input');
=head1 REQUIREMENTS
-Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON,
-Perl6::Slurp, and YAML::XS, all of which are available from CPAN.
+Perl 5.24 or later and the modules Git::Repository, File::BaseDir,
+File::ShareDir, JSON::MaybeXS, Path::Iterator::Rule, Path::Tiny, Perl6::Slurp,
+and YAML::XS, all of which are available from CPAN.
=head1 DESCRIPTION
@@ -348,6 +458,13 @@ F<docs/docknot.yaml> relative to the current directory.
Load the legacy JSON metadata and write out the YAML equivalent.
+=item update_spin([PATH])
+
+Update the input tree for App::DocKnot::Spin to follow current expectations.
+PATH is the path to the input tree, which defaults to the current directory
+if not given. If the input tree is the working tree for a Git repository,
+any changes are also registered with Git (but not committed).
+
=back
=head1 AUTHOR
@@ -356,7 +473,7 @@ Russ Allbery <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/t/data/spin/update/input/module.rpod b/t/data/spin/update/input/module.rpod
new file mode 100644
index 0000000..5f75697
--- /dev/null
+++ b/t/data/spin/update/input/module.rpod
@@ -0,0 +1 @@
+/path/Module.pm
diff --git a/t/data/spin/update/input/readme.rpod b/t/data/spin/update/input/readme.rpod
new file mode 100644
index 0000000..042274b
--- /dev/null
+++ b/t/data/spin/update/input/readme.rpod
@@ -0,0 +1,2 @@
+/path/readme.pod
+-c -t 'Basic Information'
diff --git a/t/data/spin/update/input/script.rpod b/t/data/spin/update/input/script.rpod
new file mode 100644
index 0000000..00f79c7
--- /dev/null
+++ b/t/data/spin/update/input/script.rpod
@@ -0,0 +1,3 @@
+/path/script
+
+/~eagle/styles/script.css
diff --git a/t/data/spin/update/output/module.spin b/t/data/spin/update/output/module.spin
new file mode 100644
index 0000000..31b97ce
--- /dev/null
+++ b/t/data/spin/update/output/module.spin
@@ -0,0 +1,2 @@
+format: pod
+path: /path/Module.pm
diff --git a/t/data/spin/update/output/readme.spin b/t/data/spin/update/output/readme.spin
new file mode 100644
index 0000000..c78a5cf
--- /dev/null
+++ b/t/data/spin/update/output/readme.spin
@@ -0,0 +1,6 @@
+format: pod
+options:
+ contents: true
+ navbar: false
+path: /path/readme.pod
+title: Basic Information
diff --git a/t/data/spin/update/output/script.spin b/t/data/spin/update/output/script.spin
new file mode 100644
index 0000000..5be3d64
--- /dev/null
+++ b/t/data/spin/update/output/script.spin
@@ -0,0 +1,3 @@
+format: pod
+path: /path/script
+style: /~eagle/styles/script.css
diff --git a/t/lib/Test/DocKnot/Spin.pm b/t/lib/Test/DocKnot/Spin.pm
index c7c33b8..2c520a2 100644
--- a/t/lib/Test/DocKnot/Spin.pm
+++ b/t/lib/Test/DocKnot/Spin.pm
@@ -82,6 +82,10 @@ sub is_spin_output_tree {
# File::Find on the output directory.
my $check_output = sub {
my $file = $_;
+ if ($file eq '.git') {
+ $File::Find::prune = 1;
+ return;
+ }
return if -d $file;
# Determine the relative path and mark it as seen.
@@ -191,7 +195,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2021 Russ Allbery <rra@cpan.org>
+Copyright 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/t/update/spin.t b/t/update/spin.t
new file mode 100755
index 0000000..c692c80
--- /dev/null
+++ b/t/update/spin.t
@@ -0,0 +1,65 @@
+#!/usr/bin/perl
+#
+# Tests for the spin part of the App::DocKnot::Update module API.
+#
+# Copyright 2022 Russ Allbery <rra@cpan.org>
+#
+# SPDX-License-Identifier: MIT
+
+use 5.024;
+use autodie;
+use warnings;
+
+use lib 't/lib';
+
+use File::Copy::Recursive qw(dircopy);
+use Git::Repository ();
+use Path::Tiny qw(path);
+use Test::DocKnot::Spin qw(is_spin_output_tree);
+
+use Test::More;
+
+# Isolate from the environment.
+local $ENV{XDG_CONFIG_HOME} = '/nonexistent';
+local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
+
+# Load the module.
+require_ok('App::DocKnot::Update');
+
+# Construct the source tree. Copy t/data/spin/update/input into a fresh Git
+# repository and commit it so that we can test the Git interaction.
+my $input = path('t', 'data', 'spin', 'update', 'input');
+my $tempdir = Path::Tiny->tempdir();
+Git::Repository->run('init', { cwd => "$tempdir", quiet => 1 });
+dircopy($input, "$tempdir")
+ or die "$0: cannot copy $input to $tempdir: $!\n";
+my $repo = Git::Repository->new(work_tree => "$tempdir");
+$repo->run(config => '--add', 'user.name', 'Test');
+$repo->run(config => '--add', 'user.email', 'test@example.com');
+$repo->run(add => '-A', q{.});
+$repo->run(commit => '-q', '-m', 'Initial commit');
+
+# Update the tree.
+my $update = App::DocKnot::Update->new();
+$update->update_spin($tempdir);
+
+# Check the resulting output.
+my $expected = path('t', 'data', 'spin', 'update', 'output');
+my $count = is_spin_output_tree("$tempdir", "$expected", 'Tree updated');
+my @changes = grep { m{ deleted | new [ ] file }xms } $repo->run('status');
+@changes = map { [split(q{ })] } sort(@changes);
+is_deeply(
+ \@changes,
+ [
+ ['deleted:', 'module.rpod'],
+ ['deleted:', 'readme.rpod'],
+ ['deleted:', 'script.rpod'],
+ ['new', 'file:', 'module.spin'],
+ ['new', 'file:', 'readme.spin'],
+ ['new', 'file:', 'script.spin'],
+ ],
+ 'Git operations',
+);
+
+# Report the end of testing.
+done_testing($count + 2);