File Coverage

blib/lib/Text/GooglewikiFormat.pm
Criterion Covered Total %
statement 170 176 96.5
branch 45 52 86.5
condition 21 31 67.7
subroutine 26 26 100.0
pod 0 14 0.0
total 262 299 87.6


~;
line stmt bran cond sub pod time code
1             package Text::GooglewikiFormat;
2            
3 4     4   223830 use warnings;
  4         10  
  4         137  
4 4     4   26 use strict;
  4         6  
  4         134  
5 4     4   4968 use URI;
  4         560132  
  4         143  
6 4     4   44 use URI::Escape;
  4         7  
  4         309  
7 4     4   2958 use Text::GooglewikiFormat::Blocks;
  4         11  
  4         26  
8 4     4   28 use Scalar::Util qw( blessed reftype );
  4         7  
  4         200  
9 4     4   3774 use URI::Find;
  4         10009  
  4         242  
10            
11 4     4   28 use vars qw( $VERSION %tags $indent $code_delimiters);
  4         6  
  4         7237  
12             $VERSION = '0.05';
13             $indent = qr/^(?:\t+|\s{4,})/;
14             $code_delimiters = 0;
15             %tags = (
16             indent => qr/^(?:\t+|\s{1,})/,
17             newline => '
',
18            
19             strong => sub { " $_[0] " },
20             italic => sub { " $_[0] " },
21             strike => sub { qq~ $_[0] ~ },
22             superscript => sub { "$_[0]" },
23             subscript => sub { "$_[0]" },
24             inline => sub { "$_[0]" },
25             strong_tag => qr/(^|\s+)\*(.+?)\*(\s+|$)/,
26             italic_tag => qr/(^|\s+)_(.+?)_(\s+|$)/,
27             strike_tag => qr/(^|\s+)\~\~(.+?)\~\~(\s+|$)/,
28             superscript_tag => qr/\^(.+?)\^/,
29             subscript_tag => qr/\,\,(.+?)\,\,/,
30             inline_tag => qr/\`(.+?)\`/,
31            
32             header => [ '', '', sub {
33             my $level = length $_[2];
34             return "", format_line($_[3], @_[-2, -1]), "" }
35             ],
36             unordered => ["
    ", "
", '
  • ', "
  • "],
    37             ordered => ["
      ", "
    ", '
  • ', "
  • "],
    38            
    39             code => [ '
    ', "
    ", sub {
    40             my ($line, $level, $args, $tags, $opts) = @_;
    41             $line =~ s/(^\{\{\{|\}\}\}$)//isg;
    42             return (length($line)) ? $line . "\n" : '';
    43             } ],
    44             paragraph => [ '

    ', "

    ", '', "
    ", 1 ],
    45             quote => [ '
    ', "
    ", '', "\n"],
    46             table => [ '', '
    ', sub {
    47             my ($line, $level, $args, $tags, $opts) = @_;
    48             $line =~ s/(^\|\||\|\|$)//isg;
    49             $line =~ s/\|\|/\<\/td\>\/isg;
    50             $line = qq~
    $line
    51             return $line,
    52             } ],
    53            
    54            
    55             blocks => {
    56             header => qr/^(=+)(.+)\1/,
    57             ordered => qr/^\#\s*/,
    58             unordered => qr/^\*\s*/,
    59             quote => qr/^ /,
    60             paragraph => qr/^/,
    61             table => qr/^\|\|/,
    62             },
    63            
    64             indented => { map { $_ => 1 } qw( ordered unordered )},
    65             nests => { map { $_ => 1 } qw( ordered unordered code table ) },
    66            
    67             blockorder =>
    68             [qw( header ordered unordered table quote paragraph code )],
    69            
    70             link => \&make_html_link,
    71             extended_link_delimiters => [qw( [ ] )],
    72             schemas => [ qw( http https ftp mailto gopher ) ],
    73             );
    74            
    75             sub merge_hash {
    76 4     4 0 8 my ($from, $to) = @_;
    77            
    78 4         21 while (my ($key, $value) = each %$from)
    79             {
    80 40 100 100     167 if ((reftype( $value ) || '' ) eq 'HASH' )
    81             {
    82 3 50       10 $to->{$key} = {} unless defined $to->{$key};
    83 3         13 merge_hash( $value, $to->{$key} );
    84 3         11 next;
    85             }
    86            
    87 37         147 $to->{$key} = $value;
    88             }
    89            
    90 4         9 return $to;
    91             }
    92            
    93             sub format {
    94 15     15 0 17063 my ($text, $newtags, $opts) = @_;
    95            
    96 15   100     153 $opts ||=
    97             {
    98             prefix => '', extended => 1, implicit_links => 1, absolute_links => 1
    99             };
    100            
    101 15         296 my %tags = %tags;
    102            
    103 15 100 50     103 merge_hash( $newtags, \%tags )
          66        
    104             if defined $newtags and ( reftype( $newtags ) || '' ) eq 'HASH';
    105 15 100 66     91 check_blocks( \%tags )
    106             if exists $newtags->{blockorder} or exists $newtags->{blocks};
    107            
    108             # find URIs
    109             my $finder = URI::Find->new( sub {
    110 6     6   41395 my($uri, $orig_uri) = @_;
    111             # If your link points to an image (that is, if it ends in .png, .gif, .jpg or .jpeg), it will get inserted as an image into the page:
    112 6 100       19 if ($uri =~ /\.(jpe?g|png|gif)$/) {
    113 2         20 return qq| |;
    114             } else {
    115 4         32 return qq|[$uri]|;
    116             }
    117 15         154 } );
    118 15         253 $finder->find(\$text);
    119 15         59004 $text =~ s/\[\[(.+?)\]/\[$1/isg; # dirty hack
    120            
    121 15         50 my @blocks = find_blocks( $text, \%tags, $opts );
    122 15         50 @blocks = merge_blocks( \@blocks );
    123 15         50 @blocks = nest_blocks( \@blocks );
    124            
    125 15         53 return process_blocks( \@blocks, \%tags, $opts );
    126             }
    127            
    128             sub check_blocks
    129             {
    130 1     1 0 2 my $tags = shift;
    131 1         2 my %blocks = %{ $tags->{blocks} };
      1         6  
    132 1         4 delete @blocks{ @{ $tags->{blockorder} } };
      1         6  
    133            
    134 1 50       8 if (keys %blocks)
    135             {
    136 0         0 require Carp;
    137 0         0 Carp::carp(
    138             "No order specified for blocks '" . join(', ', keys %blocks )
    139             . "'\n"
    140             )
    141             }
    142             }
    143            
    144             sub find_blocks
    145             {
    146 15     15 0 33 my ($text, $tags, $opts) = @_;
    147            
    148 15         21 my @blocks;
    149 15         123 for my $line ( split(/\r?\n/, $text) )
    150             {
    151 68         150 my $block = start_block( $line, $tags, $opts );
    152 68 50       342 push @blocks, $block if $block;
    153             }
    154            
    155 15         80 return @blocks;
    156             }
    157            
    158             sub start_block
    159             {
    160 68     68 0 109 my ($text, $tags, $opts) = @_;
    161 68 100       182 return new_block( 'end', level => 0 ) unless $text;
    162            
    163             # for {{{ }}}
    164 57 100 100     452 if ($text =~ /^\}\}\}$/) {
        100          
    165 3         8 $code_delimiters = 0;
    166 3         11 return new_block( 'end', level => 1 );
    167             } elsif ($code_delimiters or $text =~ /^\{\{\{$/) {
    168 7         11 $code_delimiters = 1;
    169 7         25 return new_block( 'code', level => 1, text => $text, opts => $opts, tags => $tags );
    170             }
    171            
    172 47         63 for my $block (@{ $tags->{blockorder} })
      47         135  
    173             {
    174 208         352 my ($line, $level, $indentation) = ( $text, 0, '' );
    175            
    176 208 100       548 if ($tags->{indented}{$block})
    177             {
    178 80         173 ($level, $line, $indentation) = get_indentation( $tags, $line );
    179 80 100       268 next unless $level;
    180             }
    181            
    182 142         742 my $marker_removed = length ($line =~ s/$tags->{blocks}{$block}//);
    183            
    184 142 100       401 next unless $marker_removed;
    185            
    186 423         1013 return new_block( $block,
    187 47   100     107 args => [ grep { defined } $1, $2, $3, $4, $5, $6, $7, $8, $9 ],
    188             level => $level || 0,
    189             opts => $opts,
    190             text => $line,
    191             tags => $tags,
    192             );
    193             }
    194             }
    195            
    196             # merge_blocks() and nest_blocks()
    197             BEGIN
    198             {
    199 4     4   20 for my $op (qw( merge nest ))
    200             {
    201 4     4   28 no strict 'refs';
      4         7  
      4         468  
    202 8         5101 *{ $op . '_blocks' } = sub
    203             {
    204 30     30   42 my $blocks = shift;
    205 30 50       71 return unless @$blocks;
    206            
    207 30         60 my @processed = shift @$blocks;
    208            
    209 30         1074 for my $block (@$blocks)
    210             {
    211 78         370 push @processed, $processed[-1]->$op( $block );
    212             }
    213            
    214 30         183 return @processed;
    215 8         63 };
    216             }
    217             }
    218            
    219             sub process_blocks
    220             {
    221 15     15 0 29 my ($blocks, $tags, $opts) = @_;
    222            
    223 15         18 my @open;
    224 15         31 for my $block (@$blocks)
    225             {
    226 40 100       144 push @open, process_block( $block, $tags, $opts )
    227             unless $block->type() eq 'end';
    228             }
    229            
    230 15         220 return join('', @open);
    231             }
    232            
    233             sub process_block {
    234 27     27 0 46 my ($block, $tags, $opts) = @_;
    235            
    236 27         89 my ($start, $end, $start_line, $end_line, $between)
    237 27         35 = @{ $tags->{ $block->type() } };
    238            
    239 27         41 my @text;
    240 27         153 for my $line ( $block->formatted_text() )
    241             {
    242 54 50       200 if (blessed( $line ))
    243             {
    244 0   0     0 my $prev_end = pop @text || ();
    245 0         0 push @text, process_block( $line, $tags, $opts ), $prev_end;
    246 0         0 next;
    247             }
    248            
    249 54 100 100     271 if ((reftype( $start_line ) || '' ) eq 'CODE' )
    250             {
    251 26         90 (my $start_line, $line, $end_line) =
    252             $start_line->(
    253             $line, $block->level(), $block->shift_args(), $tags, $opts
    254             );
    255 26         62 push @text, $start_line;
    256             }
    257             else
    258             {
    259 28         42 push @text, $start_line;
    260             }
    261 54         134 push @text, $line, $end_line;
    262             }
    263            
    264 27 100       75 pop @text if $between;
    265            
    266 27         955 @text = grep { defined $_ } @text; # remove warnings
      151         577  
    267 27         254 return join('', $start, @text, $end);
    268             }
    269            
    270             sub get_indentation
    271             {
    272 80     80 0 115 my ($tags, $text) = @_;
    273            
    274 80 100       663 return 0, $text unless $text =~ s/($tags->{indent})//;
    275 14         76 return( length( $1 ) + 1, $text, $1 );
    276             }
    277            
    278             sub format_line {
    279 53     53 0 83 my ($text, $tags, $opts) = @_;
    280 53   50     121 $opts ||= {};
    281            
    282 53         242 $text =~ s!$tags->{strong_tag}!$tags->{strong}->($2, $opts)!eg;
      8         28  
    283 53         228 $text =~ s!$tags->{italic_tag}!$tags->{italic}->($2, $opts)!eg;
      6         26  
    284 53         288 $text =~ s!$tags->{strike_tag}!$tags->{strike}->($2, $opts)!eg;
      2         10  
    285 53         170 $text =~ s!$tags->{superscript_tag}!$tags->{superscript}->($1, $opts)!eg;
      2         9  
    286 53         165 $text =~ s!$tags->{subscript_tag}!$tags->{subscript}->($1, $opts)!eg;
      2         17  
    287 53         246 $text =~ s!$tags->{inline_tag}!$tags->{inline}->($1, $opts)!eg;
      2         9  
    288            
    289 53         122 $text = find_extended_links( $text, $tags, $opts );
    290            
    291 53         158 $text =~ s|(?=])\b((?:[A-Z][a-z0-9]\w*){2,})|
    292 3         17 $tags->{link}->($1, $opts)|egx;
    293            
    294 53         239 return $text;
    295             }
    296            
    297             sub find_innermost_balanced_pair
    298             {
    299 57     57 0 81 my ($text, $open, $close) = @_;
    300            
    301 57         95 my $start_pos = rindex( $text, $open );
    302 57 100       1257 return if $start_pos == -1;
    303            
    304 4         8 my $end_pos = index( $text, $close, $start_pos );
    305 4 50       10 return if $end_pos == -1;
    306            
    307 4         8 my $open_length = length( $open );
    308 4         6 my $close_length = length( $close );
    309 4         7 my $close_pos = $end_pos + $close_length;
    310 4         6 my $enclosed_length = $close_pos - $start_pos;
    311            
    312 4         12 my $enclosed_atom = substr( $text, $start_pos, $enclosed_length );
    313 4         25 return substr( $enclosed_atom, $open_length, 0 - $close_length ),
    314             substr( $text, 0, $start_pos ),
    315             substr( $text, $close_pos );
    316             }
    317            
    318             sub find_extended_links
    319             {
    320 53     53 0 84 my ($text, $tags, $opts) = @_;
    321            
    322 53         62 my $schemas = join('|', @{$tags->{schemas}});
      53         155  
    323 53         357 $text =~ s!(\s+)(($schemas):\S+)!$1 . $tags->{link}->($2, $opts)!egi;
      0         0  
    324            
    325 53         64 my ($start, $end) = @{ $tags->{extended_link_delimiters} };
      53         118  
    326            
    327 53         119 while (my @pieces = find_innermost_balanced_pair( $text, $start, $end ) )
    328             {
    329 4 50       8 my ($tag, $before, $after) = map { defined $_ ? $_ : '' } @pieces;
      12         32  
    330 4   50     11 my $extended = $tags->{link}->( $tag, $opts ) || '';
    331 4         20 $text = $before . $extended . $after;
    332             };
    333            
    334 53         154 return $text;
    335             }
    336            
    337             sub make_html_link {
    338 7     7 0 19 my ($link, $opts) = @_;
    339 7   50     20 $opts ||= {};
    340            
    341 7         21 ($link, my $title) = find_link_title( $link, $opts );
    342 7         23 ($link, my $is_relative) = escape_link( $link, $opts );
    343            
    344 7 100 66     224 my $prefix = ( defined $opts->{prefix} && $is_relative )
    345             ? $opts->{prefix} : '';
    346            
    347 7 100       19 unless ($is_relative) {
    348 4         26 return qq|$title|;
    349             } else {
    350 3         25 return qq|$title|;
    351             }
    352             }
    353            
    354             sub escape_link {
    355 7     7 0 11 my ($link, $opts) = @_;
    356            
    357 7         41 my $u = URI->new( $link );
    358 7 100       6923 return $link if $u->scheme();
    359            
    360             # it's a relative link
    361 3         400 return( uri_escape( $link ), 1 );
    362             }
    363            
    364             sub find_link_title {
    365 7     7 0 12 my ($link, $opts) = @_;
    366 7         10 my $title;
    367            
    368 7         22 ($link, $title) = split(/\s+/, $link, 2);
    369 7 100       22 $title = $link unless $title;
    370            
    371 7         24 return $link, $title;
    372             }
    373            
    374             'shamelessly adapted from the Jellybean project, directly from Text::WikiFormat';
    375            
    376             __END__