File Coverage

blib/lib/Syntax/Highlight/XML.pm
Criterion Covered Total %
statement 126 153 82.3
branch 53 70 75.7
condition 38 54 70.3
subroutine 24 32 75.0
pod 2 2 100.0
total 243 311 78.1


line stmt bran cond sub pod time code
1 1     1   27 use 5.008;
  1         3  
  1         53  
2 1     1   5 use strict;
  1         2  
  1         38  
3 1     1   4 use warnings;
  1         2  
  1         237  
4              
5             {
6             package Syntax::Highlight::XML;
7              
8             our $AUTHORITY = 'cpan:TOBYINK';
9             our $VERSION = '0.003';
10            
11             our %STYLE = (
12             xml_pointy => '',
13             xml_slash => '',
14             xml_equals => '',
15             xml_tagname => '',
16             xml_attributename => 'color:#009999',
17             xml_attributevalue => 'color:#990099',
18             xml_tag_start => 'color:#0000CC',
19             xml_tag_is_pi => 'font-weight:bold',
20             xml_tag_is_doctype => 'font-weight:bold',
21             "xml_attribute_is_xmlns .xml_attributename" => 'color:#009900',
22             "xml_attribute_is_core .xml_attributename" => 'color:#009900',
23             );
24              
25 1         40 use MooX::Struct -retain, -rw,
26             Feature => [],
27             Token => [-extends => [qw<Feature>], qw($spelling!)],
28             Name => [-extends => [qw<Token>]],
29             Pointy => [-extends => [qw<Token>]],
30             Equals => [-extends => [qw<Token>]],
31             AttributeName => [-extends => [qw<Name>]],
32             AttributeValue => [-extends => [qw<Token>]],
33             Data => [-extends => [qw<Token>]],
34             Data_Whitespace => [-extends => [qw<Data>]],
35             TagName => [-extends => [qw<Name>]],
36             Slash => [-extends => [qw<Token>]],
37             Whitespace => [-extends => [qw<Token>]],
38             Structure_Start => [-extends => [qw<Feature>], qw($end)],
39             Structure_End => [-extends => [qw<Feature>], q($start) => [weak_ref => 1]],
40             Attribute_Start => [-extends => [qw<Structure_Start>], qw($name $value)],
41             Attribute_End => [-extends => [qw<Structure_End>]],
42             Tag_Start => [-extends => [qw<Structure_Start>], qw($name +is_opening +is_closing +is_pi +is_doctype)],
43             Tag_End => [-extends => [qw<Structure_End>]],
44 1     1   5 ;
  1         1  
45              
46             use Throwable::Factory
47 1         13 Tokenization => [qw( $remaining -caller )],
48             NotImplemented => [qw( -notimplemented )],
49             WTF => [],
50             WrongInvocant => [qw( -caller )],
51 1     1   1271 ;
  1         1  
52              
53             {
54 1     1   544 use HTML::HTML5::Entities qw/encode_entities/;
  1         2  
  1         65  
55            
56 1     1   6 no strict 'refs';
  1         2  
  1         696  
57 0     0   0 *{Feature . "::tok"} = sub { sprintf "%s~", $_[0]->TYPE };
58 0     0   0 *{Token . "::tok"} = sub { sprintf "%s[%s]", $_[0]->TYPE, $_[0]->spelling };
59 0     0   0 *{Whitespace . "::tok"} = sub { $_[0]->TYPE };
60 0     0   0 *{Data_Whitespace . "::tok"} = sub { $_[0]->TYPE };
61 0     0   0 *{Feature . "::TO_STRING"} = sub { "" };
62 0     0   0 *{Token . "::TO_STRING"} = sub { $_[0]->spelling };
63             *{Token . "::TO_HTML"} = sub {
64 68     68   3331 sprintf "<span class=\"xml_%s\">%s</span>", lc $_[0]->TYPE, encode_entities($_[0]->spelling)
65             };
66 8     8   551 *{Whitespace . "::TO_HTML"} = sub { $_[0]->spelling };
67 10     10   262 *{Data_Whitespace . "::TO_HTML"} = sub { $_[0]->spelling };
68             *{Structure_Start . "::TO_HTML"} = sub {
69 0     0   0 my @attrs = sprintf 'class="xml_%s"', lc $_[0]->TYPE;
70 0         0 sprintf "<span %s>", join " ", @attrs;
71             };
72             *{Structure_End . "::TO_HTML"} = sub {
73 17     17   920 "</span>"
74             };
75             *{Tag_Start . "::TO_HTML"} = sub {
76 14     14   767 my @classes = sprintf 'xml_%s', lc $_[0]->TYPE;
77 14 100       390 push @classes, "xml_tag_is_pi" if $_[0]->is_pi;
78 14 100       404 push @classes, "xml_tag_is_doctype" if $_[0]->is_doctype;
79 14 100       385 push @classes, "xml_tag_is_opening" if $_[0]->is_opening;
80 14 100       391 push @classes, "xml_tag_is_closing" if $_[0]->is_closing;
81 14 50 66     388 push @classes, "xml_tag_self_closing" if $_[0]->is_opening && $_[0]->is_closing;
82 14         629 my @attrs = sprintf 'data-xml-name="%s"', $_[0]->name->spelling;
83 14         1437 sprintf '<span class="%s" %s>', join(" ", @classes), join(" ", @attrs);
84             };
85             *{Attribute_Start . "::TO_HTML"} = sub {
86 3     3   34 my @classes = sprintf 'xml_%s', lc $_[0]->TYPE;
87 3 100       93 push @classes, "xml_attribute_is_xmlns" if $_[0]->name->spelling =~ /^xmlns\b/;
88 3 50       180 push @classes, "xml_attribute_is_core" if $_[0]->name->spelling =~ /^xml\b/;
89 3         172 my @attrs = sprintf 'data-xml-name="%s"', $_[0]->name->spelling;
90 3         127 sprintf '<span class="%s" %s>', join(" ", @classes), join(" ", @attrs);
91             };
92             }
93              
94 1     1   6 use Moo;
  1         3  
  1         7  
95              
96             has _tokens => (is => 'rw');
97             has _remaining => (is => 'rw');
98            
99 1     1   353 use IO::Detect qw( as_filehandle );
  1         1  
  1         9  
100 1     1   279 use Scalar::Util qw( blessed );
  1         2  
  1         1613  
101            
102             sub _peek
103             {
104 258     258   4171 my $self = shift;
105 258         346 my ($regexp) = @_;
106 258 100       899 $regexp = qr{^(\Q$regexp\E)} unless ref $regexp;
107            
108 258 100       284 if (my @m = (${$self->_remaining} =~ $regexp))
  258         1566  
109             {
110 72         357 return \@m;
111             }
112            
113 186         1211 return;
114             }
115              
116             sub _pull_token
117             {
118 86     86   8840 my $self = shift;
119 86         187 my ($spelling, $class, %more) = @_;
120 86 50       191 defined $spelling or WTF->throw("Tried to pull undef token!");
121 86         103 substr(${$self->_remaining}, 0, length $spelling, "");
  86         242  
122 86         114 push @{$self->_tokens}, $class->new(spelling => $spelling, %more);
  86         2220  
123 86         25068 return $self->_tokens->[-1];
124             }
125            
126             sub _pull_data
127             {
128 14     14   23 my $self = shift;
129 14 100       16 my $data = (${$self->_remaining} =~ /^(.*?)</ms) ? $1 : ${$self->_remaining};
  14         100  
  1         5  
130 14 100       73 $self->_pull_token($data, $data =~ /\S/ms ? Data : Data_Whitespace);
131             }
132            
133             sub _pull_attribute_value
134             {
135 6     6   10 my $self = shift;
136            
137 6 50       9 ${$self->_remaining} =~ /^(".*?")/m and return $self->_pull_token($1, AttributeValue);
  6         54  
138 0 0       0 ${$self->_remaining} =~ /^('.*?')/m and return $self->_pull_token($1, AttributeValue);
  0         0  
139            
140 0         0 Tokenization->throw(
141             "Called _pull_attribute_value when remaining string doesn't look like an attribute value",
142 0         0 remaining => ${$self->_remaining},
143             );
144             }
145            
146             sub _serializer
147             {
148 0     0   0 require RDF::Trine::Serializer::RDFXML;
149 0         0 return "RDF::Trine::Serializer::RDFXML";
150             }
151            
152             sub _scalarref
153             {
154 1     1   3 my $self = shift;
155 1         3 my ($thing) = @_;
156            
157 1 0 33     9 if (blessed $thing and $thing->isa("RDF::Trine::Model") and $self->can("_serializer"))
      33        
158             {
159 0         0 my $t = $self->_serializer->new->serialize_model_to_string($thing);
160 0         0 $thing = \$t
161             }
162            
163 1 0 33     9 if (blessed $thing and $thing->isa("RDF::Trine::Iterator") and $thing->can("as_xml"))
      33        
164             {
165 0         0 my $t = $thing->as_xml;
166 0         0 $thing = \$t
167             }
168            
169 1 0 33     8 if (blessed $thing and $thing->isa("RDF::Trine::Iterator") and $self->can("_serializer"))
      33        
170             {
171 0         0 my $t = $self->_serializer->new->serialize_iterator_to_string($thing);
172 0         0 $thing = \$t
173             }
174            
175 1 50 33     7 if (blessed $thing and $thing->isa("XML::LibXML::Node"))
176             {
177 0         0 my $t = $thing->toString;
178 0         0 $thing = \$t
179             }
180            
181 1 50       6 unless (ref $thing eq 'SCALAR')
182             {
183 0         0 my $fh = as_filehandle($thing);
184 0         0 local $/;
185 0         0 my $t = <$fh>;
186 0         0 $thing = \$t;
187             }
188            
189 1         6 return $thing;
190             }
191            
192             sub tokenize
193             {
194 1     1 1 4 my $self = shift;
195 1 50       4 ref $self or WrongInvocant->throw("this is an object method!");
196            
197 1         6 $self->_remaining( $self->_scalarref(@_) );
198 1         4 $self->_tokens([]);
199            
200 1         2 my ($TAG, $ATTR);
201            
202             # Declare this ahead of time for use in the big elsif!
203 0         0 my $matches;
204            
205 1         3 while (length ${ $self->_remaining })
  87         4129  
206             {
207 86 100 100     376 if ($matches = $self->_peek(qr#^(<[?!]?)#))
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
      66        
208             {
209 14         18 push @{$self->_tokens}, ($TAG = Tag_Start->new);
  14         53  
210 14         7886 $self->_pull_token($matches->[0], Pointy);
211 14 100       88 if ($matches->[0] =~ /\!/)
    100          
212             {
213 1         7 $TAG->is_doctype(1);
214             }
215             elsif ($matches->[0] =~ /\?/)
216             {
217 1         6 $TAG->is_pi(1);
218             }
219             }
220             elsif ($TAG and $matches = $self->_peek(qr#^(\??>)#))
221             {
222 14         41 my $end = Tag_End->new(start => $TAG);
223 14         6027 $TAG->end($end);
224 14         1382 $self->_pull_token($matches->[0], Pointy);
225 14         26 push @{$self->_tokens}, $end;
  14         37  
226 14         66 undef $TAG;
227             }
228             elsif ($TAG and $matches = $self->_peek(qr#^(\s+)#s))
229             {
230 8         28 $self->_pull_token($matches->[0], Whitespace);
231             }
232             elsif ($TAG and !$TAG->name and $matches = $self->_peek(qr#^((?:\w+[:])?\w+)#))
233             {
234 14         52 $TAG->name( $self->_pull_token($matches->[0], TagName) );
235 14 100       893 if (!$TAG->is_closing)
236             {
237 8         957 $TAG->is_opening(1);
238             }
239             }
240             elsif ($TAG and $TAG->name and $matches = $self->_peek(qr#^((?:\w+[:])?\w+)#))
241             {
242 6 100 100     145 if ($TAG->is_pi or $TAG->is_doctype)
243             {
244 3         89 $self->_pull_token($matches->[0], AttributeName);
245             }
246             else
247             {
248 3         108 push @{$self->_tokens}, ($ATTR = Attribute_Start->new);
  3         15  
249 3         1920 $ATTR->name( $self->_pull_token($matches->[0], AttributeName) );
250             }
251             }
252             elsif ($TAG and $self->_peek("="))
253             {
254 4         19 $self->_pull_token("=", Equals);
255             }
256             elsif ($TAG and $self->_peek("/"))
257             {
258 6         22 $self->_pull_token("/", Slash);
259 6 50       142 if (!$TAG->name)
260             {
261 6         166 $TAG->is_closing(1);
262             }
263             }
264             elsif ($TAG and $self->_peek(qr{^["']}))
265             {
266 6         20 $self->_pull_attribute_value;
267 6 100       43 if ($ATTR) # doctype?? pi??
268             {
269 3         24 my $end = Attribute_End->new(start => $ATTR);
270 3         3304 $ATTR->end($end);
271 3         136 push @{$self->_tokens}, $end;
  3         30  
272             }
273             }
274             else
275             {
276 14         39 $self->_pull_data;
277             }
278             }
279            
280 1         6 return $self->_tokens;
281             }
282            
283 1     1   2 sub _fixup { 1 };
284            
285             sub highlight
286             {
287 1     1 1 1152 my $self = shift;
288 1 50       7 ref $self or WrongInvocant->throw("this is an object method!");
289            
290 1         8 $self->tokenize(@_);
291 1         6 $self->_fixup;
292 1         3 return join "", map $_->TO_HTML, @{$self->_tokens};
  1         9  
293             }
294             }
295              
296             1;
297              
298             __END__
299              
300             =pod
301              
302             =encoding utf-8
303              
304             =head1 NAME
305              
306             Syntax::Highlight::XML - syntax highlighting for XML
307              
308             =head1 SYNOPSIS
309              
310             use Syntax::Highlight::XML;
311             my $syntax = "Syntax::Highlight::XML"->new;
312             print $syntax->highlight($filehandle);
313              
314             =head1 DESCRIPTION
315              
316             Outputs pretty syntax-highlighted HTML for XML. (Actually just
317             adds C<< <span> >> elements with C<< class >> attributes. You're expected to
318             bring your own CSS.)
319              
320             =head2 Methods
321              
322             =over
323              
324             =item C<< highlight($input) >>
325              
326             Highlight some XML.
327              
328             C<< $input >> may be a file handle, filename or a scalar ref of text.
329              
330             Returns a string of HTML.
331              
332             =item C<< tokenize($input) >>
333              
334             This is mostly intended for subclassing Syntax::Highlight::XML.
335              
336             C<< $input >> may be a file handle, filename or a scalar ref of text.
337              
338             Returns an arrayref of token objects. The exact API for the token objects
339             is subject to change, but currently they support C<< TYPE >> and
340             C<< spelling >> methods.
341              
342             =back
343              
344             =head1 BUGS
345              
346             Please report any bugs to
347             L<http://rt.cpan.org/Dist/Display.html?Queue=Syntax-Highlight-RDF>.
348              
349             =head1 SEE ALSO
350              
351             L<Syntax::Highlight::JSON2>,
352             L<Syntax::Highlight::RDF>.
353              
354             =head1 AUTHOR
355              
356             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
357              
358             =head1 COPYRIGHT AND LICENCE
359              
360             This software is copyright (c) 2013 by Toby Inkster.
361              
362             This is free software; you can redistribute it and/or modify it under
363             the same terms as the Perl 5 programming language system itself.
364              
365             =head1 DISCLAIMER OF WARRANTIES
366              
367             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
368             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
369             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
370