File Coverage

blib/lib/App/Changelog2x.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # This file copyright (c) 2008-2009 by Randy J. Ray, all rights reserved
4             #
5             # Copying and distribution are permitted under the terms of the Artistic
6             # License 2.0 (http://www.opensource.org/licenses/artistic-license-2.0.php) or
7             # the GNU LGPL (http://www.opensource.org/licenses/lgpl-license.php).
8             #
9             ###############################################################################
10             #
11             # Description: A wrapper in the App::* space for the core functionality
12             # provided by the changelog2x script.
13             #
14             # Functions: new
15             # version
16             # default_xslt_path
17             # default_date_format
18             # date_format
19             # xslt_path
20             # application_tokens
21             # format_date
22             # credits
23             # transform_changelog
24             #
25             # Libraries: XML::LibXML
26             # XML::LibXSLT
27             # DateTime
28             # DateTime::Format::ISO8601
29             # File::Spec
30             #
31             # Global Consts: $VERSION
32             # URI
33             #
34             ###############################################################################
35              
36             package App::Changelog2x;
37              
38 2     2   13096 use 5.008;
  2         9  
  2         80  
39 2     2   12 use strict;
  2         4  
  2         66  
40 2     2   19 use warnings;
  2         4  
  2         89  
41 2     2   12 use vars qw($VERSION $FORMAT $DEFAULT_XSLT_PATH);
  2         3  
  2         154  
42 2         10 use subs qw(new version default_xslt_path default_date_format date_format
43             xslt_path application_tokens format_date credits
44 2     2   2146 transform_changelog);
  2         40  
45 2     2   206 use constant URI => 'http://www.blackperl.com/2009/01/ChangeLogML';
  2         4  
  2         121  
46              
47 2     2   12 use File::Spec;
  2         4  
  2         47  
48              
49 2     2   908 use XML::LibXML;
  0            
  0            
