File Coverage

blib/lib/Text/WikiFormat.pm
Criterion Covered Total %
statement 176 176 100.0
branch 57 60 95.0
condition 32 38 84.2
subroutine 29 29 100.0
pod 2 15 13.3
total 296 318 93.0


line stmt bran cond sub pod time code
1             package Text::WikiFormat;
2              
3 14     14   308529 use strict;
  14         35  
  14         680  
4              
5 14     14   18796 use URI;
  14         140903  
  14         531  
6 14     14   232 use Carp ();
  14         41  
  14         423  
7 14     14   80 use URI::Escape;
  14         26  
  14         1193  
8 14     14   23835 use Text::WikiFormat::Blocks;
  14         46  
  14         96  
9 14     14   89 use Scalar::Util qw( blessed reftype );
  14         27  
  14         5431  
10              
11 14     14   242 use vars qw( $VERSION %tags $indent );
  14         188  
  14         18604  
12             $VERSION = '0.81';
13             $indent = qr/^(?:\t+|\s{4,})/;
14             %tags = (
15             indent => qr/^(?:\t+|\s{4,})/,
16             newline => '
',
17             link => \&make_html_link,
18             strong => sub { "$_[0]" },
19             emphasized => sub { "$_[0]" },
20             strong_tag => qr/'''(.+?)'''/,
21             emphasized_tag => qr/''(.+?)''/,
22              
23             code => [ '
', "
\n", '', "\n" ],
24             line => [ '', "\n", '
', "\n" ],
25             paragraph => [ '

', "

\n", '', "
\n", 1 ],
26             unordered => [ "
    \n", "
\n", '
  • ', "
  • \n" ],
    27             ordered => [ "
      \n", "
    \n",
    28             sub { qq|
  • |, $_[0], "
  • \n" } ],
    29             header => [ '', "\n", sub {
    30             my $level = length $_[2];
    31             return "", format_line($_[3], @_[-2, -1]), "\n" }
    32             ],
    33              
    34             blocks => {
    35             ordered => qr/^([\dA-Za-z]+)\.\s*/,
    36             unordered => qr/^\*\s*/,
    37             code => qr/^(?:\t+|\s{4,}) /,
    38             header => qr/^(=+) (.+) \1/,
    39             paragraph => qr/^/,
    40             line => qr/^-{4,}/,
    41             },
    42              
    43             indented => { map { $_ => 1 } qw( ordered unordered )},
    44             nests => { map { $_ => 1 } qw( ordered unordered ) },
    45              
    46             blockorder =>
    47             [qw( header line ordered unordered code paragraph )],
    48             extended_link_delimiters => [qw( [ ] )],
    49              
    50             schemas => [ qw( http https ftp mailto gopher ) ],
    51             );
    52              
    53             sub process_args
    54             {
    55 6     6 0 13 my $self = shift;
    56              
    57 6 50       21 return as => 'wikiformat' unless @_;
    58 6 100       27 return as => shift if @_ == 1;
    59 5         33 return as => 'wikiformat', @_;
    60             }
    61              
    62             sub default_opts
    63             {
    64 6     6 0 11 my ($class, $args) = @_;
    65              
    66             return
    67 24         74 implicit_links => 1,
    68 6         12 map { $_ => delete $args->{ $_ } }
    69             qw( prefix extended implicit_links absolute_links );
    70             }
    71              
    72             sub merge_hash
    73             {
    74 102     102 0 4510 my ($from, $to) = @_;
    75              
    76 102         437 while (my ($key, $value) = each %$from)
    77             {
    78 195 100 100     775 if ((reftype( $value ) || '' ) eq 'HASH' )
    79             {
    80 43 100       125 $to->{$key} = {} unless defined $to->{$key};
    81 43         115 merge_hash( $value, $to->{$key} );
    82 43         171 next;
    83             }
    84              
    85 152         571 $to->{$key} = $value;
    86             }
    87              
    88 102         215 return $to;
    89             }
    90              
    91             sub import
    92             {
    93 18     18   4846 my $class = shift;
    94 18 100       15022 return unless @_;
    95              
    96 6         23 my %args = $class->process_args( @_ );
    97 6         26 my %defopts = $class->default_opts( \%args );
    98              
    99 6         20 my $caller = caller();
    100 6         18 my $name = delete $args{as};
    101              
    102 14     14   109 no strict 'refs';
      14         31  
      14         11961  
    103 6         8293 *{ $caller . "::$name" } = sub
    104             {
    105 9     9   13988 my ($text, $tags, $opts) = @_;
    106              
    107 9   100     37 $tags ||= {};
    108 9   100     40 $opts ||= {};
    109              
    110 9         92 my %tags = %args;
    111 9         34 merge_hash( $tags, \%tags );
    112 9         50 my %opts = %defopts;
    113 9         26 merge_hash( $opts, \%opts );
    114              
    115 9         33 Text::WikiFormat::format( $text, \%tags, \%opts);
    116             }
    117 6         32 }
    118              
    119             sub format
    120             {
    121 45     45   44480 my ($text, $newtags, $opts) = @_;
    122              
    123 45   100     263 $opts ||=
    124             {
    125             prefix => '', extended => 0, implicit_links => 1, absolute_links => 0,
    126             nofollow_extended => 0
    127             };
    128              
    129 45         714 my %tags = %tags;
    130              
    131 45 100 50     522 merge_hash( $newtags, \%tags )
          66        
    132             if defined $newtags and ( reftype( $newtags ) || '' ) eq 'HASH';
    133 45 100 100     315 check_blocks( \%tags )
    134             if exists $newtags->{blockorder} or exists $newtags->{blocks};
    135              
    136 45         216 my @blocks = find_blocks( $text, \%tags, $opts );
    137 45         164 @blocks = merge_blocks( \@blocks );
    138 45         144 @blocks = nest_blocks( \@blocks );
    139 45         168 return process_blocks( \@blocks, \%tags, $opts );
    140             }
    141              
    142             sub check_blocks
    143             {
    144 18     18 1 2963 my $tags = shift;
    145 18         60 my %blocks = %{ $tags->{blocks} };
      18         98  
    146 18         43 delete @blocks{ @{ $tags->{blockorder} } };
      18         158  
    147              
    148 18 100       114 if (keys %blocks)
    149             {
    150 4         37 require Carp;
    151 4         737 Carp::carp(
    152             "No order specified for blocks '" . join(', ', keys %blocks )
    153             . "'\n"
    154             )
    155             }
    156             }
    157              
    158             sub find_blocks
    159             {
    160 45     45 0 80 my ($text, $tags, $opts) = @_;
    161              
    162 45         67 my @blocks;
    163 45         529 for my $line ( split(/\r?\n/, $text) )
    164             {
    165 253         1517 my $block = start_block( $line, $tags, $opts );
    166 253 100       1228 push @blocks, $block if $block;
    167             }
    168              
    169 45         232 return @blocks;
    170             }
    171              
    172             sub start_block
    173             {
    174 258     258 0 2256 my ($text, $tags, $opts) = @_;
    175 258 100       687 return new_block( 'end', level => 0 ) unless $text;
    176              
    177 180         197 for my $block (@{ $tags->{blockorder} })
      180         397  
    178             {
    179 809         1210 my ($line, $level, $indentation) = ( $text, 0, '' );
    180              
    181 809 100       1966 if ($tags->{indented}{$block})
    182             {
    183 297         550 ($level, $line, $indentation) = get_indentation( $tags, $line );
    184 297 100       960 next unless $level;
    185             }
    186              
    187 689         3497 my $marker_removed = length ($line =~ s/$tags->{blocks}{$block}//);
    188              
    189 689 100       10880 next unless $marker_removed;
    190              
    191 1602         4418 return new_block( $block,
    192 178   100     393 args => [ grep { defined } $1, $2, $3, $4, $5, $6, $7, $8, $9 ],
    193             level => $level || 0,
    194             opts => $opts,
    195             text => $line,
    196             tags => $tags,
    197             );
    198             }
    199             }
    200              
    201             # merge_blocks() and nest_blocks()
    202             BEGIN
    203             {
    204 14     14   40 for my $op (qw( merge nest ))
    205             {
    206 14     14   90 no strict 'refs';
      14         32  
      14         2198  
    207 28         19972 *{ $op . '_blocks' } = sub
    208             {
    209 95     95   5289 my $blocks = shift;
    210 95 100       237 return unless @$blocks;
    211              
    212 93         191 my @processed = shift @$blocks;
    213              
    214 93         172 for my $block (@$blocks)
    215             {
    216 358         1508 push @processed, $processed[-1]->$op( $block );
    217             }
    218            
    219 93         572 return @processed;
    220 28         215 };
    221             }
    222             }
    223              
    224             sub process_blocks
    225             {
    226 46     46 0 134 my ($blocks, $tags, $opts) = @_;
    227              
    228 46         119 my @open;
    229 46         96 for my $block (@$blocks)
    230             {
    231 184 100       614 push @open, process_block( $block, $tags, $opts )
    232             unless $block->type() eq 'end';
    233             }
    234              
    235 46         378 return join('', @open);
    236             }
    237              
    238             sub process_block
    239             {
    240 116     116 0 244 my ($block, $tags, $opts) = @_;
    241              
    242 116         346 my ($start, $end, $start_line, $end_line, $between)
    243 116         134 = @{ $tags->{ $block->type() } };
    244              
    245 116         169 my @text;
    246              
    247 116         493 for my $line ( $block->formatted_text() )
    248             {
    249 195 100       620 if (blessed( $line ))
    250             {
    251 11   33     42 my $prev_end = pop @text || ();
    252 11         50 push @text, process_block( $line, $tags, $opts ), $prev_end;
    253 11         25 next;
    254             }
    255              
    256 184 100 100     863 if ((reftype( $start_line ) || '' ) eq 'CODE' )
    257             {
    258 36         120 (my $start_line, $line, $end_line) =
    259             $start_line->(
    260             $line, $block->level(), $block->shift_args(), $tags, $opts
    261             );
    262 36         96 push @text, $start_line;
    263             }
    264             else
    265             {
    266 148         228 push @text, $start_line;
    267             }
    268 184         426 push @text, $line, $end_line;
    269             }
    270              
    271 116 100       313 pop @text if $between;
    272 116         11281 return join('', $start, @text, $end);
    273             }
    274              
    275             sub get_indentation
    276             {
    277 297     297 0 411 my ($tags, $text) = @_;
    278              
    279 297 100       2573 return 0, $text unless $text =~ s/($tags->{indent})//;
    280 177         876 return( length( $1 ) + 1, $text, $1 );
    281             }
    282              
    283             sub format_line
    284             {
    285 182     182 1 11592 my ($text, $tags, $opts) = @_;
    286 182   100     426 $opts ||= {};
    287              
    288 182         752 $text =~ s!$tags->{strong_tag}!$tags->{strong}->($1, $opts)!eg;
      5         18  
    289 182         534 $text =~ s!$tags->{emphasized_tag}!$tags->{emphasized}->($1, $opts)!eg;
      5         16  
    290              
    291 182 100       464 $text = find_extended_links( $text, $tags, $opts ) if $opts->{extended};
    292              
    293 182 100 100     1120 $text =~ s|(?=])\b((?:[A-Z][a-z0-9]\w*){2,})|
    294 14         272 $tags->{link}->($1, $opts)|egx
    295             if !defined $opts->{implicit_links} or $opts->{implicit_links};
    296              
    297 182         945 return $text;
    298             }
    299              
    300             sub find_innermost_balanced_pair
    301             {
    302 42     42 0 73 my ($text, $open, $close) = @_;
    303              
    304 42         83 my $start_pos = rindex( $text, $open );
    305 42 100       160 return if $start_pos == -1;
    306              
    307 15         26 my $end_pos = index( $text, $close, $start_pos );
    308 15 50       32 return if $end_pos == -1;
    309              
    310 15         21 my $open_length = length( $open );
    311 15         21 my $close_length = length( $close );
    312 15         23 my $close_pos = $end_pos + $close_length;
    313 15         25 my $enclosed_length = $close_pos - $start_pos;
    314              
    315 15         33 my $enclosed_atom = substr( $text, $start_pos, $enclosed_length );
    316 15         94 return substr( $enclosed_atom, $open_length, 0 - $close_length ),
    317             substr( $text, 0, $start_pos ),
    318             substr( $text, $close_pos );
    319             }
    320              
    321             sub find_extended_links
    322             {
    323 27     27 0 46 my ($text, $tags, $opts) = @_;
    324              
    325 27         37 my $schemas = join('|', @{$tags->{schemas}});
      27         92  
    326 27 100       322 $text =~ s!(^|\s+)(($schemas):\S+)!$1 . $tags->{link}->($2, $opts)!egi
      8         27  
    327             if $opts->{absolute_links};
    328              
    329 27         47 my ($start, $end) = @{ $tags->{extended_link_delimiters} };
      27         69  
    330              
    331 27         73 while (my @pieces = find_innermost_balanced_pair( $text, $start, $end ) )
    332             {
    333 15 50       25 my ($tag, $before, $after) = map { defined $_ ? $_ : '' } @pieces;
      45         117  
    334 15   100     48 my $extended = $tags->{link}->( $tag, $opts ) || '';
    335 15         94 $text = $before . $extended . $after;
    336             };
    337              
    338 27         76 return $text;
    339             }
    340              
    341             sub make_html_link
    342             {
    343 34     34 0 84 my ($link, $opts) = @_;
    344 34   50     84 $opts ||= {};
    345              
    346 34         97 ($link, my $title) = find_link_title( $link, $opts );
    347 34         100 ($link, my $is_relative) = escape_link( $link, $opts );
    348              
    349 34 100 66     2026 my $prefix = ( defined $opts->{prefix} && $is_relative )
    350             ? $opts->{prefix} : '';
    351              
    352 34 100 100     151 my $nofollow = (!$is_relative && $opts->{nofollow_extended})
    353             ? ' rel="nofollow"' : '';
    354              
    355 34         271 return qq|$title|;
    356             }
    357              
    358             sub escape_link
    359             {
    360 34     34 0 59 my ($link, $opts) = @_;
    361              
    362 34         175 my $u = URI->new( $link );
    363 34 100       70607 return $link if $u->scheme();
    364              
    365             # it's a relative link
    366 26         1267 return( uri_escape( $link ), 1 );
    367             }
    368              
    369             sub find_link_title
    370             {
    371 34     34 0 67 my ($link, $opts) = @_;
    372 34         44 my $title;
    373              
    374 34 100       143 ($link, $title) = split(/\|/, $link, 2) if $opts->{extended};
    375 34 100       321 $title = $link unless $title;
    376              
    377 34         187 return $link, $title;
    378             }
    379              
    380             'shamelessly adapted from the Jellybean project';
    381              
    382             __END__