File Coverage

blib/lib/Text/WikiText.pm
Criterion Covered Total %
statement 117 146 80.1
branch 40 62 64.5
condition 30 50 60.0
subroutine 13 15 86.6
pod 3 10 30.0
total 203 283 71.7


line stmt bran cond sub pod time code
1             # WikiText parser modules, Copyright (C) 2006 Enno Cramer, Mikhael Goikhman
2             #
3             # This program is free software; you can redistribute it and/or modify
4             # it under the terms of the Perl Artistic License or the GNU General
5             # Public License as published by the Free Software Foundation; either
6             # version 2 of the License, or (at your option) any later version.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # GNU General Public License for more details.
12             #
13             # You should have received a copy of the GNU General Public License
14             # along with this program; if not, write to the Free Software
15             # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16              
17             package Text::WikiText;
18              
19 4     4   5052 use strict;
  4         8  
  4         148  
20 4     4   24 use warnings;
  4         6  
  4         394  
21              
22             our $VERSION = 1.01;
23              
24             use constant {
25 4         829 COMMENT => 'comment',
26             VERBATIM => 'verbatim',
27              
28             SECTION => 'section',
29              
30             QUOTE => 'quotation',
31             LISTING => 'listing',
32             ENUMERATION => 'enumeration',
33             DESCRIPTION => 'description',
34              
35             TABLE => 'table',
36             RULE => 'horizontal-rule',
37             P => 'paragraph',
38             PRE => 'preformatted',
39             CODE => 'code',
40              
41             EMPHASIS => 'emphasis',
42             STRONG => 'strong',
43             UNDERLINE => 'underline',
44             STRIKE => 'strike',
45             TYPEWRITER => 'typewriter',
46             LINK => 'link',
47              
48             TEXT => 'normal text',
49 4     4   28 };
  4         8  
50              
51 4     4   22 use base 'Exporter';
  4         5  
  4         6804  
