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 | 8 | 8 | 52 | use strict; | |||
8 | 13 | ||||||
8 | 220 | ||||||
4 | 8 | 8 | 36 | use warnings; | |||
8 | 15 | ||||||
8 | 189 | ||||||
5 | 8 | 8 | 36 | use feature "state"; | |||
8 | 26 | ||||||
8 | 568 | ||||||
6 | |||||||
7 | 8 | 8 | 42 | use base qw(Exporter); | |||
8 | 12 | ||||||
8 | 670 | ||||||
8 | our @EXPORT_OK = qw(line_parse image_markup eval_perl); | ||||||
9 | |||||||
10 | 8 | 8 | 61 | use App::WRT::Image qw(image_size); | |||
8 | 19 | ||||||
8 | 370 | ||||||
11 | 8 | 8 | 54 | use App::WRT::Util qw(file_get_contents); | |||
8 | 13 | ||||||
8 | 346 | ||||||
12 | |||||||
13 | 8 | 8 | 42 | use Carp; | |||
8 | 17 | ||||||
8 | 386 | ||||||
14 | 8 | 8 | 44 | use File::Basename; | |||
8 | 12 | ||||||
8 | 534 | ||||||
15 | 8 | 8 | 6049 | use Text::Textile; | |||
8 | 211162 | ||||||
8 | 339 | ||||||
16 | 8 | 8 | 3340 | use Text::Markdown::Discount; | |||
8 | 6374 | ||||||
8 | 12481 | ||||||
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 | 251 | 251 | 1 | 412 | my $self = shift; | ||
59 | 251 | 417 | my ($text) = @_; | ||||
60 | |||||||
61 | 251 | 1037 | while ($text =~ m{ |
||||
62 | 61 | 149 | my $block = $1; | ||||
63 | |||||||
64 | # Run the $block, and include anything returned: | ||||||
65 | 61 | 3925 | my $output = eval $block; | ||||
66 | |||||||
67 | 61 | 50 | 256 | if ($@) { | |||
68 | # Errors - log and return an empty string: | ||||||
69 | 0 | 0 | carp($@); | ||||
70 | 0 | 0 | $output = ''; | ||||
71 | } | ||||||
72 | |||||||
73 | 61 | 927 | $text =~ s{ |
||||
74 | } | ||||||
75 | |||||||
76 | # Interpolate variables: | ||||||
77 | 251 | 1032 | $text =~ s{ | ||||
78 | \$\{ ([a-zA-Z_]+) \} | ||||||
79 | }{ | ||||||
80 | 1140 | 50 | 2402 | if (defined $self->{$1}) { | |||
81 | 1140 | 3805 | $self->{$1}; | ||||
82 | } else { | ||||||
83 | # TODO: Possibly this should be fatal. | ||||||
84 | 0 | 0 | "UNDEFINED: $1"; | ||||
85 | } | ||||||
86 | }gex; | ||||||
87 | |||||||
88 | 251 | 717 | 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 | 251 | 251 | 1 | 331 | my $self = shift; | ||
120 | 251 | 387 | my ($everything, $file) = (@_); | ||||
121 | |||||||
122 | # Take care of |
||||||
123 | 251 | 510 | include_process($self, $everything); | ||||
124 | 251 | 496 | textile_process($everything); | ||||
125 | 251 | 3537 | markdown_process($everything); | ||||
126 | 251 | 1797 | $everything =~ s! |
||||
10 | 44 | ||||||
127 | |||||||
128 | 251 | 723 | foreach my $key (keys %tags) { | ||||
129 | # Set some replacements, unless they've been explicitly set already: | ||||||
130 | 753 | 33 | 1535 | $end_tags{$key} ||= $tags{$key}; | |||
131 | |||||||
132 | # Transform blocks: | ||||||
133 | 753 | 11270 | while ($everything =~ m| (<$key>\n?) (.*?) (\n?$key>) |sx) { | ||||
134 | 2 | 6 | my $open = $1; | ||||
135 | 2 | 5 | my $block = $2; | ||||
136 | 2 | 4 | my $close = $3; | ||||
137 | |||||||
138 | # Save the bits between instances of the block: | ||||||
139 | 2 | 36 | my (@interstices) = split /\Q$open$block$close\E/s, $everything; | ||||
140 | |||||||
141 | # Transform dashes, blank lines, and newlines: | ||||||
142 | 2 | 100 | 12 | dashes($dashes{$key}, $block) if defined $dashes{$key}; | |||
143 | 2 | 50 | 15 | $block =~ s/\n\n/$blank_lines{$key}/gs if defined $blank_lines{$key}; | |||
144 | 2 | 100 | 13 | 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 | 16 | $everything = join $block, @interstices; | ||||
150 | } | ||||||
151 | } | ||||||
152 | |||||||
153 | 251 | 1251 | 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 | 10 | $_[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 | 4 | $_[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 | 251 | 251 | 1 | 276 | my $wrt = shift; | ||
197 | |||||||
198 | 251 | 1037 | $_[0] =~ s{ | ||||
199 | |||||||
200 | |
||||||
201 | (.*?) # anything (non-greedy) | ||||||
202 | # end tag | ||||||
203 | |||||||
204 | }{ | ||||||
205 | 12 | 41 | 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 | 20 | my $wrt = shift; | ||
218 | 12 | 35 | my ($file) = @_; | ||||
219 | |||||||
220 | # Trim leading and trailing spaces: | ||||||
221 | 12 | 33 | $file =~ s/^\s+//; | ||||
222 | 12 | 43 | $file =~ s/\s+$//; | ||||
223 | |||||||
224 | 12 | 50 | 45 | 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 | 46 | $file = $wrt->{root_dir} . '/' . $file; | ||||
230 | } | ||||||
231 | |||||||
232 | 12 | 50 | 36 | 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 | 219 | unless (-e $file) { | |||
239 | 0 | 0 | carp "No such file: $file"; | ||||
240 | 0 | 0 | return ''; | ||||
241 | } | ||||||
242 | |||||||
243 | 12 | 50 | 140 | 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 | 51 | 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 | 54 | 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 | 251 | 251 | 1 | 252 | 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 | 251 | 902 | $_[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 | 16 | $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 | 253 | $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 | 251 | 251 | 1 | 236 | state $markdown; | ||
318 | |||||||
319 | 251 | 301 | my $flags = Text::Markdown::Discount::MKD_EXTRA_FOOTNOTE(); | ||||
320 | |||||||
321 | 251 | 1051 | $_[0] =~ s{ | ||||
322 | |
||||||
323 | }{ | ||||||
324 | 21 | 66 | 125 | $markdown //= Text::Markdown::Discount->new; | |||
325 | 21 | 143 | $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 | 20 | my $self = shift; | ||
339 | 10 | 47 | my ($file, $block) = @_; | ||||
340 | |||||||
341 | # Get a basename and directory for the file (entry) referencing the image: | ||||||
342 | 10 | 304 | my ($basename, $dir) = fileparse($file); | ||||
343 | |||||||
344 | # Truncated file date that just includes date + sub docs: | ||||||
345 | 10 | 42 | 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 | 66 | my ($image_url, $alt_text, $title_text) = split /\n/, $block; | ||||
357 | 10 | 50 | 34 | $alt_text ||= q{}; | |||
358 | 10 | 33 | 66 | $title_text ||= $alt_text; | |||
359 | |||||||
360 | # Resolve relative paths: | ||||||
361 | 10 | 18 | my $image_file; | ||||
362 | 10 | 50 | 318 | 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 | 44 | $image_file = $self->{entry_dir} . "/$image_url"; | ||||
370 | } | ||||||
371 | |||||||
372 | # Get width & height in pixels for known filetypes: | ||||||
373 | 10 | 73 | my ($width, $height) = image_size($self->{root_dir_abs} . '/' . $image_file); | ||||
374 | |||||||
375 | # This probably relies on mod_rewrite working: | ||||||
376 | 10 | 2062 | $image_url = $self->{image_url_root} . $image_url; | ||||
377 | 10 | 108 | 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; |