summaryrefslogtreecommitdiff
path: root/lib/App/DocKnot/Spin/Pointer.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/App/DocKnot/Spin/Pointer.pm')
-rw-r--r--lib/App/DocKnot/Spin/Pointer.pm428
1 files changed, 428 insertions, 0 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