| blib/lib/App/WRT/Markup.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % | 
| statement | 99 | 113 | 87.6 | 
| branch | 14 | 26 | 53.8 | 
| condition | 6 | 14 | 42.8 | 
| subroutine | 19 | 19 | 100.0 | 
| pod | 9 | 9 | 100.0 | 
| total | 147 | 181 | 81.2 | 
| line | stmt | bran | cond | sub | pod | time | code | 
|---|---|---|---|---|---|---|---|
| 1 | package App::WRT::Markup; | ||||||
| 2 | |||||||
| 3 | 9 | 9 | 68 | use strict; | |||
| 9 | 19 | ||||||
| 9 | 294 | ||||||
| 4 | 9 | 9 | 47 | use warnings; | |||
| 9 | 18 | ||||||
| 9 | 291 | ||||||
| 5 | 9 | 9 | 51 | use feature "state"; | |||
| 9 | 16 | ||||||
| 9 | 852 | ||||||
| 6 | |||||||
| 7 | 9 | 9 | 57 | use base qw(Exporter); | |||
| 9 | 15 | ||||||
| 9 | 936 | ||||||
| 8 | our @EXPORT_OK = qw(line_parse image_markup eval_perl); | ||||||
| 9 | |||||||
| 10 | 9 | 9 | 76 | use App::WRT::Image qw(image_size); | |||
| 9 | 18 | ||||||
| 9 | 441 | ||||||
| 11 | 9 | 9 | 53 | use App::WRT::Util qw(file_get_contents); | |||
| 9 | 17 | ||||||
| 9 | 474 | ||||||
| 12 | |||||||
| 13 | 9 | 9 | 74 | use Carp; | |||
| 9 | 16 | ||||||
| 9 | 512 | ||||||
| 14 | 9 | 9 | 57 | use File::Basename; | |||
| 9 | 16 | ||||||
| 9 | 819 | ||||||
| 15 | 9 | 9 | 7818 | use Text::Textile; | |||
| 9 | 273138 | ||||||
| 9 | 466 | ||||||
| 16 | 9 | 9 | 4374 | use Text::Markdown::Discount; | |||
| 9 | 8366 | ||||||
| 9 | 16114 | ||||||
| 17 | |||||||
| 18 | # Some useful defaults: | ||||||
| 19 | |||||||
| 20 | my %tags = ( | ||||||
| 21 | retcon => q{div class="retcon"}, | ||||||
| 22 | freeverse => 'p', | ||||||
| 23 | list      => "ul>\n | ||||||
| 24 | ); | ||||||
| 25 | |||||||
| 26 | my %end_tags = ( | ||||||
| 27 | retcon => 'div', | ||||||
| 28 | freeverse => 'p', | ||||||
| 29 | list => "li>\n | ||||||
| 30 | ); | ||||||
| 31 | |||||||
| 32 | my %blank_lines = ( | ||||||
| 33 | freeverse => "\n\n ", | ||||||
| 34 | list      => "\n\n | ||||||
| 35 | ); | ||||||
| 36 | |||||||
| 37 | my %newlines = ( | ||||||
| 38 | freeverse => " \n" | ||||||
| 39 | ); | ||||||
| 40 | |||||||
| 41 | my %dashes = ( | ||||||
| 42 | freeverse => ' — ' | ||||||
| 43 | ); | ||||||
| 44 | |||||||
| 45 | =over | ||||||
| 46 | |||||||
| 47 | =item eval_perl | ||||||
| 48 | |||||||
| 49 | Evaluate embedded Perl in a string, replacing blocks enclosed with | ||||||
| 50 | with whatever they return (well, evaluated in a scalar context). Returns the | ||||||
| 51 | modified string. | ||||||
| 52 | |||||||
| 53 | Also handles simple ${variables}, replacing them from the keys to $self. | ||||||
| 54 | |||||||
| 55 | =cut | ||||||
| 56 | |||||||
| 57 | sub eval_perl { | ||||||
| 58 | 253 | 253 | 1 | 483 | my $self = shift; | ||
| 59 | 253 | 486 | my ($text) = @_; | ||||
| 60 | |||||||
| 61 | 253 | 1221 | while ($text =~ m{ | ||||
| 62 | 62 | 163 | my $block = $1; | ||||
| 63 | |||||||
| 64 | # Run the $block, and include anything returned: | ||||||
| 65 | 62 | 4431 | my $output = eval $block; | ||||
| 66 | |||||||
| 67 | 62 | 50 | 294 | if ($@) { | |||
| 68 | # Errors - log and return an empty string: | ||||||
| 69 | 0 | 0 | carp($@); | ||||
| 70 | 0 | 0 | $output = ''; | ||||
| 71 | } | ||||||
| 72 | |||||||
| 73 | 62 | 1096 | $text =~ s{ | ||||
| 74 | } | ||||||
| 75 | |||||||
| 76 | # Interpolate variables: | ||||||
| 77 | 253 | 1139 | $text =~ s{ | ||||
| 78 | \$\{ ([a-zA-Z_]+) \} | ||||||
| 79 | }{ | ||||||
| 80 | 1159 | 50 | 2555 | if (defined $self->{$1}) { | |||
| 81 | 1159 | 4139 | $self->{$1}; | ||||
| 82 | } else { | ||||||
| 83 | # TODO: Possibly this should be fatal. | ||||||
| 84 | 0 | 0 | "UNDEFINED: $1"; | ||||
| 85 | } | ||||||
| 86 | }gex; | ||||||
| 87 | |||||||
| 88 | 253 | 897 | return $text; | ||||
| 89 | } | ||||||
| 90 | |||||||
| 91 | =item line_parse | ||||||
| 92 | |||||||
| 93 | Performs substitutions on lines called by fragment_slurp, at least. Calls | ||||||
| 94 | include_process(), image_markup(), textile_process(), markdown_process(). | ||||||
| 95 | |||||||
| 96 | Returns string. | ||||||
| 97 | |||||||
| 98 | Parses some special markup. Specifically: | ||||||
| 99 | |||||||
| 100 |  | ||||||
| 101 | ${variable} interpolation from the WRT object | ||||||
| 102 | |||||||
| 103 |  | ||||||
| 104 | |||||||
| 105 |  | ||||||
| 106 |  | ||||||
| 107 | |||||||
| 108 |  | ||||||
| 109 | optional alt tag | ||||||
| 110 | optional title text | ||||||
| 111 | |||||||
| 112 |  | ||||||
| 113 |  | ||||||
| 114 |  | ||||||
| 115 | |||||||
| 116 | =cut | ||||||
| 117 | |||||||
| 118 | sub line_parse { | ||||||
| 119 | 253 | 253 | 1 | 387 | my $self = shift; | ||
| 120 | 253 | 482 | my ($everything, $file) = (@_); | ||||
| 121 | |||||||
| 122 | # Take care of | ||||||
| 123 | 253 | 594 | include_process($self, $everything); | ||||
| 124 | 253 | 563 | textile_process($everything); | ||||
| 125 | 253 | 3644 | markdown_process($everything); | ||||
| 126 | 253 | 2184 | $everything =~ s! | ||||
| 10 | 51 | ||||||
| 127 | |||||||
| 128 | 253 | 759 | foreach my $key (keys %tags) { | ||||
| 129 | # Set some replacements, unless they've been explicitly set already: | ||||||
| 130 | 759 | 33 | 1776 | $end_tags{$key} ||= $tags{$key}; | |||
| 131 | |||||||
| 132 | # Transform blocks: | ||||||
| 133 | 759 | 13307 | while ($everything =~ m| (<$key>\n?) (.*?) (\n?$key>) |sx) { | ||||
| 134 | 2 | 6 | my $open = $1; | ||||
| 135 | 2 | 5 | my $block = $2; | ||||
| 136 | 2 | 3 | my $close = $3; | ||||
| 137 | |||||||
| 138 | # Save the bits between instances of the block: | ||||||
| 139 | 2 | 35 | my (@interstices) = split /\Q$open$block$close\E/s, $everything; | ||||
| 140 | |||||||
| 141 | # Transform dashes, blank lines, and newlines: | ||||||
| 142 | 2 | 100 | 14 | dashes($dashes{$key}, $block) if defined $dashes{$key}; | |||
| 143 | 2 | 50 | 13 | $block =~ s/\n\n/$blank_lines{$key}/gs if defined $blank_lines{$key}; | |||
| 144 | 2 | 100 | 9 | newlines($newlines{$key}, $block) if defined $newlines{$key}; | |||
| 145 | |||||||
| 146 | # Slap it all back together as $everything, with start and end | ||||||
| 147 | # tags: | ||||||
| 148 | 2 | 10 | $block = "<$tags{$key}>$block$end_tags{$key}>"; | ||||
| 149 | 2 | 17 | $everything = join $block, @interstices; | ||||
| 150 | } | ||||||
| 151 | } | ||||||
| 152 | |||||||
| 153 | 253 | 1515 | return $everything; | ||||
| 154 | } | ||||||
| 155 | |||||||
| 156 | =item newlines($replacement, $block) | ||||||
| 157 | |||||||
| 158 | Inline replace single newlines (i.e., line ends) within the block, except those | ||||||
| 159 | preceded by a double-quote, which probably indicates a still-open tag. | ||||||
| 160 | |||||||
| 161 | =cut | ||||||
| 162 | |||||||
| 163 | sub newlines { | ||||||
| 164 | 1 | 1 | 1 | 9 | $_[1] =~ s/(?<=[^"\n]) # not a double-quote or newline | ||
| 165 | # don't capture | ||||||
| 166 | |||||||
| 167 | \n # end-of-line | ||||||
| 168 | |||||||
| 169 | (?=[^\n]) # not a newline | ||||||
| 170 | # don't capture | ||||||
| 171 | /$_[0]/xgs; | ||||||
| 172 | } | ||||||
| 173 | |||||||
| 174 | =item dashes($replacement, $block) | ||||||
| 175 | |||||||
| 176 | Inline replace double dashes in a block - " -- " - with a given replacement. | ||||||
| 177 | |||||||
| 178 | =cut | ||||||
| 179 | |||||||
| 180 | sub dashes { | ||||||
| 181 | 1 | 1 | 1 | 5 | $_[1] =~ s/(\s+) # whitespace - no capture | ||
| 182 | \-{2} # two dashes | ||||||
| 183 | (\n|\s+|$) # newline, whitespace, or eol | ||||||
| 184 | /$1$_[0]$2/xgs; | ||||||
| 185 | |||||||
| 186 | } | ||||||
| 187 | |||||||
| 188 | =item include_process | ||||||
| 189 | |||||||
| 190 | Inline replace | ||||||
| 191 | contents of files. | ||||||
| 192 | |||||||
| 193 | =cut | ||||||
| 194 | |||||||
| 195 | sub include_process { | ||||||
| 196 | 253 | 253 | 1 | 325 | my $wrt = shift; | ||
| 197 | |||||||
| 198 | 253 | 1165 | $_[0] =~ s{ | ||||
| 199 | |||||||
| 200 |  | ||||||
| 201 | (.*?) # anything (non-greedy) | ||||||
| 202 | # end tag | ||||||
| 203 | |||||||
| 204 | }{ | ||||||
| 205 | 12 | 48 | retrieve_include($wrt, $1); | ||||
| 206 | }xesg; | ||||||
| 207 | } | ||||||
| 208 | |||||||
| 209 | =item retrieve_include | ||||||
| 210 | |||||||
| 211 | Get the contents of an included file. This probably needs a great | ||||||
| 212 | deal more thought than I am presently giving it. | ||||||
| 213 | |||||||
| 214 | =cut | ||||||
| 215 | |||||||
| 216 | sub retrieve_include { | ||||||
| 217 | 12 | 12 | 1 | 23 | my $wrt = shift; | ||
| 218 | 12 | 41 | my ($file) = @_; | ||||
| 219 | |||||||
| 220 | # Trim leading and trailing spaces: | ||||||
| 221 | 12 | 41 | $file =~ s/^\s+//; | ||||
| 222 | 12 | 47 | $file =~ s/\s+$//; | ||||
| 223 | |||||||
| 224 | 12 | 50 | 51 | if ($file =~ m{^ (/ | [.]/) }x) { | |||
| 225 | # TODO: Leads with a slash or a ./ | ||||||
| 226 | 0 | 0 | croak('Tried to open an include path with a leading / or ./ - not yet supported.'); | ||||
| 227 | } else { | ||||||
| 228 | # Use the archive root as path. | ||||||
| 229 | 12 | 52 | $file = $wrt->{root_dir} . '/' . $file; | ||||
| 230 | } | ||||||
| 231 | |||||||
| 232 | 12 | 50 | 39 | if ($wrt->{cache_includes}) { | |||
| 233 | 0 | 0 | 0 | if (defined $wrt->{include_cache}->{$file}) { | |||
| 234 | 0 | 0 | return $wrt->{include_cache}->{$file}; | ||||
| 235 | } | ||||||
| 236 | } | ||||||
| 237 | |||||||
| 238 | 12 | 50 | 236 | unless (-e $file) { | |||
| 239 | 0 | 0 | carp "No such file: $file"; | ||||
| 240 | 0 | 0 | return ''; | ||||
| 241 | } | ||||||
| 242 | |||||||
| 243 | 12 | 50 | 164 | if (-d $file) { | |||
| 244 | 0 | 0 | carp("Tried to open a directory as an include path: $file"); | ||||
| 245 | 0 | 0 | return ''; | ||||
| 246 | } | ||||||
| 247 | |||||||
| 248 | 12 | 50 | 54 | if ($wrt->{cache_includes}) { | |||
| 249 | 0 | 0 | $wrt->{include_cache}->{$file} = file_get_contents($file); | ||||
| 250 | 0 | 0 | return $wrt->{include_cache}->{$file}; | ||||
| 251 | } else { | ||||||
| 252 | 12 | 50 | return file_get_contents($file); | ||||
| 253 | } | ||||||
| 254 | } | ||||||
| 255 | |||||||
| 256 | =item textile_process | ||||||
| 257 | |||||||
| 258 | Inline replace | ||||||
| 259 | |||||||
| 260 | =cut | ||||||
| 261 | |||||||
| 262 | # This is exactly the kind of code that, even though it isn't doing anything | ||||||
| 263 | # especially over the top, looks ghastly to people who don't read Perl, so I'll | ||||||
| 264 | # try to explain a bit. | ||||||
| 265 | |||||||
| 266 | sub textile_process { | ||||||
| 267 | |||||||
| 268 | # First, there's a state variable here which can retain the Text::Textile | ||||||
| 269 | # object between invocations of the function, saving us a bit of time on | ||||||
| 270 | # subsequent calls. This should be equivalent to creating a closure around | ||||||
| 271 | # the function and keeping a $textile variable there. | ||||||
| 272 | 253 | 253 | 1 | 311 | state $textile; | ||
| 273 | |||||||
| 274 | # Second, instead of unrolling the arguments to the function, we just act | ||||||
| 275 | # directly on the first (0th) one. =~ more or less means "do a regexy | ||||||
| 276 | # thing on this". It's followed by s, the substitution operator, which can | ||||||
| 277 | # use curly braces as delimiters between pattern and replacement. | ||||||
| 278 | |||||||
| 279 | 253 | 1044 | $_[0] =~ s{ | ||||
| 280 | |||||||
| 281 | # Find tags... | ||||||
| 282 | |||||||
| 283 |  | ||||||
| 284 | (.*?) # anything (non-greedy) | ||||||
| 285 | # end tag | ||||||
| 286 | |||||||
| 287 | }{ | ||||||
| 288 | |||||||
| 289 | # ...and replace them with the result of evaluating this block. | ||||||
| 290 | |||||||
| 291 | # //= means "defined-or-equals"; if the var hasn't been defined yet, | ||||||
| 292 | # then make a new Textile object: | ||||||
| 293 | 1 | 33 | 17 | $textile //= Text::Textile->new(); | |||
| 294 | |||||||
| 295 | # Process the stuff we slurped out of our tags - this value will be | ||||||
| 296 | # used to replace the entire match from above (in Perl, the last | ||||||
| 297 | # expression evaluated is the return value of subs, evals, etc.): | ||||||
| 298 | 1 | 273 | $textile->process($1); | ||||
| 299 | |||||||
| 300 | }xesg; | ||||||
| 301 | |||||||
| 302 | # x: eXtended regexp - whitespace ignored by default, comments allowed | ||||||
| 303 | # e: Execute the replacement as Perl code, and use its value | ||||||
| 304 | # s: treat all lines of the search subject as a Single string | ||||||
| 305 | # g: Globally replace all matches | ||||||
| 306 | |||||||
| 307 | # For the genuinely concise version of this, see markdown_process(). | ||||||
| 308 | } | ||||||
| 309 | |||||||
| 310 | =item markdown_process | ||||||
| 311 | |||||||
| 312 | Inline replace | ||||||
| 313 | |||||||
| 314 | =cut | ||||||
| 315 | |||||||
| 316 | sub markdown_process { | ||||||
| 317 | 253 | 253 | 1 | 285 | state $markdown; | ||
| 318 | |||||||
| 319 | 253 | 369 | my $flags = Text::Markdown::Discount::MKD_EXTRA_FOOTNOTE(); | ||||
| 320 | |||||||
| 321 | 253 | 1199 | $_[0] =~ s{ | ||||
| 322 |  | ||||||
| 323 | }{ | ||||||
| 324 | 21 | 66 | 162 | $markdown //= Text::Markdown::Discount->new; | |||
| 325 | 21 | 185 | $markdown->markdown($1, $flags); | ||||
| 326 | }xesg; | ||||||
| 327 | } | ||||||
| 328 | |||||||
| 329 | =item image_markup | ||||||
| 330 | |||||||
| 331 | Parse out an image tag and return the appropriate html. | ||||||
| 332 | |||||||
| 333 | Relies on image_size from App::WRT::Image. | ||||||
| 334 | |||||||
| 335 | =cut | ||||||
| 336 | |||||||
| 337 | sub image_markup { | ||||||
| 338 | 10 | 10 | 1 | 25 | my $self = shift; | ||
| 339 | 10 | 45 | my ($file, $block) = @_; | ||||
| 340 | |||||||
| 341 | # Get a basename and directory for the file (entry) referencing the image: | ||||||
| 342 | 10 | 341 | my ($basename, $dir) = fileparse($file); | ||||
| 343 | |||||||
| 344 | # Truncated file date that just includes date + sub docs: | ||||||
| 345 | 10 | 47 | my ($file_date) = $dir =~ m{ | ||||
| 346 | ( | ||||||
| 347 | [0-9]{4}/ # year | ||||||
| 348 | [0-9]{1,2}/ # month | ||||||
| 349 | [0-9]{1,2}/ # day | ||||||
| 350 | ([a-z]*/)* # sub-entries | ||||||
| 351 | ) | ||||||
| 352 | $ | ||||||
| 353 | }x; | ||||||
| 354 | |||||||
| 355 | # Process the contents of the | ||||||
| 356 | 10 | 64 | my ($image_url, $alt_text, $title_text) = split /\n/, $block; | ||||
| 357 | 10 | 50 | 44 | $alt_text ||= q{}; | |||
| 358 | 10 | 33 | 65 | $title_text ||= $alt_text; | |||
| 359 | |||||||
| 360 | # Resolve relative paths: | ||||||
| 361 | 10 | 19 | my $image_file; | ||||
| 362 | 10 | 50 | 402 | if (-e "$dir/$image_url" ) { | |||
| 50 | |||||||
| 363 | # The path is to an image file in the same directory as current entry: | ||||||
| 364 | 0 | 0 | $image_file = "$dir/$image_url"; | ||||
| 365 | 0 | 0 | $image_url = "${file_date}${image_url}"; | ||||
| 366 | } elsif (-e $self->{entry_dir} . "/$image_url") { | ||||||
| 367 | # The path is to an image file starting with the entry_dir, like | ||||||
| 368 | # 2005/9/20/candles.jpg -> ./archives/2005/9/20/candles.jpg | ||||||
| 369 | 10 | 59 | $image_file = $self->{entry_dir} . "/$image_url"; | ||||
| 370 | } | ||||||
| 371 | |||||||
| 372 | # Get width & height in pixels for known filetypes: | ||||||
| 373 | 10 | 90 | my ($width, $height) = image_size($self->{root_dir_abs} . '/' . $image_file); | ||||
| 374 | |||||||
| 375 | # This probably relies on mod_rewrite working: | ||||||
| 376 | 10 | 2357 | $image_url = $self->{image_url_root} . $image_url; | ||||
| 377 | 10 | 127 | return <<"IMG"; | ||||
| 378 |  | ||||||
| 379 | width="$width" | ||||||
| 380 | height="$height" | ||||||
| 381 | alt="$alt_text" | ||||||
| 382 | title="$title_text" /> | ||||||
| 383 | IMG | ||||||
| 384 | } | ||||||
| 385 | |||||||
| 386 | =back | ||||||
| 387 | |||||||
| 388 | 1; |