The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Dist::Zilla::Role::TextTemplater - Have text templating capabilities in your Dist::Zilla plugin

VERSION

Version v0.8.7, released on 2018-02-27 21:17 UTC.

WHAT?

Dist-Zilla-Role-TextTemplater is a Dist::Zilla role, a replacement for standard role TextTemplate. Both roles have the same great Text::Template engine under the hood, but this one provides better control over the engine and much better error reporting.

This is Dist::Zilla::Role::TextTemplater module documentation. Read this if you want to have text templating capabilities in your Dist::Zilla plugin.

If you are using a TextTemplater-based plugin, read the manual. General topics like getting source, building, installing, bug reporting and some others are covered in the README.

SYNOPSIS

    package Dist::Zilla::Plugin::YourPlugin;
    use Moose;
    use namespace::autoclean;
    with 'Dist::Zilla::Role::Plugin';
    with 'Dist::Zilla::Role::TextTemplater';

    sub method {
        my $self = shift( @_ );
        ...;
        $result = $self->fill_in_string( $template );
        ...;
    };

    sub another_method {
        my $self = shift( @_ );
        ...;
        $self->fill_in_file( $file );
        ...;
    };

    __PACKAGE__->meta->make_immutable;
    1;

DESCRIPTION

The role provides a consuming plugin with fill_in_string and fill_in_file methods and bunch of accompanying attributes and dist.ini options.

OBJECT ATTRIBUTES

delimiters

Pair of opening delimiter and closing delimiter to denote code fragments in template.

Attribute introduces dist.ini option with the same name. Option value will be split on whitespaces (result should be two items) to initialize the attribute.

Str|ArrayRef[Str], read-only. Default value is [ '{{', '}}' ].

See "Delimiters" in Dist::Zilla::Role::TextTemplater.

package

Name of package to evaluate code fragments in.

Attribute introduces dist.ini option with the same name.

Str, read-only, optional.

See "Package" in Dist::Zilla::Role::TextTemplater.

prepend

Perl code to prepend to the beginning of every code fragment.

Attribute introduces dist.ini multi-value option with the same name.

ArrayRef[Str], read-only, auto dereferenced. Default value is empty array. Consumers may specify alternative default by defining _build_prepend method.

See "Prepend" in Dist::Zilla::Role::TextTemplater.

tt_file

File being processed (either actual file or temporary InMemory file when processing a string). Available only during template processing. May be used in tt_broken method.

Object, read-only, not an init arg.

tt_errors

Errors detected in template file, in format suitable for log_errors_in_file (defined in ErrorLogger role). May be used in tt_broken method.

ArrayRef, read-write, not an init arg.

tt_broken_count

Number of tt_broken calls. The counter is increased before tt_broken call.

Int, read-only, not an init arg.

tt_broken_limit

If number of completed tt_broken calls equals or exceeds this limit, processing stops.

Int, read-only, not an init arg, default value 10.

There is no (official) way to change the attribute value now. Let me know if you need it.

OBJECT METHODS

tt_broken

This method is called if a code fragment dies. It formats error message(s) and sends it to the log by calling log_error.

See BROKEN option of "fill_in" in Text::Template.

mvp_multivalue_args

The method tells Dist::Zilla that prepend is a multi-value option.

tt_fill_in

    $file = Dist::Zilla::File::OnDisk( ... );   # or
    $file = Dist::Zilla::File::InMemory( ... ); # or
    $file = Dist::Zilla::File::FromCode( ... );

    $result = $self->fill_in_string( $file, \%variables, \%extra_args );
    $result = $self->fill_in_string( $file );

Internal working horse of the role.

The method creates Text::Template object, enforces Text::Template to respect filename argument (see FILENAME parameter has no effect), takes care about warnings, then calls fill_in method on the object, making Text::Template compilation errors (if found) more user-friendly.

$file->content is passed to the Text::Template constructor. \%variables, \%extra_args, and package, prepend, broken attributes are combined and passed to both Text::Template constructor and fill_in method.

\%variables become hash Text::Template option (see "HASH" in Text::Template for details). Variables plugin (reference to object executing the method, i. e. $self) and dist (reference to Dist::Zilla, i. e. $self->zilla) are added to \%variables automatically, if they are not exist.

package, prepend, broken attributes become same-name Text::Template options. \%extra_args is expanded to list and passed last, so caller can override any option specified by tt_fill_in (except filename), for example:

    $self->tt_fill_in( $file, undef, { package => 'MY' } );

will execute template code fragments in context of MY package regardless of package attribute. Another, a bit more complicated example:

    $self->tt_fill_in( $file, undef, { hash => { } } );

processes template with no predefined variables: plugin and dist are added to \%variables, but entire \%variables is overridden by hash extra argument.

tt_fill_in takes special care about package: by default nested tt_fill_in calls use the same package as the outermost call. Of course, if package extra argument is explicitly specified in inner call, it takes priority over default package.

When defining variables with either \%variables argument or hash extra argument, remember that Text::Template dereferences all the references. It especially important if you want to pass a reference to template:

    $array = [ … ]; $hash = { … };
    $self->tt_fill_in(
        $file,
        { array => \$array, hash => \$hash, plugin => \$self },
    );

