File Coverage

lib/Spoon/Formatter.pm
Criterion Covered Total %
statement 204 230 88.7
branch 46 74 62.1
condition 13 24 54.1
subroutine 41 46 89.1
pod 0 9 0.0
total 304 383 79.3


line stmt bran cond sub pod time code
1             package Spoon::Formatter;
2 4     4   27895 use Spoon::Base -Base;
  4         8  
  4         54  
3 4     4   9302  
  4     4   10  
  4         156  
  4         22  
  4         9  
  4         2328  
4             const class_id => 'formatter';
5             stub 'top_class';
6              
7 4     4 0 14 sub new {
8 4         15 $self = super;
9 4         398 $self->hub;
10 4         36 return $self;
11             }
12              
13 6     6 0 3537 sub text_to_html {
14 6         20 $self->text_to_parsed(@_)->to_html;
15             }
16              
17 6     6 0 8 sub text_to_parsed {
18 6         23 $self->top_class->new(text => shift)->parse;
19             }
20              
21 16   66 16 0 112 sub table { $self->{table} ||= $self->create_table }
  16         72  
22              
23 1     1 0 3 sub create_table {
24 1         6 my $class_prefix = $self->class_prefix;
25 2 50       27 my %table = map {
26 1         8 my $class = /::/ ? $_ : "$class_prefix$_";
27 2 50       20 $class->can('formatter_id') ? ($class->formatter_id, $class) : ();
28             } $self->formatter_classes;
29 1         28 \ %table;
30             }
31              
32 6   66 6 0 42 sub wafl_table { $self->{wafl_table} ||= $self->create_wafl_table }
  6         130  
33              
34 1     1 0 2 sub create_wafl_table {
35 1         4 my $class_prefix = $self->class_prefix;
36 0 0       0 my %table = map {
37 1         15 my $class = /::/ ? $_ : "$class_prefix$_";
38 0 0       0 $class->can('wafl_id') ? ($class->wafl_id, $class) : ();
39             } $self->wafl_classes;
40 1         9 $self->add_external_wafl(\ %table);
41 1         31 \ %table;
42             }
43              
44 1     1 0 2 sub add_external_wafl {
45 1 50       5 return unless $self->hub->registry_loaded;
46 0         0 my $table = shift;
47 0         0 my $map = $self->hub->registry->lookup->wafl;
48 0         0 for my $wafl_id (keys %$map) {
49 0         0 $table->{$wafl_id} = $map->{$wafl_id};
50             }
51             }
52              
53 1     1 0 3 sub wafl_classes { () }
  1         2  
54              
55             package Spoon::Formatter::Unit;
56 4     4   28 use Spoon::Base -Base;
  4         7  
  4         38  
57 4     4   1948 use Scalar::Util qw(weaken);
  4         8  
  4         6836  