52              
53             our @EXPORT = qw();
54             our @EXPORT_OK = qw(
55             COMMENT VERBATIM
56             SECTION QUOTE LISTING ENUMERATION DESCRIPTION
57             TABLE RULE P PRE CODE
58             EMPHASIS STRONG UNDERLINE STRIKE TYPEWRITER LINK
59             TEXT
60             );
61             our %EXPORT_TAGS = (
62             generic => [qw(COMMENT VERBATIM)],
63             environment => [qw(SECTION QUOTE LISTING ENUMERATION DESCRIPTION)],
64             paragraphs => [qw(TABLE RULE P PRE CODE)],
65             inline => [qw(EMPHASIS STRONG UNDERLINE STRIKE TYPEWRITER LINK TEXT VERBATIM)],
66             types => [qw(
67             COMMENT VERBATIM
68             SECTION QUOTE LISTING ENUMERATION DESCRIPTION
69             TABLE RULE P PRE CODE
70             EMPHASIS STRONG UNDERLINE STRIKE TYPEWRITER LINK
71             TEXT
72             )],
73             );
74              
75             my $RE_INLINE_PRE = qr/[\s(]/;
76             my $RE_INLINE_POST = qr/[\s).!?,:;]|$/;
77              
78             my %DEFAULT_INLINE_RE = (
79             EMPHASIS() => {
80             open => qr/\//,
81             close => qr/\//,
82             },
83              
84             STRONG() => {
85             open => qr/\*/,
86             close => qr/\*/,
87             },
88              
89             UNDERLINE() => {
90             open => qr/_/,
91             close => qr/_/,
92             },
93              
94             STRIKE() => {
95             open => qr/-/,
96             close => qr/-/,
97             },
98              
99             TYPEWRITER() => {
100             open => qr/{/,
101             close => qr/}/,
102             },
103              
104             VERBATIM() => {
105             open => qr/{{/,
106             close => qr/}}/,
107             },
108              
109             LINK() => {
110             open => qr/\[[>=\#]?/,
111             close => qr/\]/,
112             code => sub {
113             my ($self, $type, $text, $match) = @_;
114              
115             (my $style = $match) =~ s/^\[//;
116             my ($target, $label) = split /\|/, $text, 2;
117              
118             $target =~ s/^\s+|\s+$//g;
119             $label =~ s/^\s+|\s+$//g if defined $label;
120              
121             return {
122             type => LINK,
123             label => $label,
124             target => $target,
125             style => $style,
126             };
127             },
128             },
129             );
130              
131             my %DEFAULT_PARA_RE = (
132             P() => {
133             open => qr/(?:.+?::\s)?/,
134             close => undef,
135             code => sub {
136             my ($self, $type, $text, $match) = @_;
137              
138             $match =~ s/\s*::\s+//;
139             $text .= "\n" unless $text =~ /\n$/;
140              
141             my $p = {
142             type => P,
143             text => $self->parse_paragraph($text)
144             };
145              
146             $p->{heading} = $match
147             if length($match);
148              
149             return $p;
150             },
151             },
152              
153             PRE() => {
154             open => qr/{\s/,
155             close => qr/(?:^|\s)}/,
156             code => sub {
157             my ($self, $type, $text) = @_;
158              
159             $text .= "\n" unless $text =~ /\n$/;
160              
161             return {
162             type => PRE,
163             text => $self->parse_paragraph($text)
164             };
165             },
166             },
167              
168             CODE() => {
169             open => qr/[!|]\s/,
170             close => undef,
171             filter => qr/[!|]($|\s)/,
172             },
173              
174             VERBATIM() => {
175             open => qr/{{\s/,
176             close => qr/(?:^|\s)}}/,
177             },
178              
179             RULE() => {
180             open => qr/-{3,}\n/,
181             close => qr//,
182             code => sub {
183             return { type => RULE };
184             },
185             },
186              
187             # TODO: fix column span vs empty cells
188             TABLE() => {
189             open => qr/\+[+-]*\+\n$/,
190             close => undef,
191             code => sub {
192             my ($self, $type, $text) = @_;
193              
194             my @rows = split /\n/, $text;
195             my $content = [];
196             for (my $i = 0; $i < @rows; ++$i) {
197             next if $rows[$i] =~ /^\+(?:-*\+)*$/;
198              
199             my $row = { cols => [] };
200              
201             $row->{heading} = 1
202             if ($i < @rows - 2) && ($rows[$i+1] =~ /^[+-]+$/);
203              
204             $rows[$i] =~ s/^\||\|$//g;
205              
206             my $span = 1;
207             foreach my $col (split /\|/, $rows[$i]) {
208             if ($col eq '') {
209             ++$span;
210              
211             } else {
212             $col =~ s/^\s+//; $col =~ s/\s+$//;
213             my $column = { text => $self->parse_paragraph($col) };
214             $column->{span} = $span if $span > 1;
215             push @{$row->{cols}}, $column;
216              
217             $span = 1;
218             }
219             }
220              
221             push @$content, $row;
222             }
223              
224             return { type => TABLE, content => $content };
225             },
226             },
227             );
228              
229             my %DEFAULT_ENVIRONMENT_RE = (
230             QUOTE() => {
231             open => qr/>\s/,
232             close => undef,
233             filter => qr/[> ]($|\s)/,
234             },
235              
236             LISTING() => {
237             open => qr/[*o-]\s/,
238             close => undef,
239             merge => 1,
240             },
241              
242             ENUMERATION() => {
243             open => qr/(?:\d+[.)]|\#)\s/,
244             close => undef,
245             merge => 1,
246             },
247              
248             DESCRIPTION() => {
249             open => qr/:.+?:\s/,
250             close => undef,
251             merge => 1,
252             code => sub {
253             my ($self, $type, $content, $match) = @_;
254             $match =~ s/^:|:\s$//g;
255             return [ $match, $content ];
256             },
257             },
258             );
259              
260             my %DEFAULT_SECTION_RE = (
261             open => qr/=+\s/,
262             close => qr/(?:^|\s)=+|^$/,
263             code => sub {
264             my ($self, $type, $heading, $content, $match) = @_;
265              
266             $heading =~ s/^\s+|\s+$//g;
267              
268             return {
269             type => SECTION,
270             level => scalar $match =~ tr/=//,
271             heading => $heading,
272             content => $content,
273             };
274             }
275             );
276              
277 4     4   2353 use Text::WikiText::InputFilter;
  4         11  
  4         7786  
278              
279             sub new {
280 4     4 1 4990 my $class = shift;
281              
282 4         10 my $self = {
283             };
284              
285 4         12 return bless $self, $class;
286             }
287              
288             sub parse_paragraph {
289 16     16 0 785 my ($self, $text) = @_;
290              
291 16         17 my @list;
292              
293 16         49 while (length $text) {
294 79         90 my $elem = undef;
295              
296 79         187 foreach my $type (keys %DEFAULT_INLINE_RE) {
297 521         676 my $def = $DEFAULT_INLINE_RE{$type};
298              
299 521 100       15876 if ($text =~ s/
300             ^($def->{open}) # opening markup
301             (\S|\S.*?\S) # content (no leading or trailing ws)
302             ($def->{close}) # closing markup
303             (?=$RE_INLINE_POST) # followed by sentence char or ws
304             //xs) {
305 9 100       46 $elem = exists $def->{code}
306             ? $def->{code}->($self, $type, $2, $1, $3)
307             : { type => $type, text => $2 };
308 9         17 last;
309             }
310             }
311              
312 79 100       231 if (! defined $elem) {
313 70 50       488 if ($text =~ s/^(.*?($RE_INLINE_PRE)+)//s) {
314 70         286 $elem = { type => TEXT, text => $& };
315             } else {
316 0         0 $elem = { type => TEXT, text => $text };
317 0         0 $text = '';
318             }
319             }
320              
321 79 100 100     495 if (@list && $list[-1]->{type} eq TEXT && $elem->{type} eq TEXT) {
      100        
322 49         182 $list[-1]->{text} .= $elem->{text};
323             } else {
324 30         78 push @list, $elem;
325             }
326             }
327              
328 16         57 return \@list;
329             }
330              
331             sub parse_parlike {
332 17     17 0 35 my ($self, $input, $filter, $close, $parbreak) = @_;
333              
334 17         25 my $para = '';
335 17         20 my $first = 1;
336 17         34 my $last;
337              
338 17   66     113 $input->push_filter($filter || qr//);
339              
340 17         36 local $_;
341              
342 17   33     44 while (
      33        
      66        
343             defined ($_ = $input->peek)
344             || (defined $close && defined ($_ = $input->readline) && /^\s*$/)
345             ) {
346 44 100 100     265 last if !defined $close && defined $parbreak && /^$parbreak/;
      100        
347              
348 38 100 100     226 $last = defined $close
349             ? s/$close\n?$//
350             : !$first && !defined $filter && s/^\n?$//;
351              
352 38         60 $para .= $_;
353 38         96 $input->commit;
354              
355 38         41 $first = 0;
356              
357 38 100       112 last if $last;
358             }
359              
360 17         52 $input->pop_filter;
361              
362 17 50 66     62 warn("Missing block terminator on input line " . $input->line_n . ".\n")
363             if defined $close && !$last;
364              
365 17         41 return $para;
366             }
367              
368             sub parse_atom {
369 17     17 0 34 my ($self, $input, $parbreak) = @_;
370              
371 17         41 my $line_n = $input->line_n;
372 17         21 my $atom = undef;
373              
374             # (foo) specials (end)
375 17 100       72 if ($input->match(qr/\((begin +)?[\w -]+\)\n/)) {
376 1         5 my $match = $input->last_match;
377 1         6 $match =~ s/^\((begin +)?| *\)\n//g;
378              
379 1         4 my @modifiers = split / +/, $match;
380 1         1 my $type = pop @modifiers;
381              
382 1         17 my $text =
383             $self->parse_parlike($input, undef, qr/\(end( +\Q$type\E)?\)/);
384              
385 1 50 33     12 $atom = exists $DEFAULT_PARA_RE{$type} && exists $DEFAULT_PARA_RE{$type}{code}
386             ? $DEFAULT_PARA_RE{$type}->{code}->($self, $type, $text, $match, [ @modifiers ])
387             : { type => $type, modifiers => [ @modifiers ], text => $text };
388              
389             } else {
390 16         47 foreach my $type (keys %DEFAULT_PARA_RE) {
391 79         101 my $def = $DEFAULT_PARA_RE{$type};
392              
393 79 100       221 if ($input->match($def->{open})) {
394 16         44 my $match = $input->last_match;
395 16         60 my $text = $self->parse_parlike(
396             $input, $def->{filter}, $def->{close}, $parbreak
397             );
398              
399 16 100       76 $atom = exists $def->{code}
400             ? $def->{code}->($self, $type, $text, $match)
401             : { type => $type, text => $text };
402 16         33 last;
403             }
404             }
405             }
406              
407 17 50       58 if (defined $atom) {
408 17         27 $atom->{line_n} = $line_n;
409 17         53 $input->flush_empty;
410             }
411              
412 17         37 return $atom;
413             }
414              
415              
416             my $RE_ALL_ENV =
417             eval "qr/" . (join "|", map { $_->{open} } values %DEFAULT_ENVIRONMENT_RE) . "|" . $DEFAULT_SECTION_RE{open} . "/";
418              
419             sub parse_block_list {
420 10     10 0 19 my ($self, $input, $filter, $close, $parbreak) = @_;
421              
422 10         13 my @list = ();
423 10         10 my $last;
424              
425 10         9 local $_;
426              
427 10         26 while (defined ($_ = $input->peek)) {
428 18 100 100     142 last if !defined $filter && /^$RE_ALL_ENV/;
429 10   33     30 $last = defined $close && s/$close\n?$//;
430              
431 10         21 push @list, $self->parse_block($input, $parbreak);
432              
433 10 50       35 last if $last;
434             }
435              
436 10         23 return \@list;
437             }
438              
439             sub parse_block {
440 21     21 0 31 my ($self, $input, $parbreak) = @_;
441              
442 21         23 my $block = undef;
443              
444 21         52 foreach my $type (keys %DEFAULT_ENVIRONMENT_RE) {
445 78         95 my $def = $DEFAULT_ENVIRONMENT_RE{$type};
446              
447 78 100       197 if ($input->match($def->{open})) {
448 4   66     24 $input->push_filter($def->{filter} || qr//);
449              
450 4 100       9 if ($def->{merge}) {
451 3         5 my $elements = [];
452              
453 3         4 do {
454 9         20 my $match = $input->last_match;
455              
456 9 50       22 if ($input->peek =~ /^\s*$/) {
457 0         0 $input->commit;
458 0         0 $input->flush_empty;
459             }
460              
461 9         29 my $content = $self->parse_block_list(
462             $input, $def->{filter}, $def->{close}, $def->{open}
463             );
464              
465 9 100       25 my $elem = exists $def->{code}
466             ? $def->{code}->($self, $type, $content, $match)
467             : $content;
468              
469 9         80 push @$elements, $elem;
470             } while ($input->match(qr/^$def->{open}/));
471              
472 3 50       99 $block = exists $def->{merge_code}
473             ? $def->{merge_code}->($self, $type, $elements)
474             : { type => $type, content => $elements };
475              
476             } else {
477 1         4 my $match = $input->last_match;
478              
479 1 50       3 if ($input->peek =~ /^\s*$/) {
480 0         0 $input->commit;
481 0         0 $input->flush_empty;
482             }
483              
484 1         3 my $content = $self->parse_block_list(
485             $input, $def->{filter}, $def->{close}
486             );
487              
488 1 50       26 $block = exists $def->{code}
489             ? $def->{code}->($self, $type, $content, $match)
490             : { type => $type, content => $content };
491             }
492              
493 4         12 $input->pop_filter;
494 4         11 $input->flush_empty;
495              
496 4         7 last;
497             }
498             }
499              
500 21 100       52 if (! defined $block) {
501 17         36 $block = $self->parse_atom($input, $parbreak);
502             }
503              
504 21         37 return $block;
505             }
506              
507             sub parse_struct_list {
508 0     0 0 0 my ($self, $input) = @_;
509              
510 0         0 my @list = ();
511 0         0 my $last;
512              
513 0         0 local $_;
514              
515 0         0 while (defined ($_ = $input->peek)) {
516 0 0       0 last if /^$DEFAULT_SECTION_RE{open}/;
517              
518 0         0 push @list, $self->parse_structure($input);
519              
520 0 0       0 last if $last;
521             }
522              
523 0         0 return \@list;
524             }
525              
526             sub parse_structure {
527 11     11 0 19 my ($self, $input) = @_;
528              
529 11         13 my $struct = undef;
530              
531             # = heading
532 11 50       33 if ($input->match($DEFAULT_SECTION_RE{open})) {
533 0         0 my $match = $input->last_match;
534 0         0 my $heading =
535             $self->parse_parlike($input, undef, $DEFAULT_SECTION_RE{close});
536              
537 0         0 $input->flush_empty;
538              
539 0         0 my $content =
540             $self->parse_struct_list($input);
541              
542 0 0       0 $struct = exists $DEFAULT_SECTION_RE{code}
543             ? $DEFAULT_SECTION_RE{code}->($self, SECTION, $heading, $content, $match)
544             : { type => SECTION, heading => $heading, content => $content };
545              
546             } else {
547 11         24 $struct = $self->parse_block($input);
548             }
549              
550 11         46 return $struct;
551             }
552              
553             sub parse {
554 3     3 1 2437 my ($self, $string_or_stream) = @_;
555              
556 3         13 my $input = Text::WikiText::InputFilter->new($string_or_stream);
557              
558 3         6 my @list;
559 3         16 while (defined $input->peek) {
560 11         28 push @list, $self->parse_structure($input);
561             }
562              
563 3         51 return \@list;
564             }
565              
566             sub convert {
567 0     0 1   my ($self, $string_or_stream, %opts) = @_;
568              
569 0   0       my $output_class = $opts{format} || 'HTML';
570 0   0       my $output_object = ref($output_class) && $output_class;
571              
572 0 0 0       $output_class = "Text::WikiText::Output::$output_class"
573             unless $output_object || $output_class =~ /::/;
574              
575 0 0         unless ($output_object) {
576 0 0         eval "require $output_class" or die $@;
577 0           $output_object = $output_class->new;
578             }
579              
580 0           my $parsed_structures = $self->parse($string_or_stream);
581 0           $output_object->dump($parsed_structures, %opts);
582             }
583              
584             1;
585              
586             __END__