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