58              
59             const formatter_id => '';
60             const html_start => '';
61             const html_end => '';
62             const contains_blocks => [];
63             const contains_phrases => [];
64             # stub 'pattern_start'; # XXX messes multiple inheritance
65             const pattern_end => qr/.*?/;
66              
67             field text => '';
68             field units => [];
69             field start_offset => 0;
70             field start_end_offset => 0;
71             # XXX this field is never used
72             #field end_start_offset => 0;
73             field end_offset => 0;
74             field matched => '';
75             field -weak => 'next_unit';
76             field -weak => 'prev_unit';
77              
78 8     8   323 sub parse {
79 8         44 $self->parse_blocks;
80 8         226 my $units = $self->units;
81              
82 8 100 66     81 if (@$units == 1 and not ref $units->[0] and @{$self->contains_phrases}) {
  6   100     24  
83 4         115 $self->text(shift @$units);
84 4         160 $self->start_offset(0);
85 4         124 $self->end_offset(0);
86 4         44 $self->parse_phrases;
87             }
88 8         40 return $self;
89             }
90              
91 8     8   10 sub link_units {
92 8         11 my $units = shift;
93 8         22 for (my $i = 0; $i < @$units; $i++) {
94 8 100       32 next unless ref $units->[$i];
95 2         66 $units->[$i]->next_unit($units->[$i + 1]);
96 2 50       27 $units->[$i]->prev_unit($units->[$i - 1]) if $i;
97             }
98             }
99              
100             # XXX extracted to allow performance analysis
101             # very similar to match_phrase_format_id, so
102             # room for refactor there
103             #
104             # Instead of calling $unit->match make it
105             # possible to call $class->match and have it
106             # work
107 8     8   11 sub match_block_format_id {
108 8         20 my ($contains, $table, $text) = @_;
109 8         9 my $match;
110 8         16 for my $format_id (@$contains) {
111 6 50       16 my $class = $table->{$format_id}
112             or die "No class for $format_id";
113 6         24 my $unit = $class->new;
114 6         190 $unit->text($text);
115 6 100       66 $unit->match or next;
116 2 50 33     43 $match = $unit
117             if not defined $match or
118             $unit->start_offset < $match->start_offset;
119 2 50       47 last unless $match->start_offset;
120             }
121 8         84 return $match;
122             }
123            
124 8     8   10 sub parse_blocks {
125 8         185 my $text = $self->text;
126 8         288 $self->text(undef);
127 8         228 my $units = $self->units;
128 8         83 my $table = $self->hub->formatter->table;
129 8         511 my $contains = $self->contains_blocks;
130 8         71 while ($text) {
131 8         29 my $match = $self->match_block_format_id($contains, $table, $text);
132 8 100       19 if (not defined $match) {
133 6         12 push @$units, $text;
134 6         10 last;
135             }
136 2 50       47 push @$units, substr($text, 0, $match->start_offset)
137             if $match->start_offset;
138 2         56 $text = substr($text, $match->end_offset);
139 2         27 $match->unit_match;
140 2         6 push @$units, $match;
141             }
142 8         29 $self->link_units($units);
143 8         10 $_->parse for grep ref($_), @{$self->units};
  8         181  
144             }
145              
146 0     0   0 sub match {
147 0 0       0 return unless $self->text =~ $self->pattern_block;
148 0         0 $self->set_match;
149             }
150              
151             # XXX extracted to allow performance analysis
152             # very similar to match_block_format_id, so
153             # room for refactor
154 12     12   14 sub match_phrase_format_id {
155 12         18 my ($contains, $table, $text) = @_;
156 12         12 my $match;
157 12         22 for my $format_id (@$contains) {
158 8 50       21 my $class = $table->{$format_id}
159             or die "No class for $format_id";
160             # XXX why do we make a new one every time, instead of
161             # just setting text and doing the match? Ah, tests
162             # show they carry some state. oh well
163 8         37 my $unit = $class->new;
164 8         251 $unit->text($text);
165 8 100       82 $unit->match_phrase or next;
166 4 50 33     15 $match = $unit
167             if not defined $match or
168             $unit->start_offset < $match->start_offset;
169 4 50       88 last if $match->start_offset == 0;
170             }
171 12         151 return $match;
172             }
173              
174 8     8   9 sub parse_phrases {
175 8         171 my $text = $self->text;
176 8         208 $self->text(undef);
177 8         218 my $units = $self->units;
178 8         62 my $table = $self->hub->formatter->table;
179 8         40 my $contains = $self->contains_phrases;
180 8         35 while ($text) {
181 12         41 my $match = $self->match_phrase_format_id($contains, $table, $text);
182 12 100       264 if ($self->start_end_offset) {
183 4 50       33 if ($text =~ $self->pattern_end) {
184 4 50 33     41 if (not defined $match or $-[0] < $match->start_offset) {
185 4         16 push @$units, substr($text, 0, $-[0]);
186 4         362 return substr($text, $+[0]);
187             }
188             }
189             else {
190 0         0 $self->end_offset(length $text);
191 0         0 push @$units, $text;
192 0         0 return '';
193             }
194             }
195 8 100       67 if (not defined $match) {
196 4         7 push @$units, $text;
197 4         9 return '';
198             }
199             # XXX: this code is never called (as far as we know...)
200             # if ($match->end_start_offset) {
201             # push @$units, $match;
202             # $text = substr($text, $match->end_offset);
203             # next;
204             # }
205 4 50       85 push @$units, substr($text, 0, $match->start_offset)
206             if $match->start_offset;
207 4         126 $text = substr($text, $match->start_end_offset);
208 4         103 $match->text($text);
209 4         39 $text = $match->parse_phrases;
210 4         15 $match->unit_match;
211 4         11 push @$units, $match;
212             }
213             }
214              
215             # empty for hooking
216 6     6   10 sub unit_match {
217             }
218              
219 8     8   655 sub match_phrase {
220 8 100       190 return unless $self->text =~ $self->pattern_start;
221 4         158 $self->start_offset($-[0]);
222 4         126 $self->start_end_offset($+[0]);
223 4         119 $self->matched(substr($self->text, $-[0], $+[0] - $-[0]));
224 4 50       164 my $pattern_end = $self->pattern_end
225             or return 1;
226 4         101 return substr($self->text, $+[0]) =~ $pattern_end;
227             }
228              
229 2     2   5 sub set_match {
230 2         5 my ($text, $start, $end) = @_;
231 2 50       6 $text = $1 unless defined $text;
232 2 50       6 $text = '' unless defined $text;
233 2 50       11 $start = $-[0] unless defined $start;
234 2 50       8 $end = $+[0] unless defined $end;
235 2         50 $self->text($text);
236 2         64 $self->start_offset($start);
237 2         62 $self->end_offset($end);
238 2         16 return 1;
239             }
240              
241 12     12   15 sub to_html {
242 12         263 my $units = $self->units;
243 12         99 for (my $i = 0; $i < @$units; $i ++) {
244 16 100       85 $units->[$i] = $self->escape_html($units->[$i])
245             unless ref $units->[$i];
246             }
247 12         48 $self->html;
248             }
249              
250 12     12   15 sub html {
251 16 100       291 my $inner = $self->text_filter(join '',
252             map {
253 12         272 ref($_) ? $_->to_html : $_;
254 12         15 } @{$self->units}
255             );
256 12         46 $self->html_start . $inner . $self->html_end;
257             }
258              
259 12     12   47 sub text_filter { shift }
  12         24  
