File Coverage

lib/WRT/Markup.pm
Criterion Covered Total %
statement 103 115 89.5
branch 9 18 50.0
condition 12 20 60.0
subroutine 17 18 94.4
pod 7 10 70.0
total 148 181 81.7


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