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__ |