rjbs forgot what he was saying

not logged in (root) | by date | tagcloud | help | login

what is pod weaver? (pt. 2: pod weaver and you)

by rjbs, created 2009-10-30 17:47
last modified 2009-10-30 21:14
tagged with: @markup:md journal perl pod programming

So, yesterday I wrote about Pod::Weaver's history. Today, the much more useful topic of "how to use it now that it exists."

I try to write classes that define objects in terms that you can think about as actual objects: machines that perform a given task. A Pod::Weaver object is a machine that expects to be given some source material from which to build a Pod::Elemental::Document. That's really all it does. It has a method called weave_document that performs that function, and everything else is a kind of constructor or support method.

There are a few kinds of common input to Pod::Weaver: preexisting Pod documents, PPI (Perl source) documents, Software::License objects, and a few other kinds of data. The exact data required are determined by the plugins loaded into the Weaver object itself. The Weaver object doesn't actually produce any output itself. Instead, it delegates all of that work to its plugins.

If you're familiar with Dist::Zilla's plugin system, then you'll understand Pod::Weaver's. It's the same basic idea: there are several phases in the process of weaving a document, and plugins can perform roles (by composing Moose::Roles into their definition). Here is most of the code in the Weaver's weave_document method:

  $self->plugins_with(-Preparer)->each_value(sub {
    $_->prepare_input($input);
  });

  $self->plugins_with(-Dialect)->each_value(sub {
    $_->translate_dialect($input->{pod_document});
  });

  $self->plugins_with(-Transformer)->each_value(sub {
    $_->transform_document($input->{pod_document});
  });

  $self->plugins_with(-Section)->each_value(sub {
    $_->weave_section($document, $input);
  });

  $self->plugins_with(-Finalizer)->each_value(sub {
    $_->finalize_document($document, $input);
  });

A single plugin can perform as many of these roles as it wants. The most common role to perform (so far) is Section. A section is expected to look at the input and, based on the input, tack content on to the end of the output document. One plugin that performs multiple roles is Collect, which will turn:

=method whatever

This is the whatever method.

=method other_one

This is the other_one method.

...into...

=head1 METHODS

=head2 whatever

This is the whatever method.

=head2 other_one

This is the other_one method.

It does this by first acting as a Transformer, altering the input Document to look like plain old Pod5 where the =method commands are seen, and then acting like a Section to find and include the METHODS section.

Configuring a Pod::Weaver object requires that you decide what plugins you need to get from your input to your desired output. Right now, most users are using Pod::Weaver to more or less emulate the original Pod-munging tasks of Dist::Zilla. They're using the default configuration of Pod::Weaver, and you can get a Weaver with the default configuration by calling the new_with_default_config constructor on Pod::Weaver. Otherwise, you can configure by hand or write a config file that can be loaded by Config::MVP. The most common format for that is INI. Here's a sample:

[@CorePrep]

[Name]
[Version]

[Region  / prelude]

[Generic / SYNOPSIS]
[Generic / DESCRIPTION]
[Generic / OVERVIEW]

[Collect / ATTRIBUTES]
command = attr

[Collect / METHODS]
command = method

[Leftovers]

[Region  / postlude]

[Authors]
[Legal]

In general, you can read this as a description of what the output document will look like. Everything is a section to produce, many of them "generic," which means they're just all the stuff after a =head1 command with the right name. The only non-section is @CorePrep, which is a bundle containing two simple transformations: conversion of raw, generic Pod elements into Pod5 elements; and nesting of content under nearby =head1 commands. (Neither of these plugins does the Section role.)

You can write your own configuration file easily, and you can write your own plugins just about as easily. Look at the source of the existing plugins to see how simple they are.

So, now you know what Pod::Weaver does and how you configure it. The next question is how to put it to use. If you use Dist::Zilla, this is really easy! All you have to do is add this line to your dist.ini or other config file:

[PodWeaver]

That's it. You'll get the stock configuration. If you'd rather write your own, you can drop a weaver.ini file in your distribution's directory. If you've written your own plugin bundle to configure Pod::Weaver the way you like, you can say:

[PodWeaver]
config_plugin = @RJBS

That would set up Pod::Weaver with Pod::Weaver::PluginBundle::RJBS -- the configuration I'm using.

That's about as easy as it's going to get. If you don't want to use Dist::Zilla, though, you can build your own Pod-weaving tool by using the Pod::Elemental::PerlMunger role. It helps build a class that expects to be handed the contents of a Perl module and other inputs, and then hands back a new string containing the rewritten Perl source. It uses Pod::Elemental and PPI to pull the Pod out of the Perl (if possible), transform them as needed, and then recombine them into one string that does not change the behavior of the module's code. To write a tool built on PerlMunger, you just need a class that includes the PerlMunger role and provides a munge_perl_string method with the following semantics:

my $output_doc = $obj->munge_perl_string(\%input_doc, \%other_input);

The input documents hash has entries for ppi and pod, which will contain the PPI::Document and Pod::Elemental::Document built from the input string. The output is a hashref with the same keys, which will be reconstituted into a new string of Perl.

The PerlMunger role alters the semantics of that method for outside callers, who will write:

my $new_perl_string = $obj->munge_perl_string($input_string, \%other_input);

This makes it easy to write something like this:

package MyMunger;
use Moose;
use autodie;

sub munge_file {
  my ($self, $filename) = @_;

  open my $input_fh, '<', $filename;
  my $string = do { local $/; <$input_fh> };

  my $new_string = $self->munge_perl_string($filename, { ... });

  open my $output_fh, '>', $filename;
  print {$output_fh} $new_string;
}

my $weaver = Pod::Weaver->new_from_config; # supply your config here

sub munge_perl_string {
  my ($self, $input_doc, $input) = @_;

  my $doc = $weaver->weave_document({
    %$input,
    ppi_document => $input_doc->{ ppi },
    pod_document => $input_doc->{ pod },
  });

  return {
    ppi => $input_doc->{ppi},
    pod => $doc,
  };
} 

with 'Pod::Elemental::PerlMunger';
1;

Then you can write a little program:

use MyMunger;

MyMunger->munge_file($_) for @ARGV;

It might not be the safest thing in the world, but the simplicity of writing the program should be pretty apparent. Now imagine bundling that into your build procedure, or just a plain old pod-munging program. After all, a Pod document with no Perl content is still a valid Perl program, so you can use PerlMunger to munge pure-Pod strings, too -- it will only barf if the nonpod sections are not valid Perl.

I'm hoping to see people outside the Dist::Zilla user base get decent use out of Pod::Weaver. Even if that doesn't materialize, though, I think it was absolutely worth the time and effort. It greatly reduces the amount of time I waste writing the boring, boilerplate Pod in my distributions, which makes it a lot easier for me to get basic documentation written, which helps keep my contributions to CPAN fun to write and usably by other people.