In template, $array will be a reference to array, $hash — reference to hash, $plugin — reference to plugin. If you forget to take references, e. g.:

    $self->tt_fill_in(
        $file,
        { array => $array, hash => $hash, plugin => $self },
    );

template will have @array (array, not reference to array), %hash (hash, not reference to hash), and $plugin will be (oops!) undefined.

Scalars can be passed either by reference or value:

    $self->tt_fill_in(
        $file,
        { str1 => "string", str2 => \"string" },
    );

If $file is mutable (i. e. does Dist::Zilla::Role::MutableFile role), file content will be updated with result of template processing.

See "HASH" in Text::Template for more details.

fill_in_string

    $template = '…';
    $result = $self->fill_in_string( $template, \%variables, \%extra_args );
    $result = $self->fill_in_string( $template );

The primary method of the role.

The fill_in_string interface is compatible with the same-name method of TextTemplate role, so this role can be used as a drop-in replacement for TextTemplate. However, method is implemented slightly differently, it may cause subtle differences in behaviour.

The method creates temporary Dist::Zilla::File::InMemory object with name "template" (it can be overridden by filename extra argument, though) and calls tt_fill_in method, passing down temporary file, \%variables and \%extra_args.

fill_in_file

    $file = Dist::Zilla::File::OnDisk->new( { … } );    # or
    $file = Dist::Zilla::File::InMemory->new( { … } );  # or
    $file = Dist::Zilla::File::FromCode->new( { … } );  # or
    $file = Path::Tiny->new( 'filename' );              # or
    $file = Path::Class::File->new( 'filename' );       # or
    $file = 'filename.ext';

    $result = $self->fill_in_file( $file, \%variables, \%extra_args );
    $result = $self->fill_in_file( $file );

Similar to fill_in_string, but the first argument is not a template but a file object or file name to read template from. File can be any of Dist::Zilla file types (file is read with content method) or Path::Tiny file (file is read with slurp_utf8 method), or Path::Class::File (read by slurp( iomode => '<:encoding(UTF-8)' )) or just a file name (temporary Dist::Zilla::File::OnDisk object is created internally).

Note that filename extra argument is ignored, file name cannot be overridden.

The method returns result of template processing. If the file is mutable (i. e. does Dist::Zilla::Role::MutableFile) file content is also updated.

Note: Dist::Zilla::Role::MutableFile introduced in Dist::Zilla version 5.000. In earlier versions there is no Dist::Zilla::Role::MutableFile role and so, file content is never updated.

NOTES

Text::Template option spelling

Text::Template allows a programmer to use different spelling of options: all-caps, first-caps, all-lowercase, with and without leading dash, e. g.: HASH, Hash, hash, -HASH, -Hash, -hash. This is documented feature.

Text::Template recommends to pick a style and stick with it. (BTW, Text::Template documentation uses all-caps spelling.) This role picked all-lowercase style. This choice have subtle consequences. Let us consider an example:

    $self->fill_in_string( $template, undef, { PACKAGE => 'MY' } );

Extra option PACKAGE may or may not have effect, depending on value of package attribute (i. e. presence or absence package option in dist.ini file), because (this is not documented) spellings are not equal: different spellings have different priority. If PACKAGE and package are specified simultaneously, package wins, PACKAGE loses.

This feature gives you a choice. If you want to ignore option specified by the user in dist.ini and provide your value, use all-lowercase option name. If you want to provide default which can be overridden by the user, use all-caps options name.

filename option

When Text::Template reads template from a file, it uses the actual file name in error messages, e. g.:

    Undefined subroutine &foo called at dir/filename.ext line n

where dir/filename.ext is the name of file containing the template. When Text::Template processes a string, it uses word "template" instead of file name, e. g.:

    Undefined subroutine &foo called at template line n

The option filename allows the caller to override it:

    $self->fill_in_file( $file, undef, { filename => 'Assa.txt' } );

Error message would look like:

    Undefined subroutine &foo called at Assa.txt line n

It may seem this does not make much sense, but in our case (Dist::Zilla and its plugins) Text::Template always processes strings and never reads files, because reading files is a duty of Dist::Zilla::File::OnDisk class. Thus, using filename option is critical to provide good error messages. Actually, fill_in_file implementation looks like

    $self->fill_in_string(
        $file->content,
        undef,
        { filename => $file->name },
    );

There are two problems with the option, though:

  • Text::Template does not document this option.

    I believe it is a mistake and option should be documented.

  • Text::Template ignores this option.

    I am sure this is a bug and hope it will be fixed eventually. I am afraid it will not be fixed soon, though.

    Meanwhile, TextTemplater implements a workaround to let the option work, so TextTemplater consumers can utilize the filename option.

WHY?

TextTemplate role from Dist::Zilla distribution v5.037 has the same great Text::Template engine under the hood, but lacks of control and has awful error reporting.

Error Reporting

Let us consider an example. For sake of example simplicity, it contains only one file, dist.ini. Two files, lib/Assa.pm and lib/Assa.pod, are generated on-the-fly with GenerateFile plugin.

