source: main/trunk/greenstone2/perllib/plugins/LOMPlugin.pm@ 24951

Last change on this file since 24951 was 24951, checked in by ak19, 12 years ago

All perlcode that accesses extrametakeys, extrametadata, extrametafile data structures has been moved into a new perl module called extrametautil.pm. The next step will be to ensure that the file_regexes used to index into these data structures are consistent (using consistent slashes, like URL style slashes).

  • Property svn:keywords set to Author Date Id Revision
File size: 19.3 KB
Line 
1###########################################################################
2#
3# LOMPlugin.pm -- plugin for import the collection from LOM
4#
5# A component of the Greenstone digital library software
6# from the New Zealand Digital Library Project at the
7# University of Waikato, New Zealand.
8#
9# Copyright (C) 2005 New Zealand Digital Library Project
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24#
25###########################################################################
26
27### Note this plugin currently can't download source documents from outside if you are behind a firewall.
28# Unless, you set the http_proxy environment variable to be your proxy server,
29# and set proxy_user and proxy_password in .wgetrc file in home directory.
30# (does that work on windows??)
31
32package LOMPlugin;
33
34use extrametautil;
35use ReadTextFile;
36use MetadataPass;
37use MetadataRead;
38use XMLParser;
39use Cwd;
40
41# methods with identical signatures take precedence in the order given in the ISA list.
42sub BEGIN {
43 @ISA = ('MetadataRead', 'ReadTextFile', 'MetadataPass');
44}
45
46use strict; # every perl program should have this!
47no strict 'refs'; # make an exception so we can use variables as filehandles
48
49
50my $arguments =
51 [ { 'name' => "process_exp",
52 'desc' => "{BasePlugin.process_exp}",
53 'type' => "string",
54 'deft' => &get_default_process_exp(),
55 'reqd' => "no" },
56 { 'name' => "root_tag",
57 'desc' => "{LOMPlugin.root_tag}",
58 'type' => "regexp",
59 'deft' => q/^(?i)lom$/,
60 'reqd' => "no" },
61 { 'name' => "check_timestamp",
62 'desc' => "{LOMPlugin.check_timestamp}",
63 'type' => "flag" },
64 { 'name' => "download_srcdocs",
65 'desc' => "{LOMPlugin.download_srcdocs}",
66 'type' => "regexp",
67 'deft' => "",
68 'reqd' => "no" }];
69
70my $options = { 'name' => "LOMPlugin",
71 'desc' => "{LOMPlugin.desc}",
72 'abstract' => "no",
73 'inherits' => "yes",
74 'args' => $arguments };
75
76
77
78my ($self);
79sub new {
80 my $class = shift (@_);
81 my ($pluginlist,$inputargs,$hashArgOptLists) = @_;
82 push(@$pluginlist, $class);
83
84 push(@{$hashArgOptLists->{"ArgList"}},@{$arguments});
85 push(@{$hashArgOptLists->{"OptList"}},$options);
86
87 $self = new ReadTextFile($pluginlist, $inputargs, $hashArgOptLists);
88
89 if ($self->{'info_only'}) {
90 # don't worry about creating the XML parser as all we want is the
91 # list of plugin options
92 return bless $self, $class;
93 }
94
95 #create XML::Parser object for parsing dublin_core.xml files
96 my $parser = new XML::Parser('Style' => 'Stream',
97 'Handlers' => {'Char' => \&Char,
98 'Doctype' => \&Doctype
99 });
100 $self->{'parser'} = $parser;
101
102 $self->{'extra_blocks'} = {};
103
104 return bless $self, $class;
105}
106
107sub get_default_process_exp {
108 my $self = shift (@_);
109
110 return q^(?i)\.xml$^;
111}
112
113
114sub can_process_this_file {
115 my $self = shift(@_);
116 my ($filename) = @_;
117
118 if ($self->SUPER::can_process_this_file($filename) && $self->check_doctype($filename)) {
119 return 1; # its a file for us
120 }
121 return 0;
122}
123
124sub metadata_read {
125 my $self = shift (@_);
126 my ($pluginfo, $base_dir, $file, $block_hash,
127 $extrametakeys, $extrametadata, $extrametafile,
128 $processor, $gli, $aux) = @_;
129
130 my $outhandle = $self->{'outhandle'};
131
132 # can we process this file??
133 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file);
134 return undef unless $self->can_process_this_file_for_metadata($filename_full_path);
135
136 $file =~ s/^[\/\\]+//; # $file often begins with / so we'll tidy it up
137
138 print $outhandle "LOMPlugin: extracting metadata from $file\n"
139 if $self->{'verbosity'} > 1;
140
141 my ($dir,$tail) = $filename_full_path =~ /^(.*?)([^\/\\]*)$/;
142 $self->{'output_dir'} = $dir;
143
144 eval {
145 $self->{'parser'}->parsefile($filename_full_path);
146 };
147
148 if ($@) {
149 print $outhandle "LOMPlugin: skipping $filename_full_path as not conformant to LOM syntax\n" if ($self->{'verbosity'} > 1);
150 print $outhandle "\n Perl Error:\n $@\n" if ($self->{'verbosity'}>2);
151 return 0;
152 }
153
154 $self->{'output_dir'} = undef;
155
156 my $file_re;
157 my $lom_srcdoc = $self->{'lom_srcdoc'};
158
159 if (defined $lom_srcdoc) {
160 my $dirsep = &util::get_re_dirsep();
161 $lom_srcdoc =~ s/^$base_dir($dirsep)//;
162 $self->{'extra_blocks'}->{$file}++;
163 $file_re = $lom_srcdoc;
164 }
165 else {
166 $file_re = $tail;
167 }
168 $file_re = &util::filename_to_regex($file_re);
169 $self->{'lom_srcdoc'} = undef; # reset for next file to be processed
170
171 &extrametautil::addmetakey($extrametakeys, $file_re);
172 &extrametautil::setmetadata($extrametadata, $file_re, $self->{'saved_metadata'});
173 if (defined $lom_srcdoc) {
174 # copied from oaiplugin
175 if (!defined &extrametautil::getmetafile($extrametafile, $file_re)) {
176 &extrametautil::setmetafile($extrametafile, $file_re, {});
177 }
178 #maps the file to full path
179 &extrametautil::setmetafile_for_named_file($extrametafile, $file_re, $file, $filename_full_path);
180 }
181
182 return 1;
183}
184
185sub check_doctype {
186 $self = shift (@_);
187
188 my ($filename) = @_;
189
190 if (open(XMLIN,"<$filename")) {
191 my $doctype = $self->{'root_tag'};
192 ## check whether the doctype has the same name as the root element tag
193 while (defined (my $line = <XMLIN>)) {
194 ## find the root element
195 if ($line =~ /<([\w\d:]+)[\s>]/){
196 my $root = $1;
197 if ($root !~ $doctype){
198 close(XMLIN);
199 return 0;
200 }
201 else {
202 close(XMLIN);
203 return 1;
204 }
205 }
206 }
207 close(XMLIN);
208 }
209
210 return undef; # haven't found a valid line
211
212}
213
214sub read_file {
215 my $self = shift (@_);
216 my ($filename, $encoding, $language, $textref) = @_;
217
218 my $metadata_table = $self->{'metadata_table'};
219
220 my $rawtext = $metadata_table->{'rawtext'};
221
222 delete $metadata_table->{'rawtext'};
223
224 $$textref = $rawtext;
225}
226
227sub read {
228 my $self = shift (@_);
229 my ($pluginfo, $base_dir, $file, $block_hash, $metadata, $processor, $maxdocs, $total_count, $gli) = @_;
230
231 my $outhandle = $self->{'outhandle'};
232
233 return 0 if (defined $self->{'extra_blocks'}->{$file});
234
235 # can we process this file??
236 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file);
237 return undef unless $self->can_process_this_file($filename_full_path);
238
239 $self->{'metadata_table'} = $metadata;
240
241 my $lom_language = $metadata->{'lom_language'};
242
243 my $store_input_encoding;
244 my $store_extract_language;
245 my $store_default_language;
246 my $store_default_encoding;
247
248 if (defined $lom_language) {
249 delete $metadata->{'lom_language'};
250
251 $store_input_encoding = $self->{'input_encoding'};
252 $store_extract_language = $self->{'extract_language'};
253 $store_default_language = $self->{'default_language'};
254 $store_default_encoding = $self->{'default_encoding'};
255
256 $self->{'input_encoding'} = "utf8";
257 $self->{'extract_language'} = 0;
258 $self->{'default_language'} = $lom_language;
259 $self->{'default_encoding'} = "utf8";
260 }
261
262 my $rv = $self->SUPER::read(@_);
263
264 if (defined $lom_language) {
265 $self->{'input_encoding'} = $store_input_encoding;
266 $self->{'extract_language'} = $store_extract_language;
267 $self->{'default_language'} = $store_default_language;
268 $self->{'default_encoding'} = $store_default_encoding;
269 }
270
271 $self->{'metadata_table'} = undef;
272
273 return $rv;
274}
275
276# do plugin specific processing of doc_obj
277sub process {
278 my $self = shift (@_);
279 my ($textref, $pluginfo, $base_dir, $file, $metadata, $doc_obj, $gli) = @_;
280 my $outhandle = $self->{'outhandle'};
281
282 my $cursection = $doc_obj->get_top_section();
283 $doc_obj->add_utf8_text($cursection, $$textref);
284
285 return 1;
286}
287
288sub Doctype {
289 my ($expat, $name, $sysid, $pubid, $internal) = @_;
290
291 my $root_tag = $self->{'root_tag'};
292
293 if ($name !~ /$root_tag/) {
294 die "Root tag $name does not match regular expression $root_tag";
295 }
296}
297
298sub StartTag {
299 my ($expat, $element) = @_;
300
301 my %attr = %_;
302
303 my $raw_tag = "&lt;$element";
304 map { $raw_tag .= " $_=\"$attr{$_}\""; } keys %attr;
305 $raw_tag .= "&gt;";
306
307 if ($element =~ m/$self->{'root_tag'}/) {
308 $self->{'raw_text'} = $raw_tag;
309
310 $self->{'saved_metadata'} = {};
311 $self->{'metaname_stack'} = [];
312 $self->{'lom_datatype'} = "";
313 $self->{'lom_language'} = undef;
314 $self->{'metadatatext'} = "<table class=\"metadata\" width=\"_pagewidth_\" >\n";
315 }
316 else {
317 my $xml_depth = scalar(@{$self->{'metaname_stack'}});
318 $self->{'raw_text'} .= "\n";
319 $self->{'raw_text'} .= "&nbsp;&nbsp;" x $xml_depth;
320 $self->{'raw_text'} .= $raw_tag;
321
322 my $metaname_stack = $self->{'metaname_stack'};
323 push(@$metaname_stack,$element);
324 if (scalar(@$metaname_stack)==1) {
325 # top level LOM category
326 my $style = "class=\"metadata\"";
327 my $open_close
328 = "<a id=\"${element}opencloselink\" href=\"javascript:hideTBodyArea('$element')\">\n";
329 $open_close
330 .= "<img id=\"${element}openclose\" border=\"0\" src=\"_httpopenmdicon_\"></a>\n";
331
332 my $header_line = " <tr $style ><th $style colspan=\"3\">$open_close \u$element</th></tr>\n";
333 my $md_tbody = "<tbody id=\"$element\">\n";
334
335 $self->{'mdheader'} = $header_line;
336 $self->{'mdtbody'} = $md_tbody;
337 $self->{'mdtbody_text'} = "";
338 }
339 }
340}
341
342sub EndTag {
343 my ($expat, $element) = @_;
344
345 my $raw_tag = "&lt;/$element&gt;";
346
347 if ($element =~ m/$self->{'root_tag'}/) {
348 $self->{'raw_text'} .= $raw_tag;
349
350 my $metadatatext = $self->{'metadatatext'};
351 $metadatatext .= "</table>";
352
353 my $raw_text = $self->{'raw_text'};
354
355 $self->{'saved_metadata'}->{'MetadataTable'} = $metadatatext;
356 $self->{'metadatatext'} = "";
357
358 $self->{'saved_metadata'}->{'rawtext'} = $raw_text;
359 $self->{'raw_text'} = "";
360
361 if (defined $self->{'lom_language'}) {
362 $self->{'saved_metadata'}->{'lom_language'} = $self->{'lom_language'};
363 $self->{'lom_language'} = undef;
364 }
365 }
366 else {
367 my $metaname_stack = $self->{'metaname_stack'};
368
369 if (scalar(@$metaname_stack)==1) {
370 my $header_line = $self->{'mdheader'};
371 my $tbody_start = $self->{'mdtbody'};
372 my $tbody_text = $self->{'mdtbody_text'};
373 if ($tbody_text !~ m/^\s*$/s) {
374 my $tbody_end = "</tbody>\n";
375 my $table_chunk
376 = $header_line.$tbody_start.$tbody_text.$tbody_end;
377
378 $self->{'metadatatext'} .= $table_chunk;
379 }
380 $self->{'mdtheader'} = "";
381 $self->{'mdtbody'} = "";
382 $self->{'mdtbody_text'} = "";
383 }
384
385 pop(@$metaname_stack);
386
387 my $xml_depth = scalar(@{$self->{'metaname_stack'}});
388 $self->{'raw_text'} .= "\n";
389 $self->{'raw_text'} .= "&nbsp;&nbsp;" x $xml_depth;
390 $self->{'raw_text'} .= $raw_tag;
391 }
392}
393
394sub process_datatype_info
395{
396 my $self = shift(@_);
397 my ($metaname_stack,$md_content) = @_;
398
399 my @without_dt_stack = @$metaname_stack; # without datatype stack
400
401 my $innermost_element = $without_dt_stack[$#without_dt_stack];
402
403 # Loose last item if encoding datatype information
404 if ($innermost_element =~ m/^(lang)?string$/) {
405 $self->{'lom_datatype'} = $innermost_element;
406
407 pop @without_dt_stack;
408 $innermost_element = $without_dt_stack[$#without_dt_stack];
409 }
410 elsif ($innermost_element =~ m/^date(Time)?$/i) {
411 if ($innermost_element =~ m/^date$/i) {
412 $self->{'lom_datatype'} = "dateTime";
413 }
414 else {
415 $self->{'lom_datatype'} = $innermost_element;
416
417 pop @without_dt_stack;
418 $innermost_element = $without_dt_stack[$#without_dt_stack];
419 }
420
421 if ($md_content =~ m/^(\d{1,2})\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s*(\d{4})/i) {
422 my ($day,$mon,$year) = ($1,$2,$3);
423
424 my %month_lookup = ( 'jan' => 1, 'feb' => 2, 'mar' => 3,
425 'apr' => 4, 'may' => 5, 'jun' => 6,
426 'jul' => 7, 'aug' => 8, 'sep' => 9,
427 'oct' => 10, 'nov' => 11, 'dec' => 12 );
428
429 my $mon_num = $month_lookup{lc($mon)};
430
431 $md_content = sprintf("%d%02d%02d",$year,$mon_num,$day);
432 }
433
434 $md_content =~ s/\-//g;
435 }
436
437 if ($innermost_element eq "source") {
438 $self->{'lom_source'} = $md_content;
439 }
440 elsif ($innermost_element eq "value") {
441 $self->{'lom_value'} = $md_content;
442 }
443
444 return (\@without_dt_stack,$innermost_element,$md_content);
445}
446
447sub reset_datatype_info
448{
449 my $self = shift(@_);
450
451 $self->{'lom_datatype'} = "";
452}
453
454
455sub pretty_print_text
456{
457 my $self = shift(@_);
458
459 my ($pretty_print_text) = @_;
460
461## $metavalue_utf8 = &util::hyperlink_text($metavalue_utf8);
462 $pretty_print_text = &util::hyperlink_text($pretty_print_text);
463
464#### $pretty_print_text =~ s/(BEGIN:vCard.*END:vCard)/<pre>$1<\/pre>/sg;
465
466 if ($self->{'lom_datatype'} eq "dateTime") {
467 if ($pretty_print_text =~ m/^(\d{4})(\d{2})(\d{2})$/) {
468 $pretty_print_text = "$1-$2-$3";
469 }
470 }
471
472 return $pretty_print_text;
473}
474
475sub pretty_print_table_tr
476{
477 my $self = shift (@_);
478 my ($without_dt_stack) = @_;
479
480 my $style = "class=\"metadata\"";
481
482 my $innermost_element = $without_dt_stack->[scalar(@$without_dt_stack)-1];
483 my $outermost_element = $without_dt_stack->[0];
484
485 # Loose top level stack item (already named in pretty print table)
486 my @pretty_print_stack = @$without_dt_stack;
487 shift @pretty_print_stack;
488
489 if ($innermost_element eq "source") {
490 return if (!defined $self->{'lom_value'});
491 }
492
493 if ($innermost_element eq "value") {
494 return if (!defined $self->{'lom_source'});
495 }
496
497 my $pretty_print_text = "";
498
499 if (($innermost_element eq "value") || ($innermost_element eq "source")) {
500 my $source = $self->{'lom_source'};
501 my $value = $self->pretty_print_text($self->{'lom_value'});
502
503 $self->{'lom_source'} = undef;
504 $self->{'lom_value'} = undef;
505
506 pop @pretty_print_stack;
507
508 $pretty_print_text = "<td $style>$source</td><td $style>$value</td>";
509 }
510 else {
511 $pretty_print_text = $self->pretty_print_text($_);
512 $pretty_print_text = "<td $style colspan=2>$pretty_print_text</td>";
513 }
514 my $pretty_print_fmn = join(' : ',map { "\u$_"; } @pretty_print_stack);
515
516
517 # my $tr_attr = "id=\"$outermost_element\" style=\"display:block;\"";
518 my $tr_attr = "$style id=\"$outermost_element\"";
519
520 my $mdtext_line = " <tr $tr_attr><td $style><nobr>$pretty_print_fmn</nobr></td>$pretty_print_text</tr>\n";
521 $self->{'mdtbody_text'} .= $mdtext_line;
522}
523
524
525sub check_for_language
526{
527 my $self = shift(@_);
528 my ($innermost_element,$md_content) = @_;
529
530 # Look for 'language' tag
531 if ($innermost_element eq "language") {
532 my $lom_lang = $self->{'lom_language'};
533
534 if (defined $lom_lang) {
535 my $new_lom_lang = $md_content;
536 $new_lom_lang =~ s/-.*//; # remove endings like -US or -GB
537
538 if ($lom_lang ne $new_lom_lang) {
539 my $outhandle = $self->{'outhandle'};
540
541 print $outhandle "Warning: Conflicting general language in record\n";
542 print $outhandle " $new_lom_lang (previous value for language = $lom_lang)\n";
543 }
544 # otherwise, existing value OK => do nothing
545 }
546 else {
547 $lom_lang = $md_content;
548 $lom_lang =~ s/-.*//; # remove endings like -US or -GB
549
550 $self->{'lom_language'} = $lom_lang;
551 }
552 }
553}
554
555sub found_specific_identifier
556{
557 my $self = shift(@_);
558 my ($specific_id,$full_mname,$md_content) = @_;
559
560 my $found_id = 0;
561 if ($full_mname eq $specific_id) {
562 if ($md_content =~ m/^(http|ftp):/) {
563 $found_id = 1;
564 }
565 }
566
567 return $found_id;
568}
569
570sub download_srcdoc
571{
572 my $self = shift(@_);
573 my ($doc_url) = @_;
574
575 my $outhandle = $self->{'outhandle'};
576 my $output_dir = $self->{'output_dir'};
577
578 $output_dir = &util::filename_cat($output_dir,"_gsdldown.all");
579
580 if (! -d $output_dir) {
581 mkdir $output_dir;
582 }
583
584 my $re_dirsep = &util::get_re_dirsep();
585 my $os_dirsep = &util::get_dirsep();
586
587 my $file_url = $doc_url;
588 $file_url =~ s/$re_dirsep/$os_dirsep/g;
589 $file_url =~ s/^(http|ftp):\/\///;
590 $file_url .= "index.html" if ($file_url =~ m/\/$/);
591
592 my $full_file_url = &util::filename_cat($output_dir,$file_url);
593 # the path to srcdoc will be used later in extrametadata to associate
594 # the lom metadata with the document. Needs to be relative to current
595 # directory.
596 my $srcdoc_path = &util::filename_cat("_gsdldown.all", $file_url);
597 my $check_timestamp = $self->{'check_timestamp'};
598 my $status;
599
600 if (($check_timestamp) || (!$check_timestamp && !-e $full_file_url)) {
601 if (!-e $full_file_url) {
602 print $outhandle "Mirroring $doc_url\n";
603 }
604 else {
605 print $outhandle "Checking to see if update needed for $doc_url\n";
606 }
607
608 # on linux, if we pass an absolute path as -P arg to wget, then it
609 # stuffs up the
610 # URL rewriting in the file. Need a relative path or none, so now
611 # we change working directory first.
612 my $changed_dir = 0;
613 my $current_dir = cwd();
614 my $wget_cmd = "";
615 if ($ENV{'GSDLOS'} ne "windows") {
616 $changed_dir = 1;
617
618 chdir "$output_dir";
619 $wget_cmd = "wget -nv --timestamping -k -p \"$doc_url\"";
620 } else {
621 $wget_cmd = "wget -nv -P \"$output_dir\" --timestamping -k -p \"$doc_url\"";
622 }
623 ##print STDERR "**** wget = $wget_cmd\n";
624
625
626 $status = system($wget_cmd);
627 if ($changed_dir) {
628 chdir $current_dir;
629 }
630 if ($status==0) {
631 $self->{'lom_srcdoc'} = $srcdoc_path;
632 }
633 else {
634 $self->{'lom_srcdoc'} = undef;
635 print $outhandle "Error: failed to execute $wget_cmd\n";
636 }
637 }
638 else {
639 # not time-stamping and file already exists
640 $status=0;
641 $self->{'lom_srcdoc'} = $srcdoc_path;
642 }
643
644 return $status==0;
645
646}
647
648
649sub check_for_identifier
650{
651 my $self = shift(@_);
652 my ($full_mname,$md_content) = @_;
653
654 my $success = 0;
655
656 my $download_re = $self->{'download_srcdocs'};
657 if (($download_re ne "") && $md_content =~ m/$download_re/) {
658
659 if ($self->found_specific_identifier("general^identifier^entry",$full_mname,$md_content)) {
660 $success = $self->download_srcdoc($md_content);
661 }
662
663 if (!$success) {
664 if ($self->found_specific_identifier("technical^location",$full_mname,$md_content)) {
665 $success = $self->download_srcdoc($md_content);
666 }
667 }
668 }
669
670 return $success;
671}
672
673
674sub Text {
675 if ($_ !~ m/^\s*$/) {
676 #
677 # Work out indentations and line wraps for raw XML
678 #
679 my $xml_depth = scalar(@{$self->{'metaname_stack'}})+1;
680 my $indent = "&nbsp;&nbsp;" x $xml_depth;
681
682 my $formatted_text = "\n".$_;
683
684 # break into lines < 80 chars on space
685 $formatted_text =~ s/(.{50,80})\s+/$1\n/mg;
686 $formatted_text =~ s/^/$indent/mg;
687 ## $formatted_text =~ s/\s+$//s;
688
689 $self->{'raw_text'} .= $formatted_text;
690 }
691
692 my $metaname_stack = $self->{'metaname_stack'};
693 if (($_ !~ /^\s*$/) && (scalar(@$metaname_stack)>0)) {
694
695 my ($without_dt_stack,$innermost_element,$md_content)
696 = $self->process_datatype_info($metaname_stack,$_);
697
698 $self->pretty_print_table_tr($without_dt_stack);
699
700 my $full_mname = join('^',@{$without_dt_stack});
701 $self->set_filere_metadata(lc($full_mname),$md_content);
702
703 $self->check_for_language($innermost_element,$md_content);
704 $self->check_for_identifier($full_mname,$md_content); # source doc
705
706 $self->reset_datatype_info();
707 }
708}
709
710# This Char function overrides the one in XML::Parser::Stream to overcome a
711# problem where $expat->{Text} is treated as the return value, slowing
712# things down significantly in some cases.
713sub Char {
714 $_[0]->{'Text'} .= $_[1];
715 return undef;
716}
717
7181;
Note: See TracBrowser for help on using the repository browser.