50             use XML::LibXSLT;
51             use DateTime;
52             use DateTime::Format::ISO8601;
53              
54             BEGIN
55             {
56             $VERSION = '0.11';
57              
58             $DEFAULT_XSLT_PATH = (File::Spec->splitpath(__FILE__))[1];
59             $DEFAULT_XSLT_PATH = File::Spec->catdir($DEFAULT_XSLT_PATH, 'changelog2x');
60             }
61              
62             ###############################################################################
63             #
64             # Sub Name: new
65             #
66             # Description: Dead-simple constructor. We're just a plain blessed
67             # hashref, here.
68             #
69             # Arguments: NAME IN/OUT TYPE DESCRIPTION
70             # $class in scalar Class to bless into
71             # %args in hash Any data to start off with
72             #
73             # Returns: object referent
74             #
75             ###############################################################################
76             sub new
77             {
78             my ($class, %args) = @_;
79              
80             my $self = bless {}, $class;
81              
82             # If the user didn't pass the xslt_path argument, set up the default
83             $args{xslt_path} ||= [ $self->default_xslt_path ];
84              
85             foreach (qw(date_format xslt_path))
86             {
87             # These are the known parameters; if present, call the method to set
88             $self->$_(delete $args{$_}) if $args{$_};
89             }
90              
91             # Copy over any remaining parameters we don't know verbatim
92             for (keys %args)
93             {
94             $self->{$_} = $args{$_};
95             }
96              
97             $self;
98             }
99              
100             # Encapsulated way of retrieving $VERSION, in case someone sub-classes us
101             sub version { $VERSION }
102              
103             # Likewise access to $DEFAULT_XSLT_PATH
104             sub default_xslt_path { $DEFAULT_XSLT_PATH }
105              
106             # And the default date-format
107             sub default_date_format { '%A %B %e, %Y, %r TZ_SHORT' }
108              
109             ###############################################################################
110             #
111             # Sub Name: date_format
112             #
113             # Description: Get or set a default format string for format_date() to
114             # use. If $format is passed, set that as the new format to
115             # use. If no format is set by the user, falls through to
116             # default_date_format().
117             #
118             # Arguments: NAME IN/OUT TYPE DESCRIPTION
119             # $self in ref Object of this class
120             # $format in scalar New format string
121             #
122             # Returns: Date format
123             #
124             ###############################################################################
125             sub date_format
126             {
127             my ($self, $format) = @_;
128              
129             if ($format)
130             {
131             $self->{format} =
132             ($format eq 'unix') ? '%a %b %d %T TZ_SHORT %Y' : $format;
133             }
134              
135             $self->{format} || $self->default_date_format;
136             }
137              
138             ###############################################################################
139             #
140             # Sub Name: xslt_path
141             #
142             # Description: Return the path to where XSLT files should be searched for.
143             # If this is not set by the user, then return the value for
144             # default_xslt_path(). If a value is passed for $path, make
145             # that the new XSLT directory.
146             #
147             # Arguments: NAME IN/OUT TYPE DESCRIPTION
148             # $self in ref Object of this class
149             # $paths in list New directories to use.
150             #
151             # Returns: path
152             #
153             ###############################################################################
154             sub xslt_path
155             {
156             my ($self, @paths) = @_;
157              
158             if (@paths)
159             {
160             if (ref($paths[0]) eq 'ARRAY')
161             {
162             $self->{xslt_path} = [ @{$paths[0]} ];
163             }
164             else
165             {
166             unshift(@{$self->{xslt_path}}, @paths);
167             }
168             }
169              
170             wantarray ? @{$self->{xslt_path}} : $self->{xslt_path};
171             }
172              
173             ###############################################################################
174             #
175             # Sub Name: application_tokens
176             #
177             # Description: Get/set the string that should be present in the "credits"
178             # string, identifying the application that is using this
179             # class to transform ChangeLogML.
180             #
181             # Arguments: NAME IN/OUT TYPE DESCRIPTION
182             # $self in ref Object of this class
183             # $tokens in scalar If present, string/tokens to
184             # store for later use
185             #
186             # Returns: application tokens
187             #
188             ###############################################################################
189             sub application_tokens
190             {
191             my ($self, $tokens) = @_;
192              
193             $self->{application_tokens} = $tokens if $tokens;
194              
195             $self->{application_tokens};
196             }
197              
198             ###############################################################################
199             #
200             # Sub Name: format_date
201             #
202             # Description: Take a date-string in (ISO 8601 format) and return a
203             # more readable format.
204             #
205             # Arguments: NAME IN/OUT TYPE DESCRIPTION
206             # $self in scalar Class name or object ref
207             # $date in scalar Date-string in ISO 8601
208             # $to_utc in scalar Boolean flag, whether to
209             # convert times to GMT/UTC
210             #
211             # Returns: Formatted date/time
212             #
213             ###############################################################################
214             sub format_date
215             {
216             my ($self, $date, $to_utc) = @_;
217              
218             my $dt = DateTime::Format::ISO8601->parse_datetime($date);
219             $dt->set_time_zone('UTC') if $to_utc;
220              
221             my $string = $dt->strftime($self->date_format);
222             if ($string =~ /TZ_/)
223             {
224             my %tz_edit = ( TZ_LONG => $dt->time_zone->name,
225             TZ_SHORT => $dt->time_zone->short_name_for_datetime );
226             $string =~ s/(TZ_LONG|TZ_SHORT)/$tz_edit{$1}/ge;
227             }
228              
229             $string;
230             }
231              
232             ###############################################################################
233             #
234             # Sub Name: credits
235             #
236             # Description: Produce a "credits" message for inclusion in transformed
237             # output. Combines app name and version, lib name and
238             # version, etc.
239             #
240             # Arguments: NAME IN/OUT TYPE DESCRIPTION
241             # $self in scalar Class name or object ref
242             #
243             # Globals: $cmd
244             # $VERSION
245             #
246             # Returns: credits string
247             #
248             ###############################################################################
249             sub credits
250             {
251             my $self = shift;
252              
253             my $credits =
254             sprintf("%s/%s, XML::LibXML/%s, XML::LibXSLT/%s, libxml/%s, " .
255             "libxslt/%s (with%s exslt)",
256             ref($self), $self->version,
257             $XML::LibXML::VERSION, $XML::LibXSLT::VERSION,
258             XML::LibXML::LIBXML_DOTTED_VERSION(),
259             XML::LibXSLT::LIBXSLT_DOTTED_VERSION(),
260             (XML::LibXSLT::HAVE_EXSLT() ? '' : 'out'));
261             if (my $apptokens = $self->application_tokens)
262             {
263             $credits = "$apptokens, $credits";
264             }
265              
266             $credits;
267             }
268              
269             ###############################################################################
270             #
271             # Sub Name: transform_changelog
272             #
273             # Description: Take a filehandle or string for input, a filehandle for
274             # output, filename/string of a XSL transform, and optional
275             # parameters. Process the input according to the XSLT and
276             # stream the results to the output handle.
277             #
278             # Arguments: NAME IN/OUT TYPE DESCRIPTION
279             # $self in scalar Class name or object ref
280             # $xmlin in scalar Filehandle to read/parse or
281             # string
282             # $xmlout in ref Filehandle to output the
283             # transformed XML to
284             # $style in scalar Stylesheet, either a string
285             # or the name of a file
286             # $params in hashref If present, parameters that
287             # should be converted for use
288             # in the XSLT and passed in.
289             #
290             # Globals: URI
291             #
292             # Returns: Success: null
293             # Failure: dies
294             #
295             ###############################################################################
296             sub transform_changelog
297             {
298             my ($self, $xmlin, $xmlout, $style, $params) = @_;
299             $params ||= {}; # In case they didn't pass any
300              
301             our $parser = XML::LibXML->new();
302             our $xslt = XML::LibXSLT->new();
303              
304             $parser->expand_xinclude(1);
305             $xslt->register_function(URI, 'format-date',
306             sub { $self->format_date(@_) });
307             $xslt->register_function(URI, 'credits',
308             sub { $self->credits(@_) });
309              
310             our (%params, $xsltc, $source, $stylesheet, $result);
311              
312             # If the template isn't already an absolute path, use the root-dir and add
313             # the "changelog2" prefix and ".xslt" suffix
314             unless ($style =~ /^<\?xml/)
315             {
316             $xsltc = $self->resolve_template($style)
317             or die "Could not resolve style '$style' to a file";
318             $style = $xsltc;
319             }
320              
321             # First copy over and properly setup/escape the parameters, so that XSLT
322             # understands them.
323             %params = map { XML::LibXSLT::xpath_to_string($_ => $params->{$_}) }
324             (keys %$params);
325              
326             # Do the steps of parsing XML documents, creating stylesheet engine and
327             # applying the transform. Each throws a die on error, so each has to be
328             # eval'd to allow for a cleaner error report:
329             eval {
330             $source = ref($xmlin) ?
331             $parser->parse_fh($xmlin) : $parser->parse_string($xmlin);
332             };
333             die "Error parsing input-XML content: $@" if $@;
334             eval {
335             $xsltc = ($style =~ /^<\?xml/) ?
336             $parser->parse_string($style) : $parser->parse_file($style);
337             };
338             die "Error parsing the XML of the XSLT stylesheet '$style': $@" if $@;
339             eval { $stylesheet = $xslt->parse_stylesheet($xsltc); };
340             die "Error parsing the XSLT syntax of the stylesheet: $@" if $@;
341             eval { $result = $stylesheet->transform($source, %params); };
342             die "Error applying transform to input content: $@" if $@;
343              
344             $stylesheet->output_fh($result, $xmlout);
345             return;
346             }
347              
348             ###############################################################################
349             #
350             # Sub Name: resolve_template
351             #
352             # Description: Resolve a non-absolute template name to a complete file.
353             # This may include adding "changelog2" and ".xslt" to the
354             # string. If the name is already absolute or starts with a
355             # '.', it is returned unchanged.
356             #
357             # Arguments: NAME IN/OUT TYPE DESCRIPTION
358             # $self in ref Object of this class
359             # $template in scalar Name to resolve
360             #
361             # Returns: Success: full path
362             # Failure: empty string
363             #
364             ###############################################################################
365             sub resolve_template
366             {
367             my ($self, $template) = @_;
368              
369             return $template if ((substr($template, 0, 1) eq '.') ||
370             File::Spec->file_name_is_absolute($template));
371              
372             my @paths = $self->xslt_path;
373             my $candidate;
374              
375             $template = "changelog2$template.xslt" unless ($template =~ /\.xslt?/i);
376              
377             for (@paths)
378             {
379             $candidate = File::Spec->catfile($_, $template);
380             last if -f $candidate;
381             undef $candidate;
382             }
383              
384             $candidate;
385             }
386              
387             1;
388              
389             __END__