Have a look at dist.ini:

    name     = Assa
    version  = 0.001
    abstract = Example
    [GenerateFile/lib/Assa.pm]
        filename = lib/Assa.pm
        content  = package Assa; 1;
    [GenerateFile/lib/Assa/Manual.pod]
        filename = lib/Assa/Manual.pod
        content  = =head1 NAME
        content  =
        content  = {{$dst->name} - {{$dist->abstract}}
        content  =
        content  = Version {{$dist->version}}.
        content  =
        content  = {{$dist->license->notice}}
    [TemplateFiles]
        filename = lib/Assa.pm
        filename = lib/Assa/Manual.pod
    [MetaResources::Template]
        homepage = https://example.org/release/{{$dist->name}}
        license  = {{$dist->license->url}}

(Do you see a typo? How many? Note this is a small example, real files are much larger.) Now let us build the distribution:

    $ dzil build
    [DZ] beginning to build Assa
    [TemplateFiles] Filling in the template returned undef for:
    [TemplateFiles] =head1 NAME
    [TemplateFiles]
    [TemplateFiles] {{$dst->name} - {{$dist->abstract}}
    [TemplateFiles]
    [TemplateFiles] Version {{$dist->version}}.
    [TemplateFiles]
    [TemplateFiles] {{$dist->license->notice}}

    [TemplateFiles] Filling in the template returned undef for:
    [TemplateFiles] =head1 NAME
    [TemplateFiles]
    [TemplateFiles] {{$dst->name} - {{$dist->abstract}}
    [TemplateFiles]
    [TemplateFiles] Version {{$dist->version}}.
    [TemplateFiles]
    [TemplateFiles] {{$dist->license->notice}}
     at /home/vdb/.usr/opt/local-lib/lib/perl5/x86_64-linux-thread-multi/Moose/Meta/Method/Delegation.pm line 110.

Oops. What's happened? Where? Why? All we have is a highly unclear error message

    Filling in the template returned undef for:

and file content printed twice. (Yep, if the file had 1000 lines, we would have it printed twice too.) We do not ever have a file name and have to guess it by the content. Good bug hunting, dude.

Ok, let us fix the problem (mistyped closing delimiter in the first line of file lib/Assa/Manual.pod) and build the distribution again:

    $ dzil build
    [DZ] beginning to build Assa
    Can't call method "name" on an undefined value at template line 3.

Oops. Error message much is better now, but where the problem is? There are many templates in the project: lib/Assa.pm, lib/Assa/Manual.pod, and even resources in META.yml — all are generated from templates. Where is the problem? Good bug hunting for us all.

Such error reporting is simply unacceptable. I am a human, I often make mistakes, and I want the tool clearly warns me what and where the problem is, so I can fix it quickly. For example, in the first case I want to see:

    $ dzil build
    [DZ] beginning to build Assa
    [Templates] Unmatched opening delimiter at lib/Assa/Manual.pod line 3.
    [Templates] lib/Assa/Manual.pod:
    [Templates]     1: =head1 NAME
    [Templates]     2:
    [Templates]     3: {{$dst->name} - {{$dist->abstract}}
    [Templates]        ^^^ Unmatched opening delimiter at lib/Assa/Manual.pod line 3. ^^^
    [Templates]     4:
    [Templates]     5: Version {{$dist->version}}.
    [Templates]        ... skipped 2 lines ...
    Aborting...

In the second case:

    $ dzil build
    [DZ] beginning to build Assa
    [Templates] Can't call method "name" on an undefined value at lib/Assa/Manual.pod line 3.
    [Templates] Bad code fragment begins at lib/Assa/Manual.pod line 3.
    [Templates] lib/Assa/Manual.pod:
    [Templates]     1: =head1 NAME
    [Templates]     2:
    [Templates]     3: {{$dst->name}} - {{$dist->abstract}}
    [Templates]        ^^^ Can't call method "name" on an undefined value at lib/Assa/Manual.pod line 3. ^^^
    [Templates]        ^^^ Bad code fragment begins at lib/Assa/Manual.pod line 3. ^^^
    [Templates]     4:
    [Templates]     5: Version {{$dist->version}}.
    [Templates]        ... skipped 2 lines ...
    Aborting...

TextTemplater makes it real. All I need is using TextTemplater-based plugins: Templates, MetaResources::Template (starting from v0.002).

Engine Control

TextTemplater allows the end-user to specify delimiters, package and prepend engine options in dist.ini file, while TextTemplate allows to specify prepend only programmatically, and does not allow to specify delimiters and package.

SEE ALSO

Dist::Zilla
Dist::Zilla::Role
Dist::Zilla::Plugin
Dist::Zilla::Role::TextTemplate
Text::Template

AUTHOR

Van de Bugger <van.de.bugger@gmail.com>

COPYRIGHT AND LICENSE

Copyright (C) 2015, 2016, 2018 Van de Bugger

License GPLv3+: The GNU General Public License version 3 or later <http://www.gnu.org/licenses/gpl-3.0.txt>.

This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.