diff options
author | Russ Allbery <rra@cpan.org> | 2022-01-02 13:23:07 -0800 |
---|---|---|
committer | Russ Allbery <rra@cpan.org> | 2022-01-02 13:23:07 -0800 |
commit | 7c2fb0c0588ca7cb7e54695935afd965c22cab27 (patch) | |
tree | 62861892e985558fdf6867a0db77f01afab1d664 | |
parent | c00d3652d51a440d6c5378631af4021c39afa644 (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-- | Changes | 5 | ||||
-rwxr-xr-x | bin/docknot | 21 | ||||
-rw-r--r-- | lib/App/DocKnot/Command.pm | 7 | ||||
-rw-r--r-- | lib/App/DocKnot/Update.pm | 147 | ||||
-rw-r--r-- | t/data/spin/update/input/module.rpod | 1 | ||||
-rw-r--r-- | t/data/spin/update/input/readme.rpod | 2 | ||||
-rw-r--r-- | t/data/spin/update/input/script.rpod | 3 | ||||
-rw-r--r-- | t/data/spin/update/output/module.spin | 2 | ||||
-rw-r--r-- | t/data/spin/update/output/readme.spin | 6 | ||||
-rw-r--r-- | t/data/spin/update/output/script.spin | 3 | ||||
-rw-r--r-- | t/lib/Test/DocKnot/Spin.pm | 6 | ||||
-rwxr-xr-x | t/update/spin.t | 65 |
12 files changed, 250 insertions, 18 deletions
@@ -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); |