File Coverage

lib/XHTML/Util.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package XHTML::Util;
2 15     15   1309760 use strict;
  15         1241  
  15         540  
3 15     15   91 use warnings;
  15         29  
  15         516  
4 15     15   80 no warnings "uninitialized";
  15         32  
  15         574  
5 15     15   83 use Carp;
  15         30  
  15         1078  
6 15     15   29527 use XML::LibXML;
  0            
  0            
7             use HTML::Tagset 3.02 ();
8             use HTML::Entities qw( encode_entities decode_entities );
9             use HTML::Selector::XPath ();
10             use HTML::DTD;
11             use Path::Class;
12             use Encode;
13             use Scalar::Util qw( blessed );
14             use HTML::TokeParser::Simple;
15             use XML::Normalize::LibXML qw( xml_normalize );
16             use LWP::Simple ();
17             use overload q{""} => sub { +shift->as_string }, fallback => 1;
18              
19             our $VERSION = "0.99_08";
20             our $AUTHORITY = 'cpan:ASHLEY';
21             our $TITLE_ATTR = join("/", __PACKAGE__, $VERSION);
22              
23             our $FRAGMENT_SELECTOR = "div[title='$TITLE_ATTR']";
24             our $FRAGMENT_XPATH = HTML::Selector::XPath::selector_to_xpath($FRAGMENT_SELECTOR);
25              
26             my $isKnown = { %HTML::Tagset::isKnown }; # We modify this one.
27             my $emptyElement = \%HTML::Tagset::emptyElement;
28             my $isBodyElement = \%HTML::Tagset::isBodyElement;
29             my $isPhraseMarkup = \%HTML::Tagset::isPhraseMarkup;
30             #my $canTighten = \%HTML::Tagset::canTighten;
31             #my $isHeadElement = \%HTML::Tagset::isHeadElement;
32             #my $isHeadOrBodyElement = \%HTML::Tagset::isHeadOrBodyElement;
33             #my $isList = \%HTML::Tagset::isList;
34             #my $isTableElement = \%HTML::Tagset::isTableElement;
35             my $isFormElement = \%HTML::Tagset::isFormElement;
36             #my $p_closure_barriers = \@HTML::Tagset::p_closure_barriers;
37              
38             # Accommodate HTML::TokeParser's idea of a "tag."
39             $isKnown->{"$_/"} = 1 for keys %{$emptyElement};
40             my $isBlockLevel = { map {; $_ => 1 }
41             grep { ! ( $isPhraseMarkup->{$_} || $isFormElement->{$_} ) }
42             keys %{$isBodyElement}
43             };
44             # use YAML; die YAML::Dump($isBlockLevel);
45              
46             sub tags {
47             grep { ! /\W/ }
48             sort keys %HTML::Tagset::isKnown;
49             }
50              
51             sub new {
52             my $class = shift;
53             my $arg = shift or croak "new requires an argument";
54             my $self = bless {}, $class;
55              
56             if ( ref($arg) eq "SCALAR" )
57             {
58             $self->_parse( $$arg );
59             }
60             elsif ( blessed($arg) eq "Path::Class::File" )
61             {
62             $self->_parse( scalar $arg->slurp );
63             }
64             elsif ( blessed($arg) eq __PACKAGE__ )
65             {
66             $self->_parse( $arg->as_string ); # Cloning.
67             }
68             elsif ( blessed($arg) =~ /\AURI::https?/ )
69             {
70             $self->_parse( LWP::Simple::get($arg) );
71             }
72             elsif ( blessed($arg) and $arg->can("getlines") )
73             {
74             $self->_parse( join("", $arg->getlines) );
75             }
76             elsif ( ref($arg) eq 'GLOB' )
77             {
78             # $self->_parse( scalar Path::Class::File->new($arg)->slurp );
79             }
80             else
81             {
82             $self->_parse( scalar Path::Class::File->new($arg)->slurp );
83             }
84             $self;
85             }
86              
87             sub debug {
88             my $self = shift;
89             $self->{_debug} = shift if @_;
90             $self->{_debug} || 0;
91             }
92              
93             sub as_string {
94             my $self = shift;
95             my @args = @_ ? @_ : ( 1, "UTF-8" );
96             if ( $self->is_document )
97             {
98             return _trim( Encode::decode_utf8( $self->doc->serialize(@args) ) );
99             }
100             elsif ( $self->is_fragment )
101             {
102             croak "No root in document\n", $self->doc->serialize
103             unless $self->root;
104              
105             my ( $fragment ) = $self->root->findnodes($FRAGMENT_XPATH);
106              
107             croak "No fragment...?\n", $self->doc->serialize
108             unless $fragment;
109              
110             my $out = "";
111             $out .= $_->serialize(@args) for $fragment->childNodes;
112              
113             return Encode::decode_utf8( _trim($out) );
114             }
115             else
116             {
117             die "No type was found, internal issue :(";
118             }
119             }
120              
121             sub is_document {
122             +shift->{_type} eq "document";
123             }
124              
125             sub is_fragment {
126             +shift->{_type} eq "fragment";
127             }
128              
129             sub _parse {
130             my $self = shift;
131             $self->{_sanitized} =
132             $self->_sanitize( $self->{_original_string} = shift );
133              
134             if ( $self->{_original_string} =~ /\A(?:<\W[^>]+>|\s+)*
135             {
136             $self->{_type} = "document";
137             $self->{_doc} = $self->parser->parse_html_string($self->{_sanitized});
138             # Special case, doc contains ONLY 1 p and its first and last
139             # child of body then we should replace it with the FRAGMENT
140             # holder div.
141             }
142             else
143             {
144             # SHOULD we sanitize first?
145             $self->{_type} = "fragment";
146              
147             $self->{_doc} = $self->parser
148             ->parse_html_string(join("\n",
149             "",
150             sprintf('
',
151             $TITLE_ATTR
152             ),
153             $self->{_sanitized},
154             #Encode::encode_utf8($self->{_sanitized}),
155             '')
156             );
157             }
158              
159             $self->root->normalize;
160             $self->doc;
161             }
162              
163             sub root {
164             +shift->doc->getDocumentElement;
165             }
166              
167             sub doc {
168             +shift->{_doc};
169             }
170              
171             sub text {
172             +shift->doc->getDocumentElement->textContent;
173             }
174              
175             sub parser {
176             my $self = shift;
177             return $self->{_parser} if $self->{_parser};
178             $self->{_parser} = XML::LibXML->new;
179             $self->{_parser}->recover_silently(1);
180             $self->{_parser}->keep_blanks(1);
181             $self->{_parser};
182             }
183              
184             sub is_valid {
185             my $self = shift;
186             return 1 if $self->doc->is_valid;
187             # 321 debug about which DTD is being used.
188             my $dtd_name = shift || "xhtml1-transitional";
189             my $dtd_string = HTML::DTD->get_dtd("$dtd_name.dtd");
190             $self->{_dtd} = XML::LibXML::Dtd->parse_string($dtd_string);
191             return $self->doc->is_valid($self->{_dtd}) ? $self : undef;
192             }
193              
194             sub validate {
195             my $self = shift;
196             return 1 if $self->is_valid;
197             return $self->doc->validate($self->{_dtd});
198             }
199              
200             sub _original_string {
201             my $self = shift;
202             $self->{_original_string} ||= shift;
203             # $self->{_original_string} ||= Encode::encode_utf8( shift ); #321
204             $self->{_original_string};
205             }
206              
207             sub _return {
208             my $self = shift; # 321 ARGS for serialize.
209             xml_normalize( $self->doc );
210             my $callers_wantarray = [ caller(1) ]->[5];
211             return unless defined $callers_wantarray; # Void context.
212             return $self; # Should always return self?
213             }
214              
215             sub fix {
216             my $self = shift;
217             return $self->_return if $self->is_valid;
218              
219             for my $fixable ( qw( img ) )
220             {
221             my $method = "_fix_$fixable";
222             for my $node ( $self->root->findnodes("//$fixable") )
223             {
224             $self->$method($node);
225             }
226             }
227              
228             $self->is_valid()
229             or carp "Could not fix the problems with this document";
230             $self->validate();
231             $self->_return;
232             }
233              
234             sub _sanitize {
235             my $self = shift;
236             my $fragment = shift or return;
237             #$fragment = Encode::decode_utf8($fragment);
238             my $p = HTML::TokeParser::Simple->new(\$fragment);
239             my $renew = "";
240             my $in_body = 0;
241             TOKEN:
242             while ( my $token = $p->get_token )
243             {
244             #warn sprintf("%10s %10s %s\n", $token->[-1], $token->get_tag, blessed($token));
245             #no warnings "uninitialized";
246             if ( $isKnown->{$token->get_tag} )
247             {
248             if ( $token->is_start_tag )
249             {
250             my @pair;
251             for my $attr ( @{ $token->get_attrseq } )
252             {
253             next if $attr eq "/";
254             my $value = encode_entities(decode_entities($token->get_attr($attr)));
255             push @pair, join("=",
256             $attr,
257             qq{"$value"});
258             }
259             $renew .= "<" . join(" ", $token->get_tag, @pair);
260             $renew .= ( $token->get_attr("/") || $emptyElement->{$token->get_tag} ) ? "/>" : ">";
261             }
262             else
263             {
264             $renew .= $token->as_is;
265             }
266             }
267             elsif ( $token->is_declaration or $token->is_pi )
268             {
269             $renew .= $token->as_is;
270             }
271             else
272             {
273             $renew .= encode_entities(decode_entities($token->as_is),'<>"&');
274             }
275             }
276             return $renew;
277             }
278              
279             sub body {
280             [ shift->doc->findnodes("//body") ]->[0];
281             }
282              
283             sub head {
284             [ shift->doc->findnodes("//head") ]->[0];
285             }
286              
287             sub as_fragment {
288             my $self = shift;
289             my ( $fragment ) = $self->doc->findnodes($FRAGMENT_XPATH);
290             $fragment ||= $self->body;
291             my $out = "";
292             $out .= $_->serialize(1,"UTF-8") for $fragment->childNodes;
293             return $out;
294             }
295              
296             sub _make_selector {
297             my $self = shift;
298             my $selector = shift;
299             unless ( $selector )
300             {
301             my $base = $self->is_fragment ? $FRAGMENT_SELECTOR : "body";
302             $selector = "$base, $base *";
303             }
304             warn "Selector: $selector" if $self->debug > 2;
305             $selector =~ m,\A/, ?
306             $selector :
307             HTML::Selector::XPath::selector_to_xpath($selector);
308             }
309              
310             sub traverse {
311             my $self = shift;
312             my $xpath = $self->_make_selector(+shift) if @_ == 2;
313             my $code = shift;
314              
315             if ( $xpath )
316             {
317             for my $node ( $self->root->findnodes("$xpath") )
318             {
319             $code->($node);
320             }
321             }
322             else
323             {
324             $code->($self->root);
325             }
326             $self->_return;
327             }
328              
329              
330             sub enpara {
331             my $self = shift;
332             my $xpath = $self->_make_selector(+shift);
333             my $root = $self->root;
334             my $doc = $self->doc;
335              
336             NODE:
337             for my $designated_enpara ( $root->findnodes("$xpath") )
338             {
339             # warn "FOUND ", $designated_enpara->nodeName, $/;
340             # warn "*********", $designated_enpara->toString if $self->debug > 2;
341             next unless $designated_enpara->nodeType == 1;
342             next NODE if $designated_enpara->nodeName eq 'p';
343             if ( $designated_enpara->nodeName eq 'pre' ) # I don't think so, honky.
344             {
345             # Expand or leave it alone? or ->validate it...?
346             carp "It makes no sense to enpara within a
; skipping"; 
347             next NODE;
348             }
349             next unless $isBlockLevel->{$designated_enpara->nodeName};
350              
351             $self->_enpara_this_nodes_content($designated_enpara, $doc);
352             }
353             $self->_enpara_this_nodes_content($root, $doc);
354             $self->_return;
355             }
356              
357             sub _enpara_this_nodes_content {
358             my ( $self, $parent, $doc ) = @_;
359             my $lastChild = $parent->lastChild;
360             my @naked_block;
361             for my $node ( $parent->childNodes )
362             {
363             if ( $isBlockLevel->{$node->nodeName}
364             or
365             $node->nodeName eq "a" # special case block level, so IGNORE
366             and
367             grep { $_->nodeName eq "img" } $node->childNodes
368             )
369             {
370             next unless @naked_block; # nothing to enblock
371             my $p = $doc->createElement("p");
372             $p->setAttribute("enpara","enpara");
373             $p->setAttribute("line",__LINE__) if $self->debug > 4;
374             $p->appendChild($_) for @naked_block;
375             $parent->insertBefore( $p, $node )
376             if $p->textContent =~ /\S/;
377             @naked_block = ();
378             }
379             elsif ( $node->nodeType == 3
380             and
381             $node->nodeValue =~ /(?:[^\S\n]*\n){2,}/
382             )
383             {
384             my $text = $node->nodeValue;
385             my @text_part = map { $doc->createTextNode($_) }
386             split /([^\S\n]*\n){2,}/, $text;
387              
388             my @new_node;
389             for ( my $x = 0; $x < @text_part; $x++ )
390             {
391             if ( $text_part[$x]->nodeValue =~ /\S/ )
392             {
393             push @naked_block, $text_part[$x];
394             }
395             else # it's a blank newline node so _STOP_
396             {
397             next unless @naked_block;
398             my $p = $doc->createElement("p");
399             $p->setAttribute("enpara","enpara");
400             $p->setAttribute("line",__LINE__) if $self->debug > 4;
401             $p->appendChild($_) for @naked_block;
402             @naked_block = ();
403             push @new_node, $p;
404             }
405             }
406             if ( @new_node )
407             {
408             $parent->insertAfter($new_node[0], $node);
409             for ( my $x = 1; $x < @new_node; $x++ )
410             {
411             $parent->insertAfter($new_node[$x], $new_node[$x-1]);
412             }
413             }
414             $node->unbindNode;
415             }
416             elsif ( $node->nodeName !~ /\Ahead|body\z/ ) # Hack? Fix real reason? 321
417             {
418             push @naked_block, $node; # if $node->nodeValue =~ /\S/;
419             }
420              
421             if ( $node->isSameNode( $lastChild )
422             and @naked_block )
423             {
424             my $p = $doc->createElement("p");
425             $p->setAttribute("enpara","enpara");
426             $p->setAttribute("line",__LINE__) if $self->debug > 4;
427             $p->appendChild($_) for ( @naked_block );
428             $parent->appendChild($p) if $p->textContent =~ /\S/;
429             }
430             }
431              
432             my $newline = $doc->createTextNode("\n");
433             my $br = $doc->createElement("br");
434              
435             for my $p ( $parent->findnodes('//p[@enpara="enpara"]') )
436             {
437             $p->removeAttribute("enpara");
438             $parent->insertBefore( $newline->cloneNode, $p );
439             $parent->insertAfter( $newline->cloneNode, $p );
440              
441             my $frag = $doc->createDocumentFragment();
442              
443             my @kids = $p->childNodes();
444             for ( my $i = 0; $i < @kids; $i++ )
445             {
446             my $kid = $kids[$i];
447             next unless $kid->nodeName eq "#text";
448             my $text = $kid->nodeValue;
449             $text =~ s/\A\r?\n// if $i == 0;
450             $text =~ s/\r?\n\z// if $i == $#kids;
451              
452             my @lines = map { $doc->createTextNode($_) }
453             split /(\r?\n)/, $text;
454              
455             for ( my $i = 0; $i < @lines; $i++ )
456             {
457             $frag->appendChild($lines[$i]);
458             unless ( $i == $#lines
459             or
460             $lines[$i]->nodeValue =~ /\A\r?\n\z/ )
461             {
462             $frag->appendChild($br->cloneNode);
463             }
464             }
465             $kid->replaceNode($frag);
466             }
467             }
468             }
469              
470             sub _trim {
471             s/\A\s+|\s+\z//g for @_;
472             wantarray ? @_ : $_[0];
473             }
474              
475             sub _fix_img {
476             my ( $self, $img ) = @_;
477             unless ( $img->hasAttribute("src") )
478             {
479             croak "There is no way to fix an image without a source";
480             }
481             unless ( $img->hasAttribute("alt") )
482             {
483             $img->setAttribute("alt", $img->getAttribute("src"));
484             }
485             }
486              
487             sub _fix_center {
488             my ( $self, $center ) = @_;
489             #
-->
490             die "Unimplemented";
491             }
492              
493             sub _make_selector_xpath {
494             my $self = shift;
495             my $selector = shift;
496             my $base = $self->is_fragment ? $FRAGMENT_SELECTOR : "body";
497             my $xpath = HTML::Selector::XPath::selector_to_xpath("$base $selector");
498             warn "XPATH: $xpath\n" if $self->debug >= 5;
499             return $xpath;
500             }
501              
502             sub remove {
503             my $self = shift;
504             my $xpath = $self->_make_selector_xpath(@_);
505             for my $node ( $self->root->findnodes($xpath) )
506             {
507             $node->parentNode->removeChild($node);
508             }
509             $self->_return;
510             }
511              
512             sub strip_tags {
513             my $self = shift;
514             my $xpath = $self->_make_selector_xpath(@_);
515              
516             for my $node ( $self->root->findnodes($xpath) )
517             {
518             my $fragment = $self->doc->createDocumentFragment;
519             for my $n ( $node->childNodes )
520             {
521             $fragment->appendChild($n);
522             }
523             $node->replaceNode($fragment);
524             }
525             $self->_return;
526             }
527              
528             sub same_same {
529             my $self = shift;
530             my $other = shift;
531             my $self2 = blessed($other) eq __PACKAGE__ ?
532             $other : __PACKAGE__->new($other);
533              
534             $self->parser->keep_blanks(0);
535              
536             my $one = $self->parser->parse_string($self->root->serialize(0))->serialize(0);
537             my $two = $self->parser->parse_string($self2->root->serialize(0))->serialize(0);
538              
539             $self->parser->keep_blanks(1);
540              
541             $one eq $two or die "$one\n\n$two"
542             }
543              
544             sub clone {
545             my $self = shift;
546             my $class = blessed($self);
547             $class->new($self);
548             }
549              
550             1;
551              
552             __END__