App-DocKnot
view release on metacpan or search on metacpan
lib/App/DocKnot/Generate.pm view on Meta::CPAN
# Default output files for specific templates.
my %DEFAULT_OUTPUT = (
'readme' => 'README',
'readme-md' => 'README.md',
);
##############################################################################
# Generator functions
##############################################################################
# The internal helper object methods in this section are generators. They
# return code references intended to be passed into Template Toolkit as code
# references so that they can be called inside templates, incorporating data
# from the App::DocKnot configuration or the package metadata.
# Returns code to center a line in $self->{width} characters given the text of
# the line. The returned code will take a line of text and return that line
# with leading whitespace added as required.
#
# Returns: Code reference to a closure that uses $self->{width} for width
sub _code_for_center {
my ($self) = @_;
my $center = sub {
my ($text) = @_;
my $space = $self->{width} - length($text);
if ($space <= 0) {
return $text;
} else {
return q{ } x int($space / 2) . $text;
}
};
return $center;
}
# Returns code that formats the copyright notices for the package. The
# resulting code reference takes two parameter, the indentation level and an
# optional prefix for each line, and wraps the copyright notices accordingly.
# They will be wrapped with a four-space outdent and kept within
# $self->{width} columns.
#
# $copyrights_ref - A reference to a list of anonymous hashes, each with keys:
# holder - The copyright holder for that copyright
# years - The years of that copyright
#
# Returns: Code reference to a closure taking an indent level and an optional
# prefix and returning the formatted copyright notice
sub _code_for_copyright {
my ($self, $copyrights_ref) = @_;
my $copyright = sub {
my ($indent, $lead) = @_;
my $prefix = ($lead // q{}) . q{ } x $indent;
my $notice;
for my $copyright ($copyrights_ref->@*) {
my $holder = $copyright->{holder};
my $years = $copyright->{years};
# Build the initial notice with the word copyright and the years.
my $text = 'Copyright ' . $copyright->{years};
local $Text::Wrap::columns = $self->{width} + 1;
local $Text::Wrap::unexpand = 0;
$text = wrap($prefix, $prefix . q{ } x 4, $text);
# See if the holder fits on the last line. If so, add it there;
# otherwise, add another line.
my $last_length;
if (rindex($text, "\n") == -1) {
$last_length = length($text);
} else {
$last_length = length($text) - rindex($text, "\n");
}
if ($last_length + length($holder) < $self->{width}) {
$text .= " $holder";
} else {
$text .= "\n" . $prefix . q{ } x 4 . $holder;
}
$notice .= $text . "\n";
}
chomp($notice);
return $notice;
};
return $copyright;
}
# Returns code to indent each line of a paragraph by a given number of spaces.
# This is constructed as a method returning a closure so that its behavior can
# be influenced by App::DocKnot configuration in the future, but it currently
# doesn't use any configuration. It takes the indentation and an optional
# prefix to put at the start of each line.
#
# Returns: Code reference to a closure
sub _code_for_indent {
my ($self) = @_;
my $indent = sub {
my ($text, $space, $lead) = @_;
$lead //= q{};
my @text = split(m{\n}xms, $text);
return join("\n", map { $lead . q{ } x $space . $_ } @text);
};
return $indent;
}
# Returns code that converts metadata text (which is assumed to be in
# Markdown) to text. This is not a complete Markdown formatter. It only
# supports the bits of markup that I've had some reason to use.
#
# This is constructed as a method returning a closure so that its behavior can
# be influenced by App::DocKnot configuration in the future, but it currently
# doesn't use any configuration.
#
# Returns: Code reference to a closure that takes a block of text and returns
# the converted text
sub _code_for_to_text {
my ($self) = @_;
my $to_text = sub {
my ($text) = @_;
# Remove triple backticks but escape all backticks inside them.
$text =~ s{ ``` \w* (\s .*?) ``` }{
my $text = $1;
$text =~ s{ [\`] }{``}xmsg;
lib/App/DocKnot/Generate.pm view on Meta::CPAN
# If this looks like thread commands or URLs, leave it alone.
if ($para =~ m{ \A \s* (?: \\ | \[\d+\] ) }xms) {
return $para;
}
# If this starts with a bullet, strip the bullet off, wrap the paragraph,
# and then add it back in.
if ($para =~ s{ \A (\s*) [*] (\s+) }{$1 $2}xms) {
my $offset = length($1);
$para = $self->_wrap_paragraph($para, { ignore_indent => 1 });
substr($para, $offset, 1, q{*});
return $para;
}
# If this starts with a number, strip the number off, wrap the paragraph,
# and then add it back in.
if ($para =~ s{\A (\s*) (\d+[.]) (\s+)}{$1 . q{ } x length($2) . $3}xmse) {
my $offset = length($1);
my $number = $2;
$para = $self->_wrap_paragraph($para, { ignore_indent => 1 });
substr($para, $offset, length($number), $number);
return $para;
}
# If this looks like a Markdown block quote, strip trailing whitespace,
# remove the leading indentation marks, wrap the paragraph, and then put
# them back.
## no critic (RegularExpressions::ProhibitCaptureWithoutTest)
if ($para =~ m{ \A (\s*) > \s }xms) {
$para =~ s{ [ ]+ \n }{\n}xmsg;
$para =~ s{ ^ (\s*) > (\s) }{$1 $2}xmsg;
my $offset = length($1);
$para = $self->_wrap_paragraph($para, { ignore_indent => 1 });
$para =~ s{ ^ (\s{$offset}) \s }{$1>}xmsg;
return $para;
}
## use critic
# If this looks like a bunch of short lines, leave it alone.
if ($para =~ m{ \A (?: \Q$indent\E [^\n]{1,45} \n ){3,} }xms) {
return $para;
}
# If this paragraph is not consistently indented, leave it alone.
if ($para !~ m{ \A (?: \Q$indent\E \S[^\n]+ \n )+ \z }xms) {
return $para;
}
# Strip the indent from each line.
$para =~ s{ (?: \A | (?<=\n) ) \Q$indent\E }{}xmsg;
# Remove any existing newlines, preserving two spaces after periods.
$para =~ s{ [.] ([)\"]?) \n (\S) }{.$1 $2}xmsg;
$para =~ s{ \n(\S) }{ $1}xmsg;
# Force locally correct configuration of Text::Wrap.
local $Text::Wrap::break = qr{\s+}xms;
local $Text::Wrap::columns = $self->{width} + 1;
local $Text::Wrap::huge = 'overflow';
local $Text::Wrap::unexpand = 0;
# Do the wrapping. This modifies @paragraphs in place.
$para = wrap($indent, $indent, $para);
# Strip any trailing whitespace, since some gets left behind after periods
# by Text::Wrap.
$para =~ s{ [ ]+ \n }{\n}xmsg;
# All done.
return $para;
}
# Word-wrap a block of text. This requires some annoying heuristics, but the
# alternative is to try to get the template to always produce correctly
# wrapped results, which is far harder.
#
# $text - The text to wrap
#
# Returns: The wrapped text
sub _wrap {
my ($self, $text) = @_;
# First, break the text up into paragraphs. (This will also turn more
# than two consecutive newlines into just two newlines.)
my @paragraphs = split(m{ \n(?:[ ]*\n)+ }xms, $text);
# Add back the trailing newlines at the end of each paragraph.
@paragraphs = map { $_ . "\n" } @paragraphs;
# Wrap all of the paragraphs. This modifies @paragraphs in place.
for my $paragraph (@paragraphs) {
$paragraph = $self->_wrap_paragraph($paragraph);
}
# Glue the paragraphs back together and return the result. Because the
# last newline won't get stripped by the split above, we have to strip an
# extra newline from the end of the file.
my $result = join("\n", @paragraphs);
$result =~ s{ \n+ \z }{\n}xms;
return $result;
}
##############################################################################
# Public interface
##############################################################################
# Create a new App::DocKnot::Generate object, which will be used for
# subsequent calls.
#
# $args - Anonymous hash of arguments with the following keys:
# metadata - Path to the directory containing package metadata
# width - Line length at which to wrap output files
#
# Returns: Newly created object
# Throws: Text exceptions on invalid metadata directory path
sub new {
my ($class, $args_ref) = @_;
# Create the config reader.
my %config_args;
( run in 0.824 second using v1.01-cache-2.11-cpan-5623c5533a1 )