diff options
Diffstat (limited to 'lib/App/DocKnot/Spin')
-rw-r--r-- | lib/App/DocKnot/Spin/Pointer.pm | 428 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/RSS.pm | 80 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Sitemap.pm | 11 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Thread.pm | 183 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Versions.pm | 8 |
5 files changed, 584 insertions, 126 deletions
diff --git a/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm new file mode 100644 index 0000000..05aa9ac --- /dev/null +++ b/lib/App/DocKnot/Spin/Pointer.pm @@ -0,0 +1,428 @@ +# Generate HTML from a pointer to an external file. +# +# The input tree for spin may contain pointers to external files in various +# formats. This module parses those pointer files and performs the conversion +# of those external files into HTML. +# +# SPDX-License-Identifier: MIT + +############################################################################## +# Modules and declarations +############################################################################## + +package App::DocKnot::Spin::Pointer 6.00; + +use 5.024; +use autodie; +use parent qw(App::DocKnot); +use warnings; + +use App::DocKnot::Config; +use App::DocKnot::Util qw(is_newer print_fh); +use Carp qw(croak); +use Encode qw(decode encode); +use File::BaseDir qw(config_files); +use IPC::System::Simple qw(capturex); +use Kwalify qw(validate); +use POSIX qw(strftime); +use Template (); +use YAML::XS (); + +# The URL to the software page for all of my web page generation software, +# used to embed a link to the software that generated the page. +my $URL = 'https://www.eyrie.org/~eagle/software/web/'; + +############################################################################## +# Format conversions +############################################################################## + +# Convert a Markdown file to HTML. +# +# $data_ref - Data from the pointer file +# path - Path to the Markdown file to convert +# style - Style sheet to use +# $output - Path to the output file +# +# Throws: Text exception on conversion failure +sub _spin_markdown { + my ($self, $data_ref, $output) = @_; + my $source = $data_ref->{path}; + + # Do the Markdown conversion using pandoc. + my $html = capturex( + $self->{pandoc_path}, '--wrap=preserve', '-f', 'markdown', + '-t', 'html', $source, + ); + + # Pull the title out of the contents of the <h1> header if not set. + my $title = $data_ref->{title}; + if (!defined($title)) { + ($title) = $html =~ m{ <h1 [^>]+ > (.*?) </h1> }xms; + } + + # Construct the template variables. + my ($links, $navbar, $style); + if ($self->{sitemap}) { + my $page = $output; + $page =~ s{ \A \Q$self->{output}\E }{}xms; + my @links = $self->{sitemap}->links($page); + if (@links) { + $links = join(q{}, @links); + } + my @navbar = $self->{sitemap}->navbar($page); + if (@navbar) { + $navbar = join(q{}, @navbar); + } + } + if ($data_ref->{style}) { + $style = $self->{style_url} . $data_ref->{style}; + } + #<<< + my %vars = ( + docknot_url => $URL, + html => decode('utf-8', $html), + links => $links, + modified => strftime('%Y-%m-%d', gmtime((stat($source))[9])), + navbar => $navbar, + now => strftime('%Y-%m-%d', gmtime()), + style => $style, + title => $title, + ); + #>>> + + # Construct the output page from those template variables. + my $result; + $self->{template}->process($self->{template_path}, \%vars, \$result) + or croak($self->{template}->error()); + + # Write the result to the output file. + open(my $outfh, '>', $output); + print_fh($outfh, $output, encode('utf-8', $result)); + close($outfh); + return; +} + +# Convert a POD file to HTML. +# +# $data_ref - Data from the pointer file +# options - Hash of conversion options +# contents - Whether to add a table of contents +# navbar - Whether to add a navigation bar +# path - Path to the POD file to convert +# style - Style sheet to use +# $output - Path to the output file +# +# Throws: Text exception on conversion failure +sub _spin_pod { + my ($self, $data_ref, $output) = @_; + my $source = $data_ref->{path}; + + # Construct the Pod::Thread formatter object. + #<<< + my %options = ( + contents => $data_ref->{options}{contents}, + style => $data_ref->{style} // 'pod', + ); + #<<< + if (exists($data_ref->{options}{navbar})) { + $options{navbar} = $data_ref->{options}{navbar}; + } else { + $options{navbar} = 1; + } + if (exists($data_ref->{title})) { + $options{title} = $data_ref->{title}; + } + my $podthread = Pod::Thread->new(%options); + + # Convert the POD to thread. + my $data; + $podthread->output_string(\$data); + $podthread->parse_file($source); + + # Spin that page into HTML. + $self->{thread}->spin_thread_output($data, $source, 'POD', $output); + return; +} + +############################################################################## +# Public interface +############################################################################## + +# Create a new HTML converter for pointers. This object can (and should) be +# reused for all pointer conversions done while spinning a tree of files. +# +# $args - Anonymous hash of arguments with the following keys: +# output - Root of the output tree +# sitemap - App::DocKnot::Spin::Sitemap object +# style-url - Partial URL to style sheets +# thread - App::DocKnot::Spin::Thread object +# +# Returns: Newly created object +# Throws: Text exception on failure to initialize Template Toolkit +sub new { + my ($class, $args_ref) = @_; + + # Get the configured path to pandoc, if any. + my $config_reader = App::DocKnot::Config->new(); + my $global_config_ref = $config_reader->global_config(); + my $pandoc = $global_config_ref->{pandoc} // 'pandoc'; + + # Add a trailing slash to the partial URL for style sheets. + my $style_url = $args_ref->{'style-url'} // q{}; + if ($style_url) { + $style_url =~ s{ /* \z }{/}xms; + } + + # Create and return the object. + my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error()); + #<<< + my $self = { + output => $args_ref->{output}, + pandoc_path => $pandoc, + sitemap => $args_ref->{sitemap}, + style_url => $style_url, + template => $tt, + thread => $args_ref->{thread}, + }; + #>>> + bless($self, $class); + $self->{template_path} = $self->appdata_path('templates', 'html.tmpl'); + return $self; +} + +# Check if the result of a pointer file needs to be regenerated. +# +# $pointer - Pointer file to process +# $output - Corresponding output path +# +# Returns: True if the output file does not exist or has a modification date +# older than either the pointer file or the underlying source file, +# false otherwise +# Throws: YAML::XS exception on invalid pointer +sub is_out_of_date { + my ($self, $pointer, $output) = @_; + my $data_ref = $self->load_yaml_file($pointer, 'pointer'); + if (!-e $data_ref->{path}) { + die "$pointer: path $data_ref->{path} does not exist\n"; + } + return !is_newer($output, $pointer, $data_ref->{path}); +} + +# Process a given pointer file. +# +# $pointer - Pointer file to process +# $output - Corresponding output path +# +# Throws: YAML::XS exception on invalid pointer +# Text exception for missing input file +# Text exception on failure to convert the file +sub spin_pointer { + my ($self, $pointer, $output, $options_ref) = @_; + my $data_ref = $self->load_yaml_file($pointer, 'pointer'); + $data_ref->{options} //= {}; + + # Dispatch to the appropriate conversion function. + if ($data_ref->{format} eq 'markdown') { + $self->_spin_markdown($data_ref, $output); + } elsif ($data_ref->{format} eq 'pod') { + $self->_spin_pod($data_ref, $output); + } else { + die "$pointer: unknown output format $data_ref->{format}\n"; + } + return; +} + +############################################################################## +# Module return value and documentation +############################################################################## + +1; + +__END__ + +=for stopwords +Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT Kwalify sublicense unstyled +navbar + +=head1 NAME + +App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file + +=head1 SYNOPSIS + + use App::DocKnot::Spin::Pointer; + use App::DocKnot::Spin::Sitemap; + + my $sitemap = App::DocKnot::Spin::Sitemap->new('/input/.sitemap'); + my $pointer = App::DocKnot::Spin::Pointer->new({ + output => '/output', + sitemap => $sitemap, + }); + $pointer->spin_pointer('/input/file.spin', '/output/file.html'); + +=head1 REQUIREMENTS + +Perl 5.24 or later and the modules File::ShareDir, Kwalify, List::SomeUtils, +Pod::Thread, and YAML::XS, all of which are available from CPAN. + +=head1 DESCRIPTION + +The tree of input files for App::DocKnot::Spin may contain pointers to +external files in various formats. These files are in YAML format and end in +C<.spin>. This module processes those files and converts them to HTML and, if +so configured, adds the links to integrate the page with the rest of the site. + +For the details of the pointer file format, see L<POINTER FILES> below. + +=head1 CLASS METHODS + +=over 4 + +=item new(ARGS) + +Create a new App::DocKnot::Spin::Pointer object. A single converter object +can be used repeatedly to convert pointers in a tree of files. ARGS should +be a hash reference with one or more of the following keys, all of which are +optional: + +=over 4 + +=item output + +The path to the root of the output tree when converting a tree of files. This +will be used to calculate relative path names for generating inter-page links +using the provided C<sitemap> argument. If C<sitemap> is given, this option +should also always be given. + +=item sitemap + +An App::DocKnot::Spin::Sitemap object. This will be used to create inter-page +links. For inter-page links, the C<output> argument must also be provided. + +=item style-url + +The base URL for style sheets. A style sheet specified in a pointer file will +be considered to be relative to this URL and this URL will be prepended to it. +If this option is not given, the name of the style sheet will be used verbatim +as its URL, except with C<.css> appended. + +=item thread + +An App::DocKnot::Spin::Thread object, used for converting POD into HTML. It +should be configured with the same App::DocKnot::Spin::Sitemap object as the +C<sitemap> argument. + +=back + +=back + +=head1 INSTANCE METHODS + +=over 4 + +=item is_out_of_date(POINTER, OUTPUT) + +Returns true if OUTPUT is missing or if it was modified less recently than the +modification time of either POINTER or the underlying file that it points to. + +=item spin_pointer(POINTER, OUTPUT) + +Convert a single pointer file to HTML. POINTER is the path to the pointer +file, and OUTPUT is the path to where to write the output. + +=back + +=head1 POINTER FILES + +A pointer file is a YAML file ending in C<.spin> that points to the source +file for a generated HTML page and provides additional configuration for its +conversion. The valid keys for a pointer file are: + +=over 4 + +=item format + +The format of the source file. Supported values are C<markdown> and C<pod>. +Required. + +=item path + +The path to the source file. It may be relative, in which case it's relative +to the pointer file. Required. + +=item options + +Additional options that control the conversion to HTML. These will be +different for each supported format. + +C<markdown> has no supported options. + +The supported options for a format of C<pod> are: + +=over 4 + +=item contents + +Boolean saying whether to generate a table of contents. The default is false. + +=item navbar + +Boolean saying whether to generate a navigation bar at the top of the page. +The default is true. + +=back + +=item style + +The style sheet to use for the converted output. Optional. If not set, +converted C<markdown> output will be unstyled and converted C<pod> output will +use a style sheet named C<pod>. + +=item title + +The title of the converted page. Optional. If not set, the title will be +taken from the converted file in a format-specific way. For Markdown, the +title will be the contents of the first top-level heading. For POD, the title +will be taken from a NAME section formatted according to the conventions for +manual pages. + +=back + +=head1 AUTHOR + +Russ Allbery <rra@cpan.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2021 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=head1 SEE ALSO + +L<docknot(1)>, L<App::DocKnot::Spin>, L<App::DocKnot::Spin::Sitemap> + +This module is part of the App-DocKnot distribution. The current version of +DocKnot is available from CPAN, or directly from its web site at +L<https://www.eyrie.org/~eagle/software/docknot/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm index 7599426..2a7460a 100644 --- a/lib/App/DocKnot/Spin/RSS.pm +++ b/lib/App/DocKnot/Spin/RSS.pm @@ -9,7 +9,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::RSS 5.00; +package App::DocKnot::Spin::RSS 6.00; use 5.024; use autodie; @@ -17,7 +17,9 @@ use warnings; use App::DocKnot; use App::DocKnot::Spin::Thread; +use App::DocKnot::Util qw(print_checked print_fh); use Cwd qw(getcwd); +use Date::Language (); use Date::Parse qw(str2time); use File::Basename qw(fileparse); use Perl6::Slurp qw(slurp); @@ -27,30 +29,6 @@ use POSIX qw(strftime); # Utility functions ############################################################################## -# print with error checking. autodie unfortunately can't help us because -# print can't be prototyped and hence can't be overridden. -sub _print_checked { - my (@args) = @_; - print @args or croak('print failed'); - return; -} - -# print with error checking and an explicit file handle. autodie -# unfortunately can't help us because print can't be prototyped and hence -# can't be overridden. -# -# $fh - Output file handle -# $file - File name for error reporting -# @args - Remaining arguments to print -# -# Returns: undef -# Throws: Text exception on output failure -sub _print_fh { - my ($fh, $file, @args) = @_; - print {$fh} @args or croak("cannot write to $file: $!"); - return; -} - # Escapes &, <, and > characters for HTML or XML output. # # $string - Input string @@ -154,7 +132,7 @@ sub _relative_url { sub _spin_file { my ($self, $file) = @_; my $source = slurp($file); - my $cwd = getcwd(); + my $cwd = getcwd(); my (undef, $dir) = fileparse($file); chdir($dir); my $page = $self->{spin}->spin_thread($source); @@ -174,7 +152,7 @@ sub _spin_file { sub _read_rfc2822_file { my ($self, $file) = @_; my $key; - my @blocks = ({}); + my @blocks = ({}); my $current = $blocks[0]; # Parse the file. $key holds the last key seen, used to append @@ -405,15 +383,16 @@ sub _rss_output { # Determine the current date and latest publication date of all of the # entries, published in the obnoxious format used by RSS. + my $lang = Date::Language->new('English'); my $format = '%a, %d %b %Y %H:%M:%S %z'; - my $now = strftime($format, localtime()); + my $now = $lang->strftime($format, [localtime()]); my $latest = $now; if ($entries_ref->@*) { $latest = strftime($format, localtime($entries_ref->[0]{date})); } # Output the RSS header. - _print_fh($fh, $file, <<"EOC"); + print_fh($fh, $file, <<"EOC"); <?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> @@ -428,18 +407,18 @@ EOC if ($metadata_ref->{'rss-base'}) { my ($name) = fileparse($file); my $url = $metadata_ref->{'rss-base'} . $name; - _print_fh( + print_fh( $fh, $file, qq{ <atom:link href="$url" rel="self"\n}, qq{ type="application/rss+xml" />\n}, ); } - _print_fh($fh, $file, "\n"); + print_fh($fh, $file, "\n"); # Output each entry, formatting the contents of the entry as we go. for my $entry_ref ($entries_ref->@*) { - my $date = strftime($format, localtime($entry_ref->{date})); + my $date = $lang->strftime($format, [localtime($entry_ref->{date})]); my $title = _escape($entry_ref->{title}); my $description; if ($entry_ref->{description}) { @@ -468,7 +447,7 @@ EOC } # Output the entry. - _print_fh( + print_fh( $fh, $file, " <item>\n", @@ -484,7 +463,7 @@ EOC } # Close the RSS structure. - _print_fh($fh, $file, " </channel>\n</rss>\n"); + print_fh($fh, $file, " </channel>\n</rss>\n"); return; } @@ -503,9 +482,9 @@ sub _thread_output { # Page prefix. if ($metadata_ref->{'thread-prefix'}) { - _print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n"); + print_fh($fh, $file, $metadata_ref->{'thread-prefix'}, "\n"); } else { - _print_fh( + print_fh( $fh, $file, "\\heading[Recent Changes][indent]\n\n", @@ -520,13 +499,13 @@ sub _thread_output { # Put headings before each month. if (!$last_month || $month ne $last_month) { - _print_fh($fh, $file, "\\h2[$month]\n\n"); + print_fh($fh, $file, "\\h2[$month]\n\n"); $last_month = $month; } # Format each entry. my $date = strftime('%Y-%m-%d', localtime($entry_ref->{date})); - _print_fh( + print_fh( $fh, $file, "\\desc[$date \\entity[mdash]\n", @@ -536,11 +515,11 @@ sub _thread_output { my $description = $entry_ref->{description}; $description =~ s{ ^ }{ }xmsg; $description =~ s{ \\ }{\\\\}xmsg; - _print_fh($fh, $file, $description, "]\n\n"); + print_fh($fh, $file, $description, "]\n\n"); } # Print out the end of the page. - _print_fh($fh, $file, "\\signature\n"); + print_fh($fh, $file, "\\signature\n"); return; } @@ -649,14 +628,14 @@ sub _index_output { # Output the prefix. if ($metadata_ref->{'index-prefix'}) { - _print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n"); + print_fh($fh, $file, $metadata_ref->{'index-prefix'}, "\n"); } # Output each entry. for my $entry_ref ($entries_ref->@*) { my @time = localtime($entry_ref->{date}); my $date = strftime('%Y-%m-%d %H:%M', @time); - my $day = strftime('%Y-%m-%d', @time); + my $day = strftime('%Y-%m-%d', @time); # Get the text of the entry. my $text; @@ -679,7 +658,7 @@ sub _index_output { }{$1 . _relative_url($2, $metadata_ref->{'index-base'}) . ']' }xmsge; # Print out the entry. - _print_fh( + print_fh( $fh, $file, "\\h2[$day: $entry_ref->{title}]\n\n", @@ -692,9 +671,9 @@ sub _index_output { # Print out the end of the page. if ($metadata_ref->{'index-suffix'}) { - _print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n"); + print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n"); } - _print_fh($fh, $file, "\\signature\n"); + print_fh($fh, $file, "\\signature\n"); return; } @@ -764,7 +743,7 @@ sub generate { # Write the output. if ($format eq 'thread') { - _print_checked("Generating thread file $prettyfile\n"); + print_checked("Generating thread file $prettyfile\n"); open(my $fh, '>', $path); $self->_thread_output($fh, $path, $metadata_ref, \@entries); close($fh); @@ -772,7 +751,7 @@ sub generate { if (scalar(@entries) > $metadata_ref->{recent}) { splice(@entries, $metadata_ref->{recent}); } - _print_checked("Generating RSS file $prettyfile\n"); + print_checked("Generating RSS file $prettyfile\n"); open(my $fh, '>', $path); $self->_rss_output($fh, $path, $metadata_ref, \@entries); close($fh); @@ -780,7 +759,7 @@ sub generate { if (scalar(@entries) > $metadata_ref->{recent}) { splice(@entries, $metadata_ref->{recent}); } - _print_checked("Generating index file $prettyfile\n"); + print_checked("Generating index file $prettyfile\n"); open(my $fh, '>', $path); $self->_index_output($fh, $path, $metadata_ref, \@entries); close($fh); @@ -813,8 +792,9 @@ App::DocKnot::Spin::RSS - Generate RSS and thread from a feed description file =head1 REQUIREMENTS -Perl 5.006 or later and the modules Date::Parse (part of the TimeDate -distribution) and Perl6::Slurp, both of which are available from CPAN. +Perl 5.006 or later and the modules Date::Language, Date::Parse (both part of +the TimeDate distribution), List::SomeUtils, and Perl6::Slurp, both of which +are available from CPAN. =head1 DESCRIPTION diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm index 038cc9f..22fbdd5 100644 --- a/lib/App/DocKnot/Spin/Sitemap.pm +++ b/lib/App/DocKnot/Spin/Sitemap.pm @@ -12,7 +12,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Sitemap 5.00; +package App::DocKnot::Spin::Sitemap 6.00; use 5.024; use autodie; @@ -141,7 +141,7 @@ sub _escape { sub _relative { my ($origin, $dest) = @_; my @origin = split(qr{ / }xms, $origin, -1); - my @dest = split(qr{ / }xms, $dest, -1); + my @dest = split(qr{ / }xms, $dest, -1); # Remove the common prefix. while (@origin && @dest && $origin[0] eq $dest[0]) { @@ -207,11 +207,13 @@ sub new { # links maps partial URLs to a list of other partial URLs (previous, next, # and then the full upwards hierarchy to the top of the site) used for # interpage links. + #<<< my $self = { links => {}, pagedesc => {}, sitemap => [], }; + #>>> bless($self, $class); # Parse the file into the newly-created object. @@ -292,7 +294,7 @@ sub navbar { # Construct the bread crumbs for the page hierarchy. my @breadcrumbs = (" <td>\n"); - my $first = 1; + my $first = 1; for my $parent (reverse(@parents)) { my ($url, $desc) = $parent->@*; my $prefix = q{ } x 4; @@ -312,7 +314,6 @@ sub navbar { @breadcrumbs, $next_link, "</tr></table>\n", - "\n", ); } @@ -335,7 +336,7 @@ sub sitemap { # Open or close <ul> elements as needed by the indentation. if ($indent > $indents[-1]) { - push(@output, (q{ } x $indent) . "<ul>\n"); + push(@output, (q{ } x $indent) . "<ul>\n"); push(@indents, $indent); } else { while ($indent < $indents[-1]) { diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm index 492666d..bf58dc0 100644 --- a/lib/App/DocKnot/Spin/Thread.pm +++ b/lib/App/DocKnot/Spin/Thread.pm @@ -9,18 +9,20 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Thread 5.00; +package App::DocKnot::Spin::Thread 6.00; use 5.024; use autodie; use warnings; use App::DocKnot; +use App::DocKnot::Util qw(print_fh); use Cwd qw(getcwd realpath); use File::Basename qw(fileparse); -use File::Spec (); +use File::Spec (); use Git::Repository (); use Image::Size qw(html_imgsize); +use Path::Tiny qw(path); use Perl6::Slurp qw(slurp); use POSIX qw(strftime); use Text::Balanced qw(extract_bracketed); @@ -34,6 +36,7 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/'; # 1. Number of arguments or -1 to consume as many arguments as it can find. # 2. Name of the method to call with the arguments and (if wanted) format. # 3. Whether to look for a format in parens before the arguments. +#<<< my %COMMANDS = ( # name args method want_format block => [1, '_cmd_block', 1], @@ -80,6 +83,7 @@ my %COMMANDS = ( q{==} => [3, '_define_macro', 0], q{\\} => [0, '_literal', 0], ); +#>>> ############################################################################## # Input and output @@ -104,22 +108,6 @@ sub _read_file { return $text; } -# print with error checking and an explicit file handle. autodie -# unfortunately can't help us because print can't be prototyped and hence -# can't be overridden. -# -# $fh - Output file handle -# $file - File name for error reporting -# @args - Remaining arguments to print -# -# Returns: undef -# Throws: Text exception on output failure -sub _print_fh { - my ($fh, $file, @args) = @_; - print {$fh} @args or croak("cannot write to $file: $!"); - return; -} - # Sends something to the output file with special handling of whitespace for # more readable HTML output. # @@ -159,7 +147,7 @@ sub _output { } # Send the results to the output file. - _print_fh($self->{out_fh}, $self->{out_path}, $output); + print_fh($self->{out_fh}, $self->{out_path}, $output); return; } @@ -255,7 +243,7 @@ sub _paragraph { # Returns: Output to write to start the structure sub _border_start { my ($self, $border, $start, $end) = @_; - my $state = $self->{state}[-1]; + my $state = $self->{state}[-1]; my $output = q{}; # If we're at the top-level block structure or inside a structure other @@ -450,7 +438,7 @@ sub _expand { my ($blocktag, $output) = $self->$handler($format, @args); return ($output, $blocktag, $rest); } else { - my ($rest, @args) = $self->_extract($text, $args); + my ($rest, @args) = $self->_extract($text, $args); my ($blocktag, $output) = $self->$handler(@args); return ($output, $blocktag, $rest); } @@ -577,7 +565,7 @@ sub _parse_context { if ($blocktag) { if ($block && $paragraph ne q{}) { $output .= $border . $self->_paragraph($paragraph); - $border = q{}; + $border = q{}; $paragraph = q{}; } else { $output .= $space; @@ -648,25 +636,29 @@ sub _parse { # since thread may contain relative paths to files that the spinning process # needs to access. # -# $thread - Thread to spin -# $in_path - Input file path if any, used for error reporting -# $out_fh - Output file handle to which to write the HTML -# $out_path - Optional output file path for error reporting and page links +# $thread - Thread to spin +# $in_path - Input file path if any, used for error reporting +# $out_fh - Output file handle to which to write the HTML +# $out_path - Optional output file path for error reporting and page links +# $input_type - Optional one-word description of input type sub _parse_document { - my ($self, $thread, $in_path, $out_fh, $out_path) = @_; + my ($self, $thread, $in_path, $out_fh, $out_path, $input_type) = @_; # Parse the thread into paragraphs and reverse them to form a stack. my @input = reverse($self->_split_paragraphs($thread)); # Initialize object state for a new document. - $self->{input} = [[\@input, $in_path, 1]]; - $self->{macro} = {}; - $self->{out_fh} = $out_fh; - $self->{out_path} = $out_path // q{-}; - $self->{rss} = []; - $self->{space} = q{}; - $self->{state} = ['BLOCK']; - $self->{variable} = {}; + #<<< + $self->{input} = [[\@input, $in_path, 1]]; + $self->{input_type} = $input_type // 'thread'; + $self->{macro} = {}; + $self->{out_fh} = $out_fh; + $self->{out_path} = $out_path // q{-}; + $self->{rss} = []; + $self->{space} = q{}; + $self->{state} = ['BLOCK']; + $self->{variable} = {}; + #>>> # Parse the thread file a paragraph at a time. _split_paragraphs takes # care of ensuring that each paragraph contains the complete value of a @@ -688,7 +680,7 @@ sub _parse_document { } # Close open tags and print any deferred whitespace. - _print_fh($out_fh, $out_path, $self->_block_end(), $self->{space}); + print_fh($out_fh, $out_path, $self->_block_end(), $self->{space}); return; } @@ -735,13 +727,13 @@ sub _split_paragraphs { # Pull paragraphs off the text one by one. while ($text ne q{} && $text =~ s{ \A ( .*? (?: \n\n+ | \s*\z ) )}{}xms) { - my $para = $1; - my $open_count = ($para =~ tr{\[}{}); + my $para = $1; + my $open_count = ($para =~ tr{\[}{}); my $close_count = ($para =~ tr{\]}{}); while ($text ne q{} && $open_count > $close_count) { if ($text =~ s{ \A ( .*? (?: \n\n+ | \s*\z ) )}{}xms) { my $extra = $1; - $open_count += ($extra =~ tr{\[}{}); + $open_count += ($extra =~ tr{\[}{}); $close_count += ($extra =~ tr{\]}{}); $para .= $extra; } else { @@ -784,7 +776,7 @@ sub _block { # Close the tag. The tag may have contained attributes, which aren't # allowed in the closing tag. - $tag =~ s{ [ ] .* }{}xms; + $tag =~ s{ [ ] .* }{}xms; $output =~ s{ \s* \z }{</$tag>}xms; if ($format ne 'packed') { $output .= "\n"; @@ -930,6 +922,7 @@ sub _literal { return (0, q{\\}) } ############################################################################## # Basic inline commands. +#<<< sub _cmd_break { return (0, '<br />') } sub _cmd_bold { my ($self, @a) = @_; return $self->_inline('b', @a) } sub _cmd_cite { my ($self, @a) = @_; return $self->_inline('cite', @a) } @@ -942,6 +935,7 @@ sub _cmd_strong { my ($self, @a) = @_; return $self->_inline('strong', @a) } sub _cmd_sub { my ($self, @a) = @_; return $self->_inline('sub', @a) } sub _cmd_sup { my ($self, @a) = @_; return $self->_inline('sup', @a) } sub _cmd_under { my ($self, @a) = @_; return $self->_inline('u', @a) } +#>>> # The headings. sub _cmd_h1 { my ($self, @a) = @_; return $self->_heading(1, @a); } @@ -990,8 +984,8 @@ sub _cmd_desc { my ($self, $format, $heading, $text) = @_; $heading = $self->_parse($heading); my $format_attr = $self->_format_attr($format); - my $border = $self->_border_start('desc', "<dl>\n", "</dl>\n\n"); - my $initial = $border . "<dt$format_attr>" . $heading . "</dt>\n"; + my $border = $self->_border_start('desc', "<dl>\n", "</dl>\n\n"); + my $initial = $border . "<dt$format_attr>" . $heading . "</dt>\n"; return $self->_block('dd', $initial, $format, $text); } @@ -1099,7 +1093,7 @@ sub _cmd_heading { sub _cmd_image { my ($self, $format, $image, $text) = @_; $image = $self->_parse($image); - $text = $self->_parse($text); + $text = $self->_parse($text); # Determine the size attributes of the image if possible. my $size = -e $image ? q{ } . lc(html_imgsize($image)) : q{}; @@ -1117,7 +1111,7 @@ sub _cmd_include { $file = realpath($self->_parse($file)); # Read the thread, split it on paragraphs, and reverse it to make a stack. - my $thread = $self->_read_file($file); + my $thread = $self->_read_file($file); my @paragraphs = reverse($self->_split_paragraphs($thread)); # Add it to the file stack. @@ -1134,7 +1128,7 @@ sub _cmd_include { # $text - Anchor text sub _cmd_link { my ($self, $format, $url, $text) = @_; - $url = $self->_parse($url); + $url = $self->_parse($url); $text = $self->_parse($text); my $format_attr = $self->_format_attr($format); return (0, qq{<a href="$url"$format_attr>$text</a>}); @@ -1164,7 +1158,7 @@ sub _cmd_pre { sub _cmd_quote { my ($self, $format, $quote, $author, $cite) = @_; $author = $self->_parse($author); - $cite = $self->_parse($cite); + $cite = $self->_parse($cite); my $output = $self->_border_end() . q{<blockquote class="quote">}; # Parse the contents of the quote in a new block context. @@ -1238,7 +1232,7 @@ sub _cmd_release { # directly; the RSS feed information is used later in _cmd_heading. sub _cmd_rss { my ($self, $url, $title) = @_; - $url = $self->_parse($url); + $url = $self->_parse($url); $title = $self->_parse($title); push($self->{rss}->@*, [$url, $title]); return (1, q{}); @@ -1251,9 +1245,9 @@ sub _cmd_signature { my $source = $self->{input}[-1][1]; my $output = $self->_border_end(); - # If we're spinning from standard input, don't add any of the standard - # footer, just close the HTML tags. - if ($self->{input}[-1][1] eq q{-}) { + # If we're spinning from standard input to standard output, don't add any + # of the standard footer, just close the HTML tags. + if ($source eq q{-} && $self->{out_path} eq q{-}) { $output .= "</body>\n</html>\n"; return (1, $output); } @@ -1262,27 +1256,33 @@ sub _cmd_signature { if ($self->{sitemap} && $self->{output}) { my $page = $self->{out_path}; $page =~ s{ \A \Q$self->{output}\E }{}xms; - $output .= join(q{}, $self->{sitemap}->navbar($page)); + $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n"; } # Figure out the modification dates. Use the Git repository if available. - my $now = strftime('%Y-%m-%d', gmtime()); - my $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9])); + my $now = strftime('%Y-%m-%d', gmtime()); + my $modified = $now; + if ($source ne q{-}) { + $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9])); + } if ($self->{repository} && $self->{source}) { - my $repository = $self->{repository}; - $modified = $repository->run('log', '-1', '--format=%ct', $source); - if ($modified) { - $modified = strftime('%Y-%m-%d', gmtime($modified)); + if (path($self->{source})->subsumes(path($source))) { + my $repository = $self->{repository}; + $modified = $repository->run('log', '-1', '--format=%ct', $source); + if ($modified) { + $modified = strftime('%Y-%m-%d', gmtime($modified)); + } } } # Determine which template to use and substitute in the appropriate times. - $output .= "<address>\n" . q{ } x 4; + $output .= "<address>\n"; my $link = qq{<a href="$URL">spun</a>}; if ($modified eq $now) { - $output .= "Last modified and\n $link $modified\n"; + $output .= " Last modified and\n $link $modified\n"; } else { - $output .= "Last $link\n $now from thread modified $modified\n"; + $output .= " Last $link\n"; + $output .= " $now from $self->{input_type} modified $modified\n"; } # Close out the document. @@ -1306,7 +1306,7 @@ sub _cmd_size { # Format the size using SI units. my @suffixes = qw(K M G T); - my $suffix = q{}; + my $suffix = q{}; while ($size > 1024 && @suffixes) { $size /= 1024; $suffix = shift(@suffixes); @@ -1410,6 +1410,7 @@ sub new { } # Create and return the object. + #<<< my $self = { output => $args_ref->{output}, repository => $repository, @@ -1418,6 +1419,7 @@ sub new { style_url => $style_url, versions => $args_ref->{versions}, }; + #>>> bless($self, $class); return $self; } @@ -1453,12 +1455,12 @@ sub spin_thread_file { # ensure that relative file references resolve properly. if (defined($input)) { my $path = realpath($input) or die "cannot canonicalize $input: $!\n"; - $input = $path; + $input = $path; $thread = slurp($input); my (undef, $input_dir) = fileparse($input); chdir($input_dir); } else { - $input = q{-}; + $input = q{-}; $thread = slurp(\*STDIN); } @@ -1482,6 +1484,39 @@ sub spin_thread_file { return; } +# Convert thread to HTML and write it to the given output file. This is used +# when the thread isn't part of the input tree but instead is intermediate +# output from some other conversion process. +# +# $thread - Thread to spin +# $input - Original input file (for modification timestamps) +# $input_type - One-word description of input type for the page footer +# $output - Output file +# +# Returns: Resulting HTML +sub spin_thread_output { + my ($self, $thread, $input, $input_type, $output) = @_; + + # Open the output file. + my $out_fh; + if (defined($output)) { + my $path = realpath($output) + or die "cannot canonicalize $output: $!\n"; + $output = $path; + open($out_fh, '>', $output); + } else { + $output = q{-}; + open($out_fh, '>&', 'STDOUT'); + } + + # Do the work. + $self->_parse_document($thread, $input, $out_fh, $output, $input_type); + + # Clean up and restore the working directory. + close($out_fh); + return; +} + ############################################################################## # Module return value and documentation ############################################################################## @@ -1501,8 +1536,9 @@ App::DocKnot::Spin::Thread - Generate HTML from the macro language thread use App::DocKnot::Spin::Thread; + my $input = 'some thread'; my $thread = App::DocKnot::Spin::Thread->new(); - $thread->spin_file('/path/to/file.th', '/path/to/file.html'); + my $output = $thread->spin_thread($input); use App::DocKnot::Spin::Sitemap; use App::DocKnot::Spin::Versions; @@ -1515,12 +1551,15 @@ App::DocKnot::Spin::Thread - Generate HTML from the macro language thread sitemap => $sitemap, versions => $versions, }); - $thread->spin_file('/input/file.th', '/output/file.th'); + $thread->spin_thread_file('/input/file.th', '/output/file.html'); + $thread->spin_thread_output( + $input, '/path/to/file.pod', 'POD', '/output/file.html' + ); =head1 REQUIREMENTS -Perl 5.24 or later and the modules Git::Repository and Image::Size, both of -which are available from CPAN. +Perl 5.24 or later and the modules Git::Repository, Image::Size, +List::SomeUtils, and Path::Tiny, all of which are available from CPAN. =head1 DESCRIPTION @@ -1601,6 +1640,16 @@ If OUTPUT is omitted, App::DocKnot::Spin::Thread will not be able to obtain sitemap information even if a sitemap was provided and therefore will not add inter-page links. +=item spin_thread_output(THREAD, INPUT, TYPE[, OUTPUT]) + +Convert the given thread to HTML, writing the result to OUTPUT. If OUTPUT is +not given, write the results to standard output. This is like spin_thread() +but does use sitemap information and adds inter-page links. It should be used +when the thread input is the result of an intermediate conversion step of a +known input file. INPUT should be the full path to the original source file, +used for modification time information. TYPE should be set to a one-word +description of the format of the input file and is used for the page footer. + =back =head1 THREAD LANGUAGE diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm index b804b08..679b368 100644 --- a/lib/App/DocKnot/Spin/Versions.pm +++ b/lib/App/DocKnot/Spin/Versions.pm @@ -12,7 +12,7 @@ # Modules and declarations ############################################################################## -package App::DocKnot::Spin::Versions 5.00; +package App::DocKnot::Spin::Versions 6.00; use 5.024; use autodie; @@ -84,9 +84,9 @@ sub _read_data { if (!defined($time)) { die "invalid line $. in $path\n"; } - @depends = @files; + @depends = @files; $timestamp = _datetime_to_seconds($date, $time, $path); - $date = strftime('%Y-%m-%d', gmtime($timestamp)); + $date = strftime('%Y-%m-%d', gmtime($timestamp)); $self->{versions}{$package} = [$version, $date]; } @@ -120,7 +120,7 @@ sub new { # Create an empty object. my $self = { - depends => {}, + depends => {}, versions => {}, }; bless($self, $class); |