260              
261 10     10   14 sub escape_html { $self->html_escape(shift) }
  10         225  
262              
263             ################################################################################
264             package Spoon::Formatter::Container;
265 4     4   32 use base 'Spoon::Formatter::Unit';
  4         8  
  4         37  
266 6     6   11 sub contains_blocks {
267 6         17 $self->hub->formatter->all_blocks;
268             }
269              
270             ################################################################################
271             package Spoon::Formatter::Block;
272 4     4   2491 use base 'Spoon::Formatter::Unit';
  4         8  
  4         20  
273 0     0   0 sub contains_phrases {
274 0         0 $self->hub->formatter->all_phrases;
275             }
276              
277             ################################################################################
278             package Spoon::Formatter::Phrase;
279 4     4   2335 use base 'Spoon::Formatter::Unit';
  4         8  
  4         23  
280 0     0   0 sub contains_phrases {
281 0         0 my $id = $self->formatter_id;
282 0         0 [ grep {$_ ne $id} @{$self->hub->formatter->all_phrases} ];
  0         0  
  0         0  
283             }
284              
285             ################################################################################
286             package Spoon::Formatter::Wafl;
287 4     4   2847 use Spoon::Base -base;
  4         9  
  4         56  
288             const contains_phrases => [];
289              
290 6     6   10 sub bless_wafl_class {
291 6         10 my $package = caller;
292 6         44 my $class = $self->hub->formatter->wafl_table->{$self->method};
293 6 50       40 if (ref $class) {
294 0         0 my $class_id;
295 0         0 ($class_id, $class) = @$class;
296 0         0 $self->hub->load_class($class_id);
297             }
298 6 50 33     24 bless $self, $class
299             if defined $class and $class->isa($package);
300 6         22 return 1;
301             }
302              
303             ################################################################################
304             package Spoon::Formatter::WaflBlock;
305 4     4   2613 use base 'Spoon::Formatter::Wafl';
  4         58  
  4         22  
306 4     4   2420 use base 'Spoon::Formatter::Block';
  4         8  
  4         17  
307             const formatter_id => 'wafl_block';
308             const html_end => "\n";
309             field 'method';
310             field 'arguments';
311              
312 2     2   4 sub html_start {
313 2         46 '
';
314             }
315              
316 6     6   8 sub match {
317             return unless
318 6 100       138 $self->text =~ /(?:^\.([\w\-]+)\ *\n)((?:.*\n)*?)(?:^\.\1\ *\n|\z)/m;
319 2         36 $self->set_match($2);
320 2         6 my $method = lc $1;
321 2         4 $method =~ s/-/_/g;
322 2         47 $self->method($method);
323 2         60 $self->matched($2);
324 2         23 $self->bless_wafl_class;
325             }
326              
327 0     0   0 sub block_text {
328 0         0 $self->units->[0];
329             }
330              
331             ################################################################################
332             package Spoon::Formatter::WaflPhrase;
333 4     4   5375 use base 'Spoon::Formatter::Wafl';
  4         10  
  4         22  
334 4     4   2348 use base 'Spoon::Formatter::Unit';
  4         8  
  4         19  
335             const formatter_id => 'wafl_phrase';
336             const pattern_start =>
337             qr/(^|(?<=[\s\-]))\{[\w-]+(\s*:)?\s*.*?\}(?=[^A-Za-z0-9]|\z)/;
338             field 'method';
339             field 'arguments';
340              
341 4     4   7 sub html_start {
342 4         93 '' . $self->arguments . '';
343             }
344              
345 8     8   12 sub match_phrase {
346 8 100       23 return unless super;
347 4 50       153 return unless $self->matched =~ /^\{([\w\-]+)(?:\s*\:)?\s*(.*)\}$/;
348 4         122 $self->arguments($2);
349 4         30 my $method = lc $1;
350 4         9 $method =~ s/-/_/g;
351 4         87 $self->method($method);
352 4         32 $self->bless_wafl_class;
353             }
354              
355 0     0     sub wafl_error {
356 0           join '',
357             '{',
358             $self->method,
359             ': ',
360             $self->arguments,
361             '}';
362             }
363              
364             __END__