| 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 " |
|||||||
| 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 => [ ' |
|||||||
| 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__ |