| 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	=> [ " 
 | ||||||
| 27 | ordered		=> [ " 
 | ||||||
| 28 | sub { qq| | ||||||
| 29 | header => [ '', "\n", sub { | ||||||
| 30 | my $level = length $_[2]; | ||||||
| 31 | return " | ||||||
| 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__ |