| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # Generate human-readable documentation from package metadata. | 
| 2 |  |  |  |  |  |  | # | 
| 3 |  |  |  |  |  |  | # This is the implementation of the docknot generate command, which uses | 
| 4 |  |  |  |  |  |  | # templates to support generation of various documentation files based on | 
| 5 |  |  |  |  |  |  | # package metadata. | 
| 6 |  |  |  |  |  |  | # | 
| 7 |  |  |  |  |  |  | # SPDX-License-Identifier: MIT | 
| 8 |  |  |  |  |  |  |  | 
| 9 |  |  |  |  |  |  | ############################################################################## | 
| 10 |  |  |  |  |  |  | # Modules and declarations | 
| 11 |  |  |  |  |  |  | ############################################################################## | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | package App::DocKnot::Generate 3.01; | 
| 14 |  |  |  |  |  |  |  | 
| 15 | 5 |  |  | 5 |  | 290517 | use 5.024; | 
|  | 5 |  |  |  |  | 39 |  | 
| 16 | 5 |  |  | 5 |  | 23 | use autodie; | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 34 |  | 
| 17 | 5 |  |  | 5 |  | 22255 | use parent qw(App::DocKnot); | 
|  | 5 |  |  |  |  | 10 |  | 
|  | 5 |  |  |  |  | 35 |  | 
| 18 | 5 |  |  | 5 |  | 234 | use warnings; | 
|  | 5 |  |  |  |  | 9 |  | 
|  | 5 |  |  |  |  | 143 |  | 
| 19 |  |  |  |  |  |  |  | 
| 20 | 5 |  |  | 5 |  | 1193 | use App::DocKnot::Config; | 
|  | 5 |  |  |  |  | 9 |  | 
|  | 5 |  |  |  |  | 164 |  | 
| 21 | 5 |  |  | 5 |  | 32 | use Carp qw(croak); | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 213 |  | 
| 22 | 5 |  |  | 5 |  | 2250 | use Template; | 
|  | 5 |  |  |  |  | 78190 |  | 
|  | 5 |  |  |  |  | 159 |  | 
| 23 | 5 |  |  | 5 |  | 1989 | use Text::Wrap qw(wrap); | 
|  | 5 |  |  |  |  | 10799 |  | 
|  | 5 |  |  |  |  | 12606 |  | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  | # Default output files for specific templates. | 
| 26 |  |  |  |  |  |  | my %DEFAULT_OUTPUT = ( | 
| 27 |  |  |  |  |  |  | 'readme'    => 'README', | 
| 28 |  |  |  |  |  |  | 'readme-md' => 'README.md', | 
| 29 |  |  |  |  |  |  | ); | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | ############################################################################## | 
| 32 |  |  |  |  |  |  | # Generator functions | 
| 33 |  |  |  |  |  |  | ############################################################################## | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | # The internal helper object methods in this section are generators.  They | 
| 36 |  |  |  |  |  |  | # return code references intended to be passed into Template Toolkit as code | 
| 37 |  |  |  |  |  |  | # references so that they can be called inside templates, incorporating data | 
| 38 |  |  |  |  |  |  | # from the App::DocKnot configuration or the package metadata. | 
| 39 |  |  |  |  |  |  |  | 
| 40 |  |  |  |  |  |  | # Returns code to center a line in $self->{width} characters given the text of | 
| 41 |  |  |  |  |  |  | # the line.  The returned code will take a line of text and return that line | 
| 42 |  |  |  |  |  |  | # with leading whitespace added as required. | 
| 43 |  |  |  |  |  |  | # | 
| 44 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 45 |  |  |  |  |  |  | # | 
| 46 |  |  |  |  |  |  | # Returns: Code reference to a closure that uses $self->{width} for width | 
| 47 |  |  |  |  |  |  | sub _code_for_center { | 
| 48 | 34 |  |  | 34 |  | 79 | my ($self) = @_; | 
| 49 |  |  |  |  |  |  | my $center = sub { | 
| 50 | 42 |  |  | 42 |  | 95 | my ($text) = @_; | 
| 51 | 42 |  |  |  |  | 97 | my $space = $self->{width} - length($text); | 
| 52 | 42 | 50 |  |  |  | 95 | if ($space <= 0) { | 
| 53 | 0 |  |  |  |  | 0 | return $text; | 
| 54 |  |  |  |  |  |  | } else { | 
| 55 | 42 |  |  |  |  | 225 | return q{ } x int($space / 2) . $text; | 
| 56 |  |  |  |  |  |  | } | 
| 57 | 34 |  |  |  |  | 170 | }; | 
| 58 | 34 |  |  |  |  | 99 | return $center; | 
| 59 |  |  |  |  |  |  | } | 
| 60 |  |  |  |  |  |  |  | 
| 61 |  |  |  |  |  |  | # Returns code that formats the copyright notices for the package.  The | 
| 62 |  |  |  |  |  |  | # resulting code reference takes two parameter, the indentation level and an | 
| 63 |  |  |  |  |  |  | # optional prefix for each line, and wraps the copyright notices accordingly. | 
| 64 |  |  |  |  |  |  | # They will be wrapped with a four-space outdent and kept within | 
| 65 |  |  |  |  |  |  | # $self->{width} columns. | 
| 66 |  |  |  |  |  |  | # | 
| 67 |  |  |  |  |  |  | # $self           - The App::DocKnot::Generate object | 
| 68 |  |  |  |  |  |  | # $copyrights_ref - A reference to a list of anonymous hashes, each with keys: | 
| 69 |  |  |  |  |  |  | #   holder - The copyright holder for that copyright | 
| 70 |  |  |  |  |  |  | #   years  - The years of that copyright | 
| 71 |  |  |  |  |  |  | # | 
| 72 |  |  |  |  |  |  | # Returns: Code reference to a closure taking an indent level and an optional | 
| 73 |  |  |  |  |  |  | #          prefix and returning the formatted copyright notice | 
| 74 |  |  |  |  |  |  | sub _code_for_copyright { | 
| 75 | 34 |  |  | 34 |  | 91 | my ($self, $copyrights_ref) = @_; | 
| 76 |  |  |  |  |  |  | my $copyright = sub { | 
| 77 | 14 |  |  | 14 |  | 44 | my ($indent, $lead) = @_; | 
| 78 | 14 |  | 50 |  |  | 92 | my $prefix = ($lead // q{}) . q{ } x $indent; | 
| 79 | 14 |  |  |  |  | 29 | my $notice; | 
| 80 | 14 |  |  |  |  | 52 | for my $copyright ($copyrights_ref->@*) { | 
| 81 | 24 |  |  |  |  | 51 | my $holder = $copyright->{holder}; | 
| 82 | 24 |  |  |  |  | 39 | my $years  = $copyright->{years}; | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | # Build the initial notice with the word copyright and the years. | 
| 85 | 24 |  |  |  |  | 57 | my $text = 'Copyright ' . $copyright->{years}; | 
| 86 | 24 |  |  |  |  | 53 | local $Text::Wrap::columns  = $self->{width} + 1; | 
| 87 | 24 |  |  |  |  | 37 | local $Text::Wrap::unexpand = 0; | 
| 88 | 24 |  |  |  |  | 105 | $text = wrap($prefix, $prefix . q{ } x 4, $text); | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  | # See if the holder fits on the last line.  If so, add it there; | 
| 91 |  |  |  |  |  |  | # otherwise, add another line. | 
| 92 | 24 |  |  |  |  | 3174 | my $last_length; | 
| 93 | 24 | 50 |  |  |  | 86 | if (rindex($text, "\n") == -1) { | 
| 94 | 24 |  |  |  |  | 36 | $last_length = length($text); | 
| 95 |  |  |  |  |  |  | } else { | 
| 96 | 0 |  |  |  |  | 0 | $last_length = length($text) - rindex($text, "\n"); | 
| 97 |  |  |  |  |  |  | } | 
| 98 | 24 | 100 |  |  |  | 71 | if ($last_length + length($holder) < $self->{width}) { | 
| 99 | 17 |  |  |  |  | 46 | $text .= " $holder"; | 
| 100 |  |  |  |  |  |  | } else { | 
| 101 | 7 |  |  |  |  | 23 | $text .= "\n" . $prefix . q{ } x 4 . $holder; | 
| 102 |  |  |  |  |  |  | } | 
| 103 | 24 |  |  |  |  | 90 | $notice .= $text . "\n"; | 
| 104 |  |  |  |  |  |  | } | 
| 105 | 14 |  |  |  |  | 39 | chomp($notice); | 
| 106 | 14 |  |  |  |  | 53 | return $notice; | 
| 107 | 34 |  |  |  |  | 168 | }; | 
| 108 | 34 |  |  |  |  | 91 | return $copyright; | 
| 109 |  |  |  |  |  |  | } | 
| 110 |  |  |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | # Returns code to indent each line of a paragraph by a given number of spaces. | 
| 112 |  |  |  |  |  |  | # This is constructed as a method returning a closure so that its behavior can | 
| 113 |  |  |  |  |  |  | # be influenced by App::DocKnot configuration in the future, but it currently | 
| 114 |  |  |  |  |  |  | # doesn't use any configuration.  It takes the indentation and an optional | 
| 115 |  |  |  |  |  |  | # prefix to put at the start of each line. | 
| 116 |  |  |  |  |  |  | # | 
| 117 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 118 |  |  |  |  |  |  | # | 
| 119 |  |  |  |  |  |  | # Returns: Code reference to a closure | 
| 120 |  |  |  |  |  |  | sub _code_for_indent { | 
| 121 | 34 |  |  | 34 |  | 131 | my ($self) = @_; | 
| 122 |  |  |  |  |  |  | my $indent = sub { | 
| 123 | 122 |  |  | 122 |  | 274 | my ($text, $space, $lead) = @_; | 
| 124 | 122 |  | 100 |  |  | 460 | $lead //= q{}; | 
| 125 | 122 |  |  |  |  | 930 | my @text = split(m{\n}xms, $text); | 
| 126 | 122 |  |  |  |  | 255 | return join("\n", map { $lead . q{ } x $space . $_ } @text); | 
|  | 2562 |  |  |  |  | 5338 |  | 
| 127 | 34 |  |  |  |  | 124 | }; | 
| 128 | 34 |  |  |  |  | 80 | return $indent; | 
| 129 |  |  |  |  |  |  | } | 
| 130 |  |  |  |  |  |  |  | 
| 131 |  |  |  |  |  |  | # Returns code that converts metadata text (which is assumed to be in | 
| 132 |  |  |  |  |  |  | # Markdown) to text.  This is not a complete Markdown formatter.  It only | 
| 133 |  |  |  |  |  |  | # supports the bits of markup that I've had some reason to use. | 
| 134 |  |  |  |  |  |  | # | 
| 135 |  |  |  |  |  |  | # This is constructed as a method returning a closure so that its behavior can | 
| 136 |  |  |  |  |  |  | # be influenced by App::DocKnot configuration in the future, but it currently | 
| 137 |  |  |  |  |  |  | # doesn't use any configuration. | 
| 138 |  |  |  |  |  |  | # | 
| 139 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 140 |  |  |  |  |  |  | # | 
| 141 |  |  |  |  |  |  | # Returns: Code reference to a closure that takes a block of text and returns | 
| 142 |  |  |  |  |  |  | #          the coverted text | 
| 143 |  |  |  |  |  |  | sub _code_for_to_text { | 
| 144 | 34 |  |  | 34 |  | 76 | my ($self) = @_; | 
| 145 |  |  |  |  |  |  | my $to_text = sub { | 
| 146 | 79 |  |  | 79 |  | 176 | my ($text) = @_; | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | # Remove triple backticks but escape all backticks inside them. | 
| 149 | 79 |  |  |  |  | 260 | $text =~ s{ ``` \w* (\s .*?) ``` }{ | 
| 150 | 35 |  |  |  |  | 76 | my $text = $1; | 
| 151 | 35 |  |  |  |  | 47 | $text =~ s{ [\`] }{``}xmsg; | 
| 152 | 35 |  |  |  |  | 153 | $text; | 
| 153 |  |  |  |  |  |  | }xmsge; | 
| 154 |  |  |  |  |  |  |  | 
| 155 |  |  |  |  |  |  | # Remove backticks, but don't look at things starting with doubled | 
| 156 |  |  |  |  |  |  | # backticks. | 
| 157 | 79 |  |  |  |  | 721 | $text =~ s{ (? | 
| 158 |  |  |  |  |  |  |  | 
| 159 |  |  |  |  |  |  | # Undo backtick escaping. | 
| 160 | 79 |  |  |  |  | 143 | $text =~ s{ `` }{\`}xmsg; | 
| 161 |  |  |  |  |  |  |  | 
| 162 |  |  |  |  |  |  | # Rewrite quoted paragraphs to have four spaces of additional | 
| 163 |  |  |  |  |  |  | # indentation. | 
| 164 | 79 |  |  |  |  | 122 | $text =~ s{ | 
| 165 |  |  |  |  |  |  | \n \n               # start of paragraph | 
| 166 |  |  |  |  |  |  | (                   # start of the text | 
| 167 |  |  |  |  |  |  | (> \s+)           #   quote mark on first line | 
| 168 |  |  |  |  |  |  | \S [^\n]* \n      #   first line | 
| 169 |  |  |  |  |  |  | (?:               #   all subsequent lines | 
| 170 |  |  |  |  |  |  | \2 \S [^\n]* \n #     start with the same prefix | 
| 171 |  |  |  |  |  |  | )*                #   any number of subsequent lines | 
| 172 |  |  |  |  |  |  | )                   # end of the text | 
| 173 |  |  |  |  |  |  | }{ | 
| 174 | 1 |  |  |  |  | 5 | my ($text, $prefix) = ($1, $2); | 
| 175 | 1 |  |  |  |  | 20 | $text =~ s{ ^ \Q$prefix\E }{  }xmsg; | 
| 176 | 1 |  |  |  |  | 6 | "\n\n" . $text; | 
| 177 |  |  |  |  |  |  | }xmsge; | 
| 178 |  |  |  |  |  |  |  | 
| 179 |  |  |  |  |  |  | # For each paragraph, remove URLs from all links, replacing them with | 
| 180 |  |  |  |  |  |  | # numeric references, and accumulate the mapping of numbers to URLs in | 
| 181 |  |  |  |  |  |  | # %urls.  Then, add to the end of the paragraph the references and | 
| 182 |  |  |  |  |  |  | # URLs. | 
| 183 | 79 |  |  |  |  | 105 | my $ref        = 1; | 
| 184 | 79 |  |  |  |  | 312 | my @paragraphs = split(m{ \n\n }xms, $text); | 
| 185 | 79 |  |  |  |  | 143 | for my $para (@paragraphs) { | 
| 186 | 362 |  |  |  |  | 418 | my %urls; | 
| 187 | 362 |  |  |  |  | 651 | while ($para =~ s{ \[ ([^\]]+) \] [(] (\S+) [)] }{$1 [$ref]}xms) { | 
| 188 | 12 |  |  |  |  | 41 | $urls{$ref} = $2; | 
| 189 | 12 |  |  |  |  | 38 | $ref++; | 
| 190 |  |  |  |  |  |  | } | 
| 191 | 362 | 100 |  |  |  | 583 | if (%urls) { | 
| 192 | 10 |  |  |  |  | 34 | my @refs = map { "[$_] $urls{$_}" } sort { $a <=> $b } | 
|  | 12 |  |  |  |  | 46 |  | 
|  | 2 |  |  |  |  | 8 |  | 
| 193 |  |  |  |  |  |  | keys(%urls); | 
| 194 | 10 |  |  |  |  | 46 | $para .= "\n\n" . join("\n", q{}, @refs, q{}); | 
| 195 |  |  |  |  |  |  | } | 
| 196 |  |  |  |  |  |  | } | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | # Rejoin the paragraphs and return the result. | 
| 199 | 79 |  |  |  |  | 792 | return join("\n\n", @paragraphs); | 
| 200 | 34 |  |  |  |  | 227 | }; | 
| 201 | 34 |  |  |  |  | 79 | return $to_text; | 
| 202 |  |  |  |  |  |  | } | 
| 203 |  |  |  |  |  |  |  | 
| 204 |  |  |  |  |  |  | # Returns code that converts metadata text (which is assumed to be in | 
| 205 |  |  |  |  |  |  | # Markdown) to thread.  This is not a complete Markdown formatter.  It only | 
| 206 |  |  |  |  |  |  | # supports the bits of markup that I've had some reason to use. | 
| 207 |  |  |  |  |  |  | # | 
| 208 |  |  |  |  |  |  | # This is constructed as a method returning a closure so that its behavior can | 
| 209 |  |  |  |  |  |  | # be influenced by App::DocKnot configuration in the future, but it currently | 
| 210 |  |  |  |  |  |  | # doesn't use any configuration. | 
| 211 |  |  |  |  |  |  | # | 
| 212 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 213 |  |  |  |  |  |  | # | 
| 214 |  |  |  |  |  |  | # Returns: Code reference to a closure that takes a block of text and returns | 
| 215 |  |  |  |  |  |  | #          the coverted thread | 
| 216 |  |  |  |  |  |  | sub _code_for_to_thread { | 
| 217 | 34 |  |  | 34 |  | 91 | my ($self) = @_; | 
| 218 |  |  |  |  |  |  | my $to_thread = sub { | 
| 219 | 39 |  |  | 39 |  | 80 | my ($text) = @_; | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | # Escape all backslashes. | 
| 222 | 39 |  |  |  |  | 76 | $text =~ s{ \\ }{\\\\}xmsg; | 
| 223 |  |  |  |  |  |  |  | 
| 224 |  |  |  |  |  |  | # Rewrite triple backticks to \pre blocks and escape backticks inside | 
| 225 |  |  |  |  |  |  | # them so that they're not turned into \code blocks. | 
| 226 | 39 |  |  |  |  | 74 | $text =~ s{ ``` \w* (\s .*?) ``` }{ | 
| 227 | 0 |  |  |  |  | 0 | my $text = $1; | 
| 228 | 0 |  |  |  |  | 0 | $text =~ s{ [\`] }{``}xmsg; | 
| 229 | 0 |  |  |  |  | 0 | '\pre[' . $1 . ']'; | 
| 230 |  |  |  |  |  |  | }xmsge; | 
| 231 |  |  |  |  |  |  |  | 
| 232 |  |  |  |  |  |  | # Rewrite backticks to \code blocks. | 
| 233 | 39 |  |  |  |  | 133 | $text =~ s{ ` ([^\`]+) ` }{\\code[$1]}xmsg; | 
| 234 |  |  |  |  |  |  |  | 
| 235 |  |  |  |  |  |  | # Undo backtick escaping. | 
| 236 | 39 |  |  |  |  | 59 | $text =~ s{ `` }{\`}xmsg; | 
| 237 |  |  |  |  |  |  |  | 
| 238 |  |  |  |  |  |  | # Rewrite all Markdown links into thread syntax. | 
| 239 | 39 |  |  |  |  | 128 | $text =~ s{ \[ ([^\]]+) \] [(] (\S+) [)] }{\\link[$2][$1]}xmsg; | 
| 240 |  |  |  |  |  |  |  | 
| 241 |  |  |  |  |  |  | # Rewrite long bullets.  This is quite tricky since we have to grab | 
| 242 |  |  |  |  |  |  | # every line from the first bulleted one to the point where the | 
| 243 |  |  |  |  |  |  | # indentation stops. | 
| 244 | 39 |  |  |  |  | 107 | $text =~ s{ | 
| 245 |  |  |  |  |  |  | (                   # capture whole contents | 
| 246 |  |  |  |  |  |  | ^ (\s*)         #   indent before bullet | 
| 247 |  |  |  |  |  |  | [*] (\s+)       #   bullet and following indent | 
| 248 |  |  |  |  |  |  | [^\n]+ \n       #   rest of line | 
| 249 |  |  |  |  |  |  | (?: \s* \n )*   #   optional blank lines | 
| 250 |  |  |  |  |  |  | (\2 [ ] \3)     #   matching indent | 
| 251 |  |  |  |  |  |  | [^\n]+ \n       #   rest of line | 
| 252 |  |  |  |  |  |  | (?:             #   one or more of | 
| 253 |  |  |  |  |  |  | \4          #       matching indent | 
| 254 |  |  |  |  |  |  | [^\n]+ \n   #       rest of line | 
| 255 |  |  |  |  |  |  | |               #   or | 
| 256 |  |  |  |  |  |  | \s* \n      #       blank lines | 
| 257 |  |  |  |  |  |  | )+              #   end of indented block | 
| 258 |  |  |  |  |  |  | )                   # full bullet with leading bullet | 
| 259 |  |  |  |  |  |  | }{ | 
| 260 | 3 |  |  |  |  | 7 | my $text = $1; | 
| 261 | 3 |  |  |  |  | 8 | $text =~ s{ [*] }{ }xms; | 
| 262 | 3 |  |  |  |  | 14 | "\\bullet[\n\n" . $text . "\n]\n"; | 
| 263 |  |  |  |  |  |  | }xmsge; | 
| 264 |  |  |  |  |  |  |  | 
| 265 |  |  |  |  |  |  | # Do the same thing, but with numbered lists.  This doesn't handle | 
| 266 |  |  |  |  |  |  | # numbers larger than 9 currently, since that requires massaging the | 
| 267 |  |  |  |  |  |  | # spacing. | 
| 268 | 39 |  |  |  |  | 200 | $text =~ s{ | 
| 269 |  |  |  |  |  |  | (                   # capture whole contents | 
| 270 |  |  |  |  |  |  | ^ (\s*)         #   indent before number | 
| 271 |  |  |  |  |  |  | \d [.] (\s+)    #   number and following indent | 
| 272 |  |  |  |  |  |  | [^\n]+ \n       #   rest of line | 
| 273 |  |  |  |  |  |  | (?: \s* \n )*   #   optional blank lines | 
| 274 |  |  |  |  |  |  | (\2 [ ][ ] \3)  #   matching indent | 
| 275 |  |  |  |  |  |  | [^\n]+ \n       #   rest of line | 
| 276 |  |  |  |  |  |  | (?:             #   one or more of | 
| 277 |  |  |  |  |  |  | \4          #       matching indent | 
| 278 |  |  |  |  |  |  | [^\n]+ \n   #       rest of line | 
| 279 |  |  |  |  |  |  | |               #   or | 
| 280 |  |  |  |  |  |  | \s* \n      #       blank lines | 
| 281 |  |  |  |  |  |  | )+              #   end of indented block | 
| 282 |  |  |  |  |  |  | )                   # full bullet with leading bullet | 
| 283 |  |  |  |  |  |  | }{ | 
| 284 | 3 |  |  |  |  | 8 | my $text = $1; | 
| 285 | 3 |  |  |  |  | 13 | $text =~ s{ \A (\s*) \d [.] }{$1  }xms; | 
| 286 | 3 |  |  |  |  | 18 | "\\number[\n\n" . $text . "\n]\n\n"; | 
| 287 |  |  |  |  |  |  | }xmsge; | 
| 288 |  |  |  |  |  |  |  | 
| 289 |  |  |  |  |  |  | # Rewrite compact bulleted lists. | 
| 290 | 39 |  |  |  |  | 111 | $text =~ s{ \n ( (?: \s* [*] \s+ [^\n]+ \s* \n ){2,} ) }{ | 
| 291 | 5 |  |  |  |  | 16 | my $list = $1; | 
| 292 | 5 |  |  |  |  | 59 | $list =~ s{ \n [*] \s+ ([^\n]+) }{\n\\bullet(packed)[$1]}xmsg; | 
| 293 | 5 |  |  |  |  | 25 | "\n" . $list; | 
| 294 |  |  |  |  |  |  | }xmsge; | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | # Done.  Return the results. | 
| 297 | 39 |  |  |  |  | 170 | return $text; | 
| 298 | 34 |  |  |  |  | 153 | }; | 
| 299 | 34 |  |  |  |  | 76 | return $to_thread; | 
| 300 |  |  |  |  |  |  | } | 
| 301 |  |  |  |  |  |  |  | 
| 302 |  |  |  |  |  |  | ############################################################################## | 
| 303 |  |  |  |  |  |  | # Helper methods | 
| 304 |  |  |  |  |  |  | ############################################################################## | 
| 305 |  |  |  |  |  |  |  | 
| 306 |  |  |  |  |  |  | # Word-wrap a paragraph of text.  This is a helper function for _wrap, mostly | 
| 307 |  |  |  |  |  |  | # so that it can be invoked recursively to wrap bulleted paragraphs. | 
| 308 |  |  |  |  |  |  | # | 
| 309 |  |  |  |  |  |  | # If the paragraph looks like regular text, which means indented by two or | 
| 310 |  |  |  |  |  |  | # four spaces and consistently on each line, remove the indentation and then | 
| 311 |  |  |  |  |  |  | # add it back in while wrapping the text. | 
| 312 |  |  |  |  |  |  | # | 
| 313 |  |  |  |  |  |  | # $self      - The App::DocKnot::Generate object | 
| 314 |  |  |  |  |  |  | # $paragraph - A paragraph of text to wrap | 
| 315 |  |  |  |  |  |  | # | 
| 316 |  |  |  |  |  |  | # Returns: The wrapped paragraph | 
| 317 |  |  |  |  |  |  | sub _wrap_paragraph { | 
| 318 | 2165 |  |  | 2165 |  | 3161 | my ($self, $paragraph) = @_; | 
| 319 | 2165 |  |  |  |  | 5281 | my ($indent) = ($paragraph =~ m{ \A ([ ]*) \S }xms); | 
| 320 |  |  |  |  |  |  |  | 
| 321 |  |  |  |  |  |  | # If the indent is longer than four characters, leave it alone. | 
| 322 | 2165 | 100 |  |  |  | 4118 | if (length($indent) > 4) { | 
| 323 | 167 |  |  |  |  | 353 | return $paragraph; | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  |  | 
| 326 |  |  |  |  |  |  | # If this looks like thread commands or URLs, leave it alone. | 
| 327 | 1998 | 100 |  |  |  | 3990 | if ($paragraph =~ m{ \A \s* (?: \\ | \[\d+\] ) }xms) { | 
| 328 | 219 |  |  |  |  | 460 | return $paragraph; | 
| 329 |  |  |  |  |  |  | } | 
| 330 |  |  |  |  |  |  |  | 
| 331 |  |  |  |  |  |  | # If this starts with a bullet, strip the bullet off, wrap the paragaraph, | 
| 332 |  |  |  |  |  |  | # and then add it back in. | 
| 333 | 1779 | 100 |  |  |  | 3752 | if ($paragraph =~ s{ \A (\s*) [*] (\s+) }{$1 $2}xms) { | 
| 334 | 80 |  |  |  |  | 151 | my $offset = length($1); | 
| 335 | 80 |  |  |  |  | 179 | $paragraph = $self->_wrap_paragraph($paragraph); | 
| 336 | 80 |  |  |  |  | 196 | substr($paragraph, $offset, 1, q{*}); | 
| 337 | 80 |  |  |  |  | 207 | return $paragraph; | 
| 338 |  |  |  |  |  |  | } | 
| 339 |  |  |  |  |  |  |  | 
| 340 |  |  |  |  |  |  | # If this looks like a Markdown block quote leave it alone, but strip | 
| 341 |  |  |  |  |  |  | # trailing whitespace. | 
| 342 | 1699 | 100 |  |  |  | 2767 | if ($paragraph =~ m{ \A \s* > \s }xms) { | 
| 343 | 13 |  |  |  |  | 195 | $paragraph =~ s{ [ ]+ \n }{\n}xmsg; | 
| 344 | 13 |  |  |  |  | 42 | return $paragraph; | 
| 345 |  |  |  |  |  |  | } | 
| 346 |  |  |  |  |  |  |  | 
| 347 |  |  |  |  |  |  | # If this looks like a bunch of short lines, leave it alone. | 
| 348 | 1686 | 100 |  |  |  | 2738 | if ($paragraph =~ m{ \A [^\n]{1,40} \n [^\n]{1,40} \n }xms) { | 
| 349 | 85 |  |  |  |  | 199 | return $paragraph; | 
| 350 |  |  |  |  |  |  | } | 
| 351 |  |  |  |  |  |  |  | 
| 352 |  |  |  |  |  |  | # If this paragraph is not consistently indented, leave it alone. | 
| 353 | 1601 | 100 |  |  |  | 11474 | if ($paragraph !~ m{ \A (?: \Q$indent\E \S[^\n]+ \n )+ \z }xms) { | 
| 354 | 105 |  |  |  |  | 311 | return $paragraph; | 
| 355 |  |  |  |  |  |  | } | 
| 356 |  |  |  |  |  |  |  | 
| 357 |  |  |  |  |  |  | # Strip the indent from each line. | 
| 358 | 1496 |  |  |  |  | 29376 | $paragraph =~ s{ (?: \A | (?<=\n) ) \Q$indent\E }{}xmsg; | 
| 359 |  |  |  |  |  |  |  | 
| 360 |  |  |  |  |  |  | # Remove any existing newlines, preserving two spaces after periods. | 
| 361 | 1496 |  |  |  |  | 3546 | $paragraph =~ s{ [.] ([)\"]?) \n (\S) }{.$1  $2}xmsg; | 
| 362 | 1496 |  |  |  |  | 6956 | $paragraph =~ s{ \n(\S) }{ $1}xmsg; | 
| 363 |  |  |  |  |  |  |  | 
| 364 |  |  |  |  |  |  | # Force locally correct configuration of Text::Wrap. | 
| 365 | 1496 |  |  |  |  | 3635 | local $Text::Wrap::break    = qr{\s+}xms; | 
| 366 | 1496 |  |  |  |  | 2632 | local $Text::Wrap::columns  = $self->{width} + 1; | 
| 367 | 1496 |  |  |  |  | 1893 | local $Text::Wrap::huge     = 'overflow'; | 
| 368 | 1496 |  |  |  |  | 1745 | local $Text::Wrap::unexpand = 0; | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | # Do the wrapping.  This modifies @paragraphs in place. | 
| 371 | 1496 |  |  |  |  | 2726 | $paragraph = wrap($indent, $indent, $paragraph); | 
| 372 |  |  |  |  |  |  |  | 
| 373 |  |  |  |  |  |  | # Strip any trailing whitespace, since some gets left behind after periods | 
| 374 |  |  |  |  |  |  | # by Text::Wrap. | 
| 375 | 1496 |  |  |  |  | 178404 | $paragraph =~ s{ [ ]+ \n }{\n}xmsg; | 
| 376 |  |  |  |  |  |  |  | 
| 377 |  |  |  |  |  |  | # All done. | 
| 378 | 1496 |  |  |  |  | 4268 | return $paragraph; | 
| 379 |  |  |  |  |  |  | } | 
| 380 |  |  |  |  |  |  |  | 
| 381 |  |  |  |  |  |  | # Word-wrap a block of text.  This requires some annoying heuristics, but the | 
| 382 |  |  |  |  |  |  | # alternative is to try to get the template to always produce correctly | 
| 383 |  |  |  |  |  |  | # wrapped results, which is far harder. | 
| 384 |  |  |  |  |  |  | # | 
| 385 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 386 |  |  |  |  |  |  | # $text - The text to wrap | 
| 387 |  |  |  |  |  |  | # | 
| 388 |  |  |  |  |  |  | # Returns: The wrapped text | 
| 389 |  |  |  |  |  |  | sub _wrap { | 
| 390 | 34 |  |  | 34 |  | 153 | my ($self, $text) = @_; | 
| 391 |  |  |  |  |  |  |  | 
| 392 |  |  |  |  |  |  | # First, break the text up into paragraphs.  (This will also turn more | 
| 393 |  |  |  |  |  |  | # than two consecutive newlines into just two newlines.) | 
| 394 | 34 |  |  |  |  | 2568 | my @paragraphs = split(m{ \n(?:[ ]*\n)+ }xms, $text); | 
| 395 |  |  |  |  |  |  |  | 
| 396 |  |  |  |  |  |  | # Add back the trailing newlines at the end of each paragraph. | 
| 397 | 34 |  |  |  |  | 111 | @paragraphs = map { $_ . "\n" } @paragraphs; | 
|  | 2085 |  |  |  |  | 3136 |  | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | # Wrap all of the paragraphs.  This modifies @paragraphs in place. | 
| 400 | 34 |  |  |  |  | 131 | for my $paragraph (@paragraphs) { | 
| 401 | 2085 |  |  |  |  | 3476 | $paragraph = $self->_wrap_paragraph($paragraph); | 
| 402 |  |  |  |  |  |  | } | 
| 403 |  |  |  |  |  |  |  | 
| 404 |  |  |  |  |  |  | # Glue the paragraphs back together and return the result.  Because the | 
| 405 |  |  |  |  |  |  | # last newline won't get stripped by the split above, we have to strip an | 
| 406 |  |  |  |  |  |  | # extra newline from the end of the file. | 
| 407 | 34 |  |  |  |  | 613 | my $result = join("\n", @paragraphs); | 
| 408 | 34 |  |  |  |  | 2044 | $result =~ s{ \n+ \z }{\n}xms; | 
| 409 | 34 |  |  |  |  | 839 | return $result; | 
| 410 |  |  |  |  |  |  | } | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | ############################################################################## | 
| 413 |  |  |  |  |  |  | # Public interface | 
| 414 |  |  |  |  |  |  | ############################################################################## | 
| 415 |  |  |  |  |  |  |  | 
| 416 |  |  |  |  |  |  | # Create a new App::DocKnot::Generate object, which will be used for | 
| 417 |  |  |  |  |  |  | # subsequent calls. | 
| 418 |  |  |  |  |  |  | # | 
| 419 |  |  |  |  |  |  | # $class - Class of object to create | 
| 420 |  |  |  |  |  |  | # $args  - Anonymous hash of arguments with the following keys: | 
| 421 |  |  |  |  |  |  | #   metadata - Path to the directory containing package metadata | 
| 422 |  |  |  |  |  |  | #   width    - Line length at which to wrap output files | 
| 423 |  |  |  |  |  |  | # | 
| 424 |  |  |  |  |  |  | # Returns: Newly created object | 
| 425 |  |  |  |  |  |  | #  Throws: Text exceptions on invalid metadata directory path | 
| 426 |  |  |  |  |  |  | sub new { | 
| 427 | 14 |  |  | 14 | 1 | 12303 | my ($class, $args_ref) = @_; | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | # Create the config reader. | 
| 430 | 14 |  |  |  |  | 29 | my %config_args; | 
| 431 | 14 | 100 |  |  |  | 60 | if ($args_ref->{metadata}) { | 
| 432 | 11 |  |  |  |  | 35 | $config_args{metadata} = $args_ref->{metadata}; | 
| 433 |  |  |  |  |  |  | } | 
| 434 | 14 |  |  |  |  | 130 | my $config = App::DocKnot::Config->new(\%config_args); | 
| 435 |  |  |  |  |  |  |  | 
| 436 |  |  |  |  |  |  | # Create and return the object. | 
| 437 |  |  |  |  |  |  | my $self = { | 
| 438 |  |  |  |  |  |  | config => $config, | 
| 439 | 13 |  | 50 |  |  | 93 | width  => $args_ref->{width} // 74, | 
| 440 |  |  |  |  |  |  | }; | 
| 441 | 13 |  |  |  |  | 29 | bless($self, $class); | 
| 442 | 13 |  |  |  |  | 42 | return $self; | 
| 443 |  |  |  |  |  |  | } | 
| 444 |  |  |  |  |  |  |  | 
| 445 |  |  |  |  |  |  | # Generate a documentation file from the package metadata. | 
| 446 |  |  |  |  |  |  | # | 
| 447 |  |  |  |  |  |  | # $self     - The App::DocKnot::Generate object | 
| 448 |  |  |  |  |  |  | # $template - Name of the documentation template (using Template Toolkit) | 
| 449 |  |  |  |  |  |  | # | 
| 450 |  |  |  |  |  |  | # Returns: The generated documentation as a string | 
| 451 |  |  |  |  |  |  | #  Throws: autodie exception on failure to read metadata or write the output | 
| 452 |  |  |  |  |  |  | #          Text exception on Template Toolkit failures | 
| 453 |  |  |  |  |  |  | #          Text exception on inconsistencies in the package data | 
| 454 |  |  |  |  |  |  | sub generate { | 
| 455 | 34 |  |  | 34 | 1 | 28419 | my ($self, $template) = @_; | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | # Load the package metadata. | 
| 458 | 34 |  |  |  |  | 259 | my $data_ref = $self->{config}->config(); | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | # Create the variable information for the template.  Start with all | 
| 461 |  |  |  |  |  |  | # metadata as loaded above. | 
| 462 | 34 |  |  |  |  | 91 | my %vars = %{$data_ref}; | 
|  | 34 |  |  |  |  | 302 |  | 
| 463 |  |  |  |  |  |  |  | 
| 464 |  |  |  |  |  |  | # Add code references for our defined helper functions. | 
| 465 | 34 |  |  |  |  | 186 | $vars{center}    = $self->_code_for_center; | 
| 466 | 34 |  |  |  |  | 130 | $vars{copyright} = $self->_code_for_copyright($data_ref->{copyrights}); | 
| 467 | 34 |  |  |  |  | 115 | $vars{indent}    = $self->_code_for_indent; | 
| 468 | 34 |  |  |  |  | 115 | $vars{to_text}   = $self->_code_for_to_text; | 
| 469 | 34 |  |  |  |  | 108 | $vars{to_thread} = $self->_code_for_to_thread; | 
| 470 |  |  |  |  |  |  |  | 
| 471 |  |  |  |  |  |  | # Ensure we were given a valid template. | 
| 472 | 34 |  |  |  |  | 179 | $template = $self->appdata_path('templates', "${template}.tmpl"); | 
| 473 |  |  |  |  |  |  |  | 
| 474 |  |  |  |  |  |  | # Run Template Toolkit processing. | 
| 475 | 34 | 50 |  |  |  | 366 | my $tt = Template->new({ ABSOLUTE => 1 }) or croak(Template->error()); | 
| 476 | 34 |  |  |  |  | 93842 | my $result; | 
| 477 | 34 | 50 |  |  |  | 215 | $tt->process($template, \%vars, \$result) or croak($tt->error); | 
| 478 |  |  |  |  |  |  |  | 
| 479 |  |  |  |  |  |  | # Word-wrap the results to our width and return them. | 
| 480 | 34 |  |  |  |  | 4123 | return $self->_wrap($result); | 
| 481 |  |  |  |  |  |  | } | 
| 482 |  |  |  |  |  |  |  | 
| 483 |  |  |  |  |  |  | # Generate all package documentation from the package metadata.  Only | 
| 484 |  |  |  |  |  |  | # generates the output for templates with a default output file. | 
| 485 |  |  |  |  |  |  | # | 
| 486 |  |  |  |  |  |  | # $self - The App::DocKnot::Generate object | 
| 487 |  |  |  |  |  |  | # | 
| 488 |  |  |  |  |  |  | # Returns: undef | 
| 489 |  |  |  |  |  |  | #  Throws: autodie exception on failure to read metadata or write the output | 
| 490 |  |  |  |  |  |  | #          Text exception on Template Toolkit failures | 
| 491 |  |  |  |  |  |  | #          Text exception on inconsistencies in the package data | 
| 492 |  |  |  |  |  |  | sub generate_all { | 
| 493 | 2 |  |  | 2 | 1 | 1490 | my ($self) = @_; | 
| 494 | 2 |  |  |  |  | 9 | for my $template (keys(%DEFAULT_OUTPUT)) { | 
| 495 | 4 |  |  |  |  | 17 | $self->generate_output($template); | 
| 496 |  |  |  |  |  |  | } | 
| 497 | 2 |  |  |  |  | 18 | return; | 
| 498 |  |  |  |  |  |  | } | 
| 499 |  |  |  |  |  |  |  | 
| 500 |  |  |  |  |  |  | # Generate a documentation file from the package metadata. | 
| 501 |  |  |  |  |  |  | # | 
| 502 |  |  |  |  |  |  | # $self     - The App::DocKnot::Generate object | 
| 503 |  |  |  |  |  |  | # $template - Name of the documentation template | 
| 504 |  |  |  |  |  |  | # $output   - Output file name (undef to use the default) | 
| 505 |  |  |  |  |  |  | # | 
| 506 |  |  |  |  |  |  | # Returns: undef | 
| 507 |  |  |  |  |  |  | #  Throws: autodie exception on failure to read metadata or write the output | 
| 508 |  |  |  |  |  |  | #          Text exception on Template Toolkit failures | 
| 509 |  |  |  |  |  |  | #          Text exception on inconsistencies in the package data | 
| 510 |  |  |  |  |  |  | sub generate_output { | 
| 511 | 10 |  |  | 10 | 1 | 5047 | my ($self, $template, $output) = @_; | 
| 512 | 10 |  | 66 |  |  | 57 | $output //= $DEFAULT_OUTPUT{$template}; | 
| 513 |  |  |  |  |  |  |  | 
| 514 |  |  |  |  |  |  | # If the template doesn't have a default output file, $output is required. | 
| 515 | 10 | 50 |  |  |  | 27 | if (!defined($output)) { | 
| 516 | 0 |  |  |  |  | 0 | croak('missing required output argument'); | 
| 517 |  |  |  |  |  |  | } | 
| 518 |  |  |  |  |  |  |  | 
| 519 |  |  |  |  |  |  | # Generate the output. | 
| 520 | 10 |  |  |  |  | 52 | open(my $outfh, '>', $output); | 
| 521 | 10 | 50 |  |  |  | 12947 | print {$outfh} $self->generate($template) | 
|  | 10 |  |  |  |  | 122 |  | 
| 522 |  |  |  |  |  |  | or croak("cannot write to $output: $!"); | 
| 523 | 10 |  |  |  |  | 3499 | close($outfh); | 
| 524 | 10 |  |  |  |  | 5382 | return; | 
| 525 |  |  |  |  |  |  | } | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | ############################################################################## | 
| 528 |  |  |  |  |  |  | # Module return value and documentation | 
| 529 |  |  |  |  |  |  | ############################################################################## | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | 1; | 
| 532 |  |  |  |  |  |  | __END__ |