File Coverage

blib/lib/HTML/Template/XPath.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package HTML::Template::XPath;
2              
3 7     7   5283 use strict;
  7         13  
  7         263  
4 7     7   13091 use XML::LibXML;
  0            
  0            
5             use HTML::Template;
6             use IO::File;
7             use IO::Handle;
8              
9             use Carp;
10              
11             use vars qw($VERSION);
12             $VERSION = '0.20';
13              
14             use constant XML_SOURCE_FILE => 1;
15             use constant XML_SOURCE_TEXT => 2;
16             use constant XML_SOURCE_LIBXML => 3;
17              
18             # these global vars are initialised and then they are readonly!
19             # this is done here mainly for speed.
20             use vars qw /$key_value_pattern/;
21              
22             # --------------------- $1 --------------------------
23             # $2 $3 $4 $5
24             $key_value_pattern = qr!(\s+(\w+)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|(\w+)))?)!; #"
25              
26              
27             # public methods
28              
29             sub new {
30             my ($class, @options) = @_;
31             my $self = { @options };
32             bless $self, $class;
33             $self->{'default_lang'} ||= 'en';
34             $self->{'relaxed_parser'} ||= 'no';
35             $self->{'template_class'} ||= 'HTML::Template';
36             return $self;
37             }
38              
39             sub file_mtimes {
40             return shift->{file_mtimes};
41             }
42              
43             sub process {
44             my ($xpt, %opt) = @_;
45              
46             # clear out data from preview call to process
47             delete $xpt->{file_mtimes};
48             delete $xpt->{lang};
49              
50             my $xpt_template_ref;
51              
52             if($opt{xpt_filename}){
53             local($/) = undef;
54             my $filename = "$xpt->{root_dir}/$opt{xpt_filename}";
55             my $xpt_handle = IO::File->new($filename) or die "can't open $filename for reading";
56             my $xpt_template = <$xpt_handle>;
57             $xpt_template_ref = \$xpt_template;
58             $xpt_handle->close;
59             } elsif ($opt{xpt_scalarref}){
60             $xpt_template_ref = $opt{xpt_scalarref};
61             }
62              
63             $opt{lang} ||= $xpt->{default_lang};
64              
65             if ($xpt->{relaxed_parser} eq 'yes') {
66              
67             # new experimental parser
68              
69             # see comments in PageKit::View::_preparse_model_tags
70              
71             # remove unneeded tags
72             $$xpt_template_ref =~ s^<(!--)?\s*/CONTENT_(?:VAR|ELSE)\s*(?(1)--)>^^sig;
73              
74             # translate all content end tags to tmpl tags
75             $$xpt_template_ref =~ s^<(!--)?\s*/CONTENT_(\w+)\s*(?(1)--)>^^sig;
76              
77             $$xpt_template_ref =~ s^<(!--)?\s*CONTENT_(\w+(?:$key_value_pattern)*)\s*/?(?(1)--)>^^sig;
78              
79             } else {
80              
81             # remove unneeded tags
82             $$xpt_template_ref =~ s^^^ig;
83              
84             # translate all content end tags to tmpl tags
85             $$xpt_template_ref =~ s^^^ig;
86              
87             $$xpt_template_ref =~ s^^^ig;
88             }
89             $opt{xml_filename} and $xpt->{_xml_source} = XML_SOURCE_FILE;
90             $opt{xml_text} and $xpt->{_xml_source} = XML_SOURCE_TEXT;
91             # not implemented yet..
92             # $opt{xml_parser} and $xpt->{_xml_source} = XML_SOURCE_LIBXML;
93             $opt{xml_filename} ||= $opt{xml_text};
94             die "No XML source - expected filename, text, or parser" unless $xpt->{_xml_source};
95             $xpt->_fill_in_content($xpt_template_ref, $opt{xml_filename}, $opt{lang}, $opt{check_for_other_lang});
96              
97             return $xpt->{lang}->{$opt{lang}};
98             }
99              
100             sub process_all_lang {
101             my ($xpt, %opt) = @_;
102             $opt{check_for_other_lang} = 1;
103             $xpt->process(%opt);
104              
105             return $xpt->{lang};
106             }
107              
108             # private methods
109              
110             sub _add_content_mtime {
111             my ($xpt, $xml_filename) = @_;
112             if ($xpt->{_xml_source} == XML_SOURCE_FILE) {
113             my $filename = "$xpt->{root_dir}/$xml_filename";
114             return if exists $xpt->{file_mtimes}->{$filename};
115             my $mtime = (stat($filename))[9];
116             $xpt->{file_mtimes}->{$filename} = $mtime;
117             } else {
118             # Hrm.. use some sort of hashing of the actual text here?
119             $xpt->{file_mtimes}->{_text} = time();
120             }
121             }
122              
123             sub _fill_in_content {
124             my ($xpt, $xpt_template_ref, $default_xml_filename, $lang, $check_for_other_lang) = @_;
125              
126             $xpt->{language_parsed}->{$lang} = 1;
127              
128             my $tmpl;
129             eval {
130             $tmpl = $xpt->{template_class}->new(scalarref => $xpt_template_ref,
131             # don't die when we set a parameter that is not in the template
132             die_on_bad_params=>0,
133             # built in __FIRST__, __LAST__, etc vars
134             loop_context_vars=>1,
135             case_sensitive=>1,
136             max_includes => 50);
137             };
138             if($@){
139             die "Can't load template (preprocessing): $@";
140             }
141              
142             my @params = $tmpl->query;
143             for my $name (@params){
144             # next unless $name =~ m!^pkit_content::!;
145             my $type = $tmpl->query(name => $name);
146             my ($xml_filename, $xpath) = $xpt->_get_document_xpath($name,$default_xml_filename);
147             $xpt->_add_content_mtime($xml_filename);
148             my $value;
149             if($type eq 'LOOP'){
150             $value = $xpt->_fill_in_content_loop($xpt_template_ref, $default_xml_filename, $tmpl, $xml_filename, $lang, [ $name ], $check_for_other_lang);
151             } else {
152             if($check_for_other_lang){
153             my $langs = $xpt->_get_xpath_langs(xml_filename => $xml_filename,
154             xpath => $xpath);
155             for my $l (@$langs){
156             $xpt->_fill_in_content($xpt_template_ref, $default_xml_filename, $l, 0)
157             unless exists $xpt->{language_parsed}->{$l};
158             }
159             }
160             my $nodeset = $xpt->_get_xpath_nodeset(xml_filename => $xml_filename,
161             xpath => $xpath,
162             lang => $lang);
163              
164             # get value of first node
165             $value = $nodeset->string_value;
166             }
167             $tmpl->param($name => $value);
168             }
169             # html, filtered for content
170             $xpt->{lang}->{$lang} = \$tmpl->output;
171             }
172              
173             sub _fill_in_content_loop {
174             my ($xpt, $xpt_template_ref, $default_xml_filename, $tmpl,
175             $context_xml_filename, $lang, $loops, $check_for_other_lang, $context) = @_;
176              
177             my ($xpath) = ($xpt->_get_document_xpath($loops->[-1],$default_xml_filename))[1];
178              
179             my @inner_param_names = $tmpl->query(loop => $loops);
180             my %inner_param;
181             for my $name (@inner_param_names){
182             next if $name =~ m!^__(inner|last|odd|first)__$!;
183             my ($xml_filename, $xpath) = $xpt->_get_document_xpath($name,$default_xml_filename);
184             $xpt->_add_content_mtime($xml_filename);
185             $inner_param{$name} = {type => $tmpl->query(name => [ @$loops, $name ]),
186             xml_filename => $xml_filename,
187             xpath => $xpath};
188             }
189              
190             my $nodeset = $xpt->_get_xpath_nodeset(xml_filename => $context_xml_filename,
191             xpath => $xpath,
192             lang => $lang,
193             context => $context);
194              
195             my $array_ref = [];
196              
197             for my $node ($nodeset->get_nodelist){
198             my $loop_param = {};
199             while (my ($name, $hash_ref) = each %inner_param){
200             my $value;
201             my $context = $node;
202             if($hash_ref->{type} eq 'LOOP'){
203             $value = $xpt->_fill_in_content_loop($xpt_template_ref, $default_xml_filename, $tmpl, $hash_ref->{xml_filename}, $lang, [ @$loops, $name], $check_for_other_lang, $node);
204             } else {
205             if($check_for_other_lang){
206             my $langs = $xpt->_get_xpath_langs(xml_filename => $hash_ref->{xml_filename},
207             xpath => $hash_ref->{xpath},
208             context => $context);
209             for my $l (@$langs){
210             $xpt->_fill_in_content($xpt_template_ref, $default_xml_filename, $l, 0)
211             unless exists $xpt->{language_parsed}->{$l};
212             }
213             }
214             my $nodeset = $xpt->_get_xpath_nodeset(xml_filename => $hash_ref->{xml_filename},
215             xpath => $hash_ref->{xpath},
216             lang => $lang,
217             context => $context);
218             # get value of first node
219             $value = $nodeset->string_value;
220             }
221             $loop_param->{"$name"} = $value;
222             }
223             push @$array_ref, $loop_param;
224             }
225             return $array_ref;
226             }
227              
228             sub _get_document_xpath {
229             my ($xpt, $name, $default_xml_filename) = @_;
230             my ($xml_filename, $xpath);
231             if($name =~ m!^document\('?(.*?)'?\)(.*)$!){
232             ($xml_filename, $xpath) = ($1, $2);
233             unless($xml_filename =~ s!^/!!){
234             # return relative to $default_xml_filename
235             (my $default_xml_dir = $default_xml_filename) =~ s![^/]*$!!;
236             $xml_filename = "$default_xml_dir$xml_filename";
237             while ($xml_filename =~ s![^/]*/\.\./!!) {};
238             }
239             } else {
240             ($xml_filename, $xpath) = ($default_xml_filename, $name);
241             }
242             return ($xml_filename, $xpath);
243             }
244              
245             sub _get_xp {
246             my ($xpt, $xml_filename, $context) = @_;
247              
248             if ( $context ) {
249             return $context;
250             } elsif(exists $xpt->{xp}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)}){
251             return $xpt->{xp}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)};
252             }
253              
254             my $xp;
255             if ($xpt->{_xml_source} == XML_SOURCE_FILE) {
256              
257             my $filename = "$xpt->{root_dir}/$xml_filename";
258             unless( -f $filename ) {
259             warn "Can't load content file $filename";
260             return;
261             }
262              
263             my $parser = XML::LibXML->new;
264             my $xpt_handle = IO::File->new("<$filename") or die "can not open $filename";
265             $xp = $parser->parse_fh($xpt_handle);
266             $xpt_handle->close;
267             # get default context (root XML element)
268             $xpt->{root_element_node}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)} = $xp->documentElement;
269              
270             $xpt->{xp}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)} = $xp;
271             } elsif ($xpt->{_xml_source} == XML_SOURCE_TEXT) {
272             my $parser = XML::LibXML->new;
273             $xp = $parser->parse_string($xml_filename);
274             $xpt->{root_element_node}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)}
275             = $xp->documentElement;
276             $xpt->{xp}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)} = $xp;
277             }
278             return $xp;
279             }
280              
281             sub _get_xpath_langs {
282             my ($xpt, %arg) = @_;
283              
284             my $xml_filename = $arg{xml_filename};
285             my $context = $arg{context} || $xpt->{root_element_node}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)};
286             my $xp = $xpt->_get_xp($xml_filename, $context);
287             return [] unless $xp;
288              
289             my $xpath = $arg{xpath};
290             $context ||= $xpt->{root_element_node}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)};
291              
292             my $nodeset = $context->findnodes($xpath);
293              
294             my %lang;
295              
296             my $return_nodeset = XML::LibXML::NodeList->new;
297              
298             for my $node ($nodeset->get_nodelist) {
299             my $nodeset = $node->findnodes(q{ancestor-or-self::*[@xml:lang]});
300             for my $node ($nodeset->get_nodelist) {
301             my $lang = $node->getAttributeNS('http://www.w3.org/XML/1998/namespace','lang');
302             $lang{$lang} = 1;
303             }
304             $return_nodeset->push($node) if $nodeset->size > 0;
305             }
306             my @lang = keys %lang;
307             return \@lang;
308             }
309              
310             sub _get_xpath_nodeset {
311             my ($xpt, %arg) = @_;
312              
313             my $xml_filename = $arg{xml_filename};
314              
315             my $return_nodeset = XML::LibXML::NodeList->new;
316             my $context = $arg{context};
317             my $xp = $xpt->_get_xp($xml_filename, $context);
318             return $return_nodeset unless $xp;
319             my $xpath = $arg{xpath};
320             my $lang = $arg{lang};
321             $context ||= $xpt->{root_element_node}->{_hash_or_file($xpt->{_xml_source}, $xml_filename)};
322              
323             my $nodeset = $context->find($xpath);
324             my @nodelist = $nodeset->get_nodelist;
325              
326             # first attempt get nodes whose ancestor-or-self[@xml:lang] eq $lang
327             for my $node (@nodelist) {
328             # lifted from XPath::Function::lang
329             my $node_lang = $node->findvalue('(ancestor-or-self::*[@xml:lang]/@xml:lang)[last()]') || $xpt->{default_lang};
330             if (substr(lc($node_lang), 0, length($lang)) eq $lang) {
331             $return_nodeset->push($node);
332             }
333             }
334             return $return_nodeset if $return_nodeset->size > 0;
335              
336             # If no nodes are found in the preferred language, then return
337             # node(s) which are in the default language
338             for my $node (@nodelist) {
339             my $node_lang = $node->findvalue('(ancestor-or-self::*[@xml:lang]/@xml:lang)[last()]') || $xpt->{default_lang};
340             if (substr(lc($node_lang), 0, length($xpt->{default_lang})) eq $xpt->{default_lang}) {
341             $return_nodeset->push($node);
342             }
343             }
344             return $return_nodeset if $return_nodeset->size > 0;
345              
346             # pass 3, just return all the nodes
347             # (even thought it's not in the right language)
348             # this is undocumented and subject to change!
349             return $nodeset;
350             }
351              
352              
353              
354             #============================================================
355             # Returns the filename or a simple hash of the text.
356             #============================================================
357             sub _hash_or_file
358             {
359             my ($type, $data) = @_;
360             return $data if $type == XML_SOURCE_FILE;
361             use Digest::MD5 qw(md5_base64);
362             return md5_base64($data);
363             }
364              
365              
366              
367             1;
368              
369             __END__