File Coverage

blib/lib/Template/Semantic/Document.pm
Criterion Covered Total %
statement 160 164 97.5
branch 81 88 92.0
condition 19 21 90.4
subroutine 18 18 100.0
pod 3 4 75.0
total 281 295 95.2


line stmt bran cond sub pod time code
1             package Template::Semantic::Document;
2 12     12   76 use strict;
  12         26  
  12         320  
3 12     12   57 use warnings;
  12         20  
  12         253  
4 12     12   50 use Carp;
  12         20  
  12         544  
5 12     12   5513 use HTML::Selector::XPath;
  12         27364  
  12         540  
6 12     12   83 use Scalar::Util qw/blessed/;
  12         23  
  12         497  
7 12     12   64 use XML::LibXML ':libxml';
  12         22  
  12         71  
8              
9 12     12   1950 use overload q{""} => sub { shift->as_string }, fallback => 1;
  12     10   23  
  12         113  
  10         2577  
10              
11             sub new {
12 168     168 0 258 my $class = shift;
13 168         476 my $self = bless { @_ }, $class;
14              
15 168         478 my $source = $self->{source};
16             # quick hack for xhtml default ns
17 168         298 $source =~ s{(]+?)xmlns="http://www\.w3\.org/1999/xhtml"}{
18 2         5 $self->{xmlns_hacked} = 1;
19 2         9 $1 . 'xmlns=""';
20             }e;
21              
22 168 100       479 if ($self->{engine}{parser}->get_option('recover')) {
23 166         1254 $source =~ s/&(?!\w+;|#(?:x[a-fA-F0-9]+|\d+);)/&/g;
24             }
25              
26 168         450 $self->{dom} = $self->{engine}{parser}->parse_string($source);
27 166         22696 $self;
28             }
29              
30 1     1 1 537 sub dom { $_[0]->{dom} }
31              
32             sub process {
33 169     169 1 277 my ($self, $vars) = @_;
34 169         413 $self->_query($self->{dom}, $vars);
35 147         2408 $self;
36             }
37              
38             sub as_string {
39 138     138 1 2659 my ($self, %opt) = @_;
40              
41 138 100       368 $opt{is_xhtml} = 1 unless defined $opt{is_xhtml};
42              
43 138 100       354 if ($self->{source} =~ /^<\?xml/) {
44 3         13 return $self->{dom}->serialize;
45             }
46             else { # for skip
47 135         189 my $r = "";
48              
49 135 100       577 if (my $dtd = $self->{dom}->internalSubset) {
    100          
50 1         19 $r = $dtd->serialize . "\n";
51             } elsif($opt{is_xhtml}) {
52             $self->{dom}->createInternalSubset(
53 133         745 'html',
54             '-//W3C//DTD XHTML 1.0 Transitional//EN',
55             'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd',
56             );
57 133         406 $self->{dom_hacked}++;
58             }
59              
60 135         222 local $XML::LibXML::skipXMLDeclaration = 1;
61 135         173 local $XML::LibXML::skipDTD = 1;
62 135         355 $r .= $self->{dom}->serialize;
63 135         7423 $r =~ s/\n*$/\n/;
64              
65 135 100       351 if ($self->{xmlns_hacked}) {
66 2         24 $r =~ s{(]+?)xmlns=""}{$1xmlns="http://www.w3.org/1999/xhtml"};
67             }
68              
69 135 100       254 if ($self->{dom_hacked}) {
70 133         493 $self->{dom}->removeInternalSubset;
71 133         340 $self->{dom_hacked} = 0;
72             }
73              
74 135         669 return $r;
75             }
76             }
77              
78             my $element_with_attr_regex = qr{
79             ^
80             \s*
81             (
82             \@[^@]+? |
83             (?:
84             (?: [^"]+? | .+?"[^"]+".+? )
85             (?: \@[^@]+? )?
86             )
87             )
88             \s*
89             (?: , | $ )
90             }x;
91              
92             sub _exp_to_xpath {
93 245     245   62888 my ($self, $exp) = @_;
94 245 50       434 return unless $exp;
95              
96 245         309 my $xpath;
97 245 100       998 if ($exp =~ m{^(?:/|\.(?:/|$))}) {
    100          
98 134         218 $xpath = $exp;
99             } elsif ($exp =~ m{^id\(}) {
100 8         18 $xpath = $exp;
101 8         59 $xpath =~ s{^id\((.+?)\)}{//\*\[\@id=$1\]}g; # id() hack
102             } else {
103             # css selector extends @attr syntax
104 103         151 my @x;
105 103         889 while ($exp =~ s/$element_with_attr_regex//) {
106 114         302 my $e = $1;
107 114         582 my ($elem, $attr) = $e =~ m{(.*?)/?(@[^/@]+)?$};
108 114         178 my $x;
109 114 100       199 if ($elem) {
    50          
110 103         256 my $x = HTML::Selector::XPath::selector_to_xpath($elem);
111 103 100       8548 $x .= "/$attr" if $attr;
112 103         414 push @x, $x;
113             } elsif ($attr) {
114 11         50 push @x, "//$attr";
115             }
116             }
117 103         272 $xpath = join " | ", @x;
118             }
119 245         557 $xpath;
120             }
121              
122             sub _query {
123 206     206   335 my ($self, $context, $vars) = @_;
124 206 50       726 croak "\$vars must be hashref." if ref($vars) ne 'HASH';
125              
126 206         642 for my $exp (keys %$vars) {
127 211 50       503 my $xpath = $self->_exp_to_xpath($exp) or next;
128 211         610 my $nodes = $context->findnodes($xpath);
129 211         9492 $self->_assign_value($nodes, $vars->{$exp});
130             }
131             }
132              
133             sub _assign_value {
134 312     312   525 my ($self, $nodes, $value) = @_;
135 312         501 my $value_type = ref $value;
136              
137 312 100 100     2459 if (not defined $value) { # => delete
    100 66        
    100 100        
    100 100        
    100          
    50          
    100          
    100          
    100          
138 8         21 for my $node (@$nodes) {
139 8         88 $node->unbindNode;
140             }
141             }
142              
143             elsif ($value_type eq 'HASH') { # => sub query
144 13         32 for my $node (@$nodes) {
145 15 100       91 if (not $node->isa('XML::LibXML::Element')) {
146 5         392 croak "Can't assign hashref to " . ref($node);
147             }
148              
149 10         94 my $parted = $self->_to_node($node->serialize);
150 10         1133 $self->_query($parted, $value);
151              
152 10 100       157 if ($node->isSameNode( $self->{dom}->documentElement )) { # to replace root
153 1         5 $self->{dom}->setDocumentElement($parted);
154             } else {
155 9         69 $node->replaceNode($parted);
156             }
157             }
158             }
159              
160             elsif ($value_type eq 'ARRAY' and ref($value->[0]) eq 'HASH') { # => sub query loop
161 15         50 for my $node (@$nodes) {
162 16 100       84 if (not $node->isa('XML::LibXML::Element')) {
163 5         383 croak "Can't assign loop list to " . ref($node);
164             }
165              
166 11         44 my $container = XML::LibXML::DocumentFragment->new;
167 11         101 my $tmpl_xml = $node->serialize;
168 11         21 my $joint;
169 11         20 for my $v (@$value) {
170 27 50       402 next if ref($v) ne 'HASH';
171              
172 27         50 my $tmpl = $self->_to_node($tmpl_xml);
173 27         3023 $self->_query($tmpl, $v);
174 27 100       310 $container->addChild($joint->cloneNode) if $joint;
175 27         197 $container->addChild($tmpl);
176              
177 27 100       51 if (not defined $joint) { # 2nd item
178 11         117 my $p = $node->previousSibling;
179 11 100 66     88 $joint = ($p and $p->serialize =~ /^(\W+)$/s) ? $p : "";
180             }
181             }
182 11         241 $node->replaceNode($container);
183             }
184             }
185              
186             elsif ($value_type eq 'ARRAY') { # => value, filter, filter, ...
187 11         32 my ($value, @filters) = @$value;
188 11         24 for my $filter (@filters) {
189 18 100       42 if (not ref $filter) {
190 12         53 $filter = $self->{engine}->call_filter($filter);
191             }
192             }
193              
194 11         46 for my $node (@$nodes) {
195 12         44 $self->_assign_value([$node], $value);
196              
197 12         27 for my $filter (@filters) {
198 19         40 $self->_assign_value([$node], $filter);
199             }
200             }
201             }
202             elsif ($value_type eq 'CODE') { # => callback
203 55         103 for my $node (@$nodes) {
204 71         178 local $_ = $self->_serialize_inner($node);
205 71         265 my $ret = eval { $value->($node) };
  71         859  
206 71 100       387 if ($@) {
207 1         75 croak "Callback error: $@";
208             } else {
209 70         181 $self->_assign_value([$node], $ret);
210             }
211             }
212             }
213             elsif (blessed($value) and $value->can('filter')) { # => Text::Pipe like filter
214 0         0 for my $node (@$nodes) {
215 0         0 my $ret = $value->filter( $self->_serialize_inner($node) );
216 0         0 $self->_assign_value([$node], $ret);
217             }
218             }
219              
220             elsif (blessed($value) and $value->isa('Template::Semantic::Document')) { # => insert result
221 9         25 for my $node (@$nodes) {
222 10 100       62 if (not $node->isa('XML::LibXML::Element')) {
223 5         534 croak "Can't assign Template::Semantic::Document to " . ref($node);
224             }
225 5         15 $self->_replace_node($node, $value->{dom}->childNodes);
226             }
227             }
228             elsif (blessed($value) and $value->isa('XML::LibXML::Node')) { # => as LibXML object
229 31 100       108 if ($value->isa('XML::LibXML::Attr')) {
230 6         827 croak "Can't assign XML::LibXML::Attr to any element";
231             }
232 25         49 for my $node (@$nodes) {
233 27         74 $self->_replace_node($node, $value);
234             }
235             }
236             elsif ($value_type eq 'SCALAR') { # => as HTML/XML
237 9         17 my $root = $self->_to_node("${$value}");
  9         38  
238 9         1064 for my $node (@$nodes) {
239 9         26 $self->_replace_node($node, $root->childNodes);
240             }
241             }
242             else { # => text or unknown(stringify)
243 161         835 my $value = XML::LibXML::Text->new("$value");
244 161         543 for my $node (@$nodes) {
245 168         344 $self->_replace_node($node, $value);
246             }
247             }
248             }
249              
250             sub _to_node {
251 46     46   86 my ($self, $xmlpart) = @_;
252 46         132 $self->{engine}{parser}->parse_string($xmlpart)->documentElement;
253             }
254              
255             sub _replace_node {
256 209     209   724 my ($self, $node, @replace) = @_;
257              
258 209 100 100     898 if ($node->isa('XML::LibXML::Element')) {
    100          
    100          
    100          
259 134         493 $node->removeChildNodes;
260 134         218 for (@replace) {
261 143 50       1613 $node->addChild($_->cloneNode(1)), next unless $_->nodeType == XML_DOCUMENT_FRAG_NODE;
262 0         0 $node->appendChild($_->cloneNode(1));
263             }
264             }
265             elsif ($node->isa('XML::LibXML::Attr')) {
266 31         64 $node->setValue(join "", map { $_->textContent } @replace);
  31         349  
267             }
268             elsif ($node->isa('XML::LibXML::Comment')
269             or $node->isa('XML::LibXML::CDATASection')) {
270 22 100       43 $node->setData(join "", map { $_->nodeValue || $_->serialize } @replace);
  22         307  
271             }
272             elsif ($node->isa('XML::LibXML::Text')) {
273 11         21 $node->setData(join "", map { $_->textContent } @replace);
  11         121  
274             }
275             }
276              
277             sub _serialize_inner {
278 71     71   122 my ($self, $node) = @_;
279 71         102 my $inner = "";
280 71 100 100     508 if ($node->isa('XML::LibXML::Attr')) {
    100          
    100          
281 11         49 $inner = $node->value;
282             }
283             elsif ($node->isa('XML::LibXML::Comment')
284             or $node->isa('XML::LibXML::CDATASection')) {
285 10         45 $inner = $node->data;
286             }
287             elsif ($node->isa('XML::LibXML::Text')) {
288 5         44 $inner = $node->serialize;
289             }
290             else {
291 45         153 $inner .= $_->serialize for $node->childNodes;
292             }
293 71         888 $inner;
294             }
295              
296             1;
297             __END__