File Coverage

blib/lib/WWW/SherlockSearch.pm
Criterion Covered Total %
statement 15 370 4.0
branch 0 146 0.0
condition 0 49 0.0
subroutine 5 39 12.8
pod 0 30 0.0
total 20 634 3.1


line stmt bran cond sub pod time code
1             # $File: //member/autrijus/WWW-SherlockSearch/lib/WWW/SherlockSearch.pm $ $Author: autrijus $
2             # $Revision: #32 $ $Change: 10623 $ $DateTime: 2004/05/22 08:07:29 $ vim: expandtab shiftwidth=4
3              
4             package WWW::SherlockSearch;
5             $WWW::SherlockSearch::VERSION = '0.20';
6              
7 1     1   2245 use strict;
  1         4  
  1         110  
8 1     1   9 use vars qw($ExcerptLength $UAClass);
  1         3  
  1         94  
9              
10 1     1   2660 use HTTP::Cookies;
  1         22149  
  1         31  
11 1     1   3062 use HTTP::Request::Common;
  1         60464  
  1         96  
12 1     1   4915 use WWW::SherlockSearch::Results;
  1         5  
  1         9813  
13              
14             $ExcerptLength = 100;
15             $UAClass = 'LWP::RobotUA';
16              
17             =head1 NAME
18              
19             WWW::SherlockSearch - Parse and execute Apple Sherlock 2 plugins
20              
21             =head1 VERSION
22              
23             This document describes version 0.20 of WWW::SherlockSearch, released
24             May 22, 2004.
25              
26             =head1 SYNOPSIS
27              
28             use WWW::SherlockSearch;
29              
30             my $sherlock = WWW::SherlockSearch->new('google.src');
31              
32             my $text = $sherlock->asString;
33             my $rss = $sherlock->asRssString;
34             my $src = $sherlock->asSherlockString;
35              
36             # fiind 'test' with limit '10'
37             my $results = $sherlock->find("test", 10);
38              
39             my $text = $results->asString;
40             my $rss = $results->asRssString;
41             my $html = $results->asHtmlString;
42              
43             =head1 DESCRIPTION
44              
45             This module parses and executes Apple Sherlock 2 plugin files,
46             and generate a result set that can be expressed in text, HTML
47             or RSS format. It is a repackaged and cleaned-up version of
48             Damian Steer's B service at L.
49              
50             The module differ from other Sherlock implementation in that
51             it can actually follow the individual links and extract the
52             full text within it, delimited by the C
53             and C tags. In RSS, they will be expressed
54             via the C attribute proposed by Aaron.
55              
56             If there is no I but I is available, the
57             C<$WWW::SherlockSearch::ExcerptLength> variable is used to
58             determine how many leading characters to use to generate the
59             description from content (defaults to C<100>). Setting it to
60             C<0> disables this feature.
61              
62             Please see L for a repository and
63             detailed description of Sherlock 2 plugins.
64              
65             =cut
66              
67             sub import {
68 0     0     my $class = shift;
69 0 0         $UAClass = shift if @_;
70             }
71              
72             sub new {
73 0     0 0   my $type = shift;
74 0           my $self = {};
75 0           bless($self, $type);
76 0 0         $self->loadFile(shift) if @_;
77 0           return $self;
78             }
79              
80             sub getChannelUrl {
81 0     0 0   my $self = shift;
82 0           return $self->{channelUrl};
83             }
84              
85             sub setChannelUrl {
86 0     0 0   my $self = shift;
87 0           $self->{channelUrl} = shift;
88 0           return $self;
89             }
90              
91             sub getQueryAttr {
92 0     0 0   my $self = shift;
93 0           return $self->{queryAttr};
94             }
95              
96             sub setQueryAttr {
97 0     0 0   my $self = shift;
98 0           $self->{queryAttr} = shift;
99 0           return $self;
100             }
101              
102             sub getPictureUrl {
103 0     0 0   my $self = shift;
104 0           return $self->{pictureUrl};
105             }
106              
107             sub setPictureUrl {
108 0     0 0   my $self = shift;
109 0           $self->{pictureUrl} = shift;
110 0           return $self;
111             }
112              
113             sub loadFile {
114 0     0 0   my $self = shift;
115 0 0         my $filename = shift or return;
116              
117 0 0         if (UNIVERSAL::isa($filename, 'SCALAR')) {
118 0           $self->initialiseSearch($$filename);
119             }
120             else {
121 0           local $/;
122 0 0         open(SHERFILE, $filename) or die "Couldn't open $filename: $!";
123 0           $self->initialiseSearch();
124 0           close SHERFILE;
125             }
126              
127 0           return $self;
128             }
129              
130             sub initialiseSearch {
131 0     0 0   my ($self, $content) = @_;
132 0           my ($action, $basehref, $host);
133              
134 0 0         if ($content) {
135 0           @{$self}{qw{
  0            
136             search interpretList inputList
137             prefetch preinputList postfetch postinputList
138             }} = parseSherlock(\$content);
139             }
140              
141 0 0         $action = $self->{search}{action} or return;
142              
143 0           ($basehref) = ($action =~ /(.*\/)/);
144 0           ($host) = ($action =~ /(.*\/\/.*?)\//);
145              
146 0           $self->{basehref} = $basehref;
147 0           $self->{host} = $host;
148              
149 0           return $self;
150             }
151              
152             # The following parses sherlock .src files
153              
154             # This takes a sherlock file, strips the comments, then passes the
155             # individual tags for further parsing.
156              
157             my %Attr = (
158             search => [qw{
159             name method action update updateCheckDays
160             description bannerImage bannerLink routeType
161             queryEncoding queryCharset queryLimit
162             }],
163             input => [qw{
164             value name user user1 user2 user3 usern prefix suffix mode
165             }],
166             interpret => [qw{
167             bannerStart bannerEnd relevanceStart relevanceEnd
168             resultListStart resultListEnd resultItemStart resultItemEnd
169             priceStart priceEnd availStart availEnd dateStart dateEnd
170             nameStart nameEnd emailStart emailEnd
171             pageNextStart pageNextEnd
172             resultItemFind resultItemReplace
173             resultContentStart resultContentEnd
174             }],
175             );
176              
177             sub parseSherlock {
178 0     0 0   my $sherfiletoparse = shift;
179              
180             # XXX: inputprev, inputnext?
181 0           my $tags = join('|', qw{
182             search interpret input prefetch postfetch
183             /search /prefetch /postfetch
184             });
185 0           my $searchAttributes = join('|', map lc, @{$Attr{search}});
  0            
186 0           my $inputAttributes = join('|', map lc, @{$Attr{input}});
  0            
187 0           my $interpretAttributes = join('|', map lc, @{$Attr{interpret}});
  0            
188              
189 0           my ($search, $prefetch, $postfetch, $interpret, $input);
190 0           my $interpretList = [];
191 0           my $inputList = [];
192 0           my $preinputList = [];
193 0           my $postinputList = [];
194 0           my ($tag, $current_tag);
195              
196 0           $$sherfiletoparse =~ s/\r/\n/g; # fix line endings
197 0           $$sherfiletoparse =~ s/(?:\s+|^)\#(?:\s+$).*//g; # remove comment lines
198              
199             PARSELOOP:
200 0           while ($$sherfiletoparse =~ /<\s*($tags)/gcis) {
201 0           $tag = $1;
202 0           $tag =~ tr/A-Z/a-z/;
203              
204 0 0         if ($tag eq 'search') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
205 0           $search = parseAttValString($sherfiletoparse, $searchAttributes);
206 0           $current_tag = 'search';
207             }
208             elsif ($tag eq 'prefetch') {
209 0           $prefetch =
210             parseAttValString($sherfiletoparse, $searchAttributes);
211 0           $current_tag = 'prefetch';
212             }
213             elsif ($tag eq 'postfetch') {
214 0           $postfetch =
215             parseAttValString($sherfiletoparse, $searchAttributes);
216 0           $current_tag = 'postfetch';
217             }
218             elsif ($tag eq 'interpret') {
219 0           $interpret =
220             parseAttValString($sherfiletoparse, $interpretAttributes);
221 0           push (@{$interpretList}, $interpret);
  0            
222             }
223             elsif ($tag eq 'input') {
224 0           $input = parseAttValString($sherfiletoparse, $inputAttributes);
225 0 0         if ($current_tag eq "prefetch") {
    0          
226 0           push (@{$preinputList}, $input);
  0            
227             }
228             elsif ($current_tag eq "postfetch") {
229 0           push (@{$postinputList}, $input);
  0            
230             }
231             else {
232 0           push (@{$inputList}, $input);
  0            
233             }
234             }
235             elsif ($tag eq '/prefetch') {
236 0           $current_tag = '';
237             }
238             elsif ($tag eq '/postfetch') {
239 0           $current_tag = '';
240             }
241             elsif ($tag eq '/search') {
242 0           last PARSELOOP;
243             }
244             }
245 0           return ($search, $interpretList, $inputList, $prefetch, $preinputList,
246             $postfetch, $postinputList);
247             }
248              
249             # This parses a string containg items of the form attribute = string
250             # or just attribute and returns a hash of attribute=>value.
251             # For lone attributes value = 'true'.
252             # $parse is of the form "attr|attr|attr|...|attr"
253              
254             sub parseAttValString {
255 0     0 0   my $stringtoparse = shift;
256 0           my $attrList = shift;
257              
258 0           my ($value, $attribute);
259 0           my $returnHash = {};
260              
261             PARSELOOP2:
262 0           while ($$stringtoparse =~ /($attrList)\s*(=| |>)\s*/gcis) {
263 0           $attribute = $1;
264 0           $attribute =~ tr/A-Z/a-z/; # lowercase to make my life easy
265 0 0         if ($2 eq '=') {
266 0           $value = parseString($stringtoparse);
267             }
268 0           else { $value = 'true'; }
269 0           $returnHash->{$attribute} = $value;
270 0 0 0       if (($2 eq '>') or ($$stringtoparse =~ /\G\s*>/gcs)) {
271 0           last PARSELOOP2;
272             }
273             }
274 0           return $returnHash;
275             }
276              
277             # This takes a string of the form "....", '.....',
278             # or just ..... (no whitespace)
279             # And returns the ..... bit.
280              
281             sub parseString {
282 0     0 0   my $string = shift;
283 0           my $content;
284 0           my $skip = 2;
285              
286 0 0         if ($$string =~ /\G\"/gcs) {
    0          
287 0 0         $content = $1 if ($$string =~ /\G(.*?[^\\])\"/gcs);
288 0           $content =~ s{\\"}{"}g;
289             }
290             elsif ($$string =~ /\G\'/gcs) {
291 0 0         $content = $1 if ($$string =~ /\G(.*?[^\\])\'/gcs);
292 0           $content =~ s{\\'}{'}g;
293             }
294             else {
295 0 0         $content = $1 if ($$string =~ /\G(\S+)/gcs);
296 0 0         if ($content =~ s/>$//) # this removes a closing tag (if present)
297             {
298 0           pos($$string)--; # this corrects for that removal
299             }
300             }
301 0           return $content;
302             }
303              
304             sub printHash {
305 0     0 0   my $hashRef = shift;
306 0           my $tab = shift;
307              
308 0           my $key;
309              
310 0           foreach $key (keys %$hashRef) {
311 0           print "$tab$key := ", $hashRef->{$key}, "\n";
312             }
313             }
314              
315             sub find {
316 0     0 0   my ($self, $query, $limit, $skip_href) = @_;
317              
318 0           my $ua_pm = "$UAClass.pm";
319 0           $ua_pm =~ s{::}{/}g;
320 0           require $ua_pm;
321            
322 0           my $search = $UAClass->new(
323             'Mozilla/5.0 Gecko/libwww-perl', 'autrijus@cpan.org',
324             );
325 0           my ($result, @post, $get, $rv);
326              
327 0           $search->cookie_jar(HTTP::Cookies->new);
328              
329 0           foreach my $stage (qw/prefetch search postfetch/) {
330 0 0 0       next unless $self->{$stage}{method} and $self->{$stage}{action};
331              
332 0   0       my $find = _encode(
333             $self->{$stage}{querycharset} || $self->{$stage}{queryencoding},
334             $query,
335             );
336              
337 0 0         if ($self->{$stage}->{method} =~ /post/i) {
    0          
338 0           @post = $self->getPostData($stage, $find);
339 0           $result = $search->request(POST $self->{$stage}->{action}, \@post);
340             }
341             elsif ($self->{$stage}->{method} =~ /get/i) {
342 0           $get = $self->getGetData($stage, $find);
343 0 0         $get = '' if $get eq '?';
344 0           $result = $search->request(GET $self->{$stage}->{action} . $get);
345             }
346             else {
347 0           die "Unknown method: $stage / $self->{$stage}->{method}";
348             }
349              
350 0 0         next unless $stage eq 'search';
351            
352 0           while ($result) {
353 0 0         if (!$result->is_success) {
354 0           print "$stage / $self->{$stage}->{method}: Warn: " .
355             $result->code . " " . $result->message;
356 0           last;
357             }
358              
359 0   0       $self->{content} = _decode(
360             $self->{$stage}{querycharset} || $self->{$stage}{queryencoding},
361             $result->content,
362             );
363              
364 0           ($rv, $result) = $self->convertResults(
365             $self->{content}, $search, $limit, $skip_href, $result->date, $rv
366             );
367             }
368             }
369              
370 0           return $rv;
371             }
372              
373             sub _encode {
374 0   0 0     my $charset = shift || 'utf8';
375 0 0         require Encode::compat if $] < 5.007001; require Encode;
  0            
376 0           return Encode::encode($charset, $_[0]);
377             }
378              
379             sub _decode {
380 0   0 0     my $charset = shift || 'utf8';
381 0 0         require Encode::compat if $] < 5.007001; require Encode;
  0            
382 0           return Encode::decode($charset, $_[0]);
383             }
384              
385             sub getPostData {
386 0     0 0   my ($self, $tag, $find) = @_;
387 0           my (@post, $item);
388 0           my $list;
389              
390 0 0         if ($tag eq 'prefetch') {
    0          
391 0           $list = 'preinputList';
392             }
393             elsif ($tag eq 'postfetch') {
394 0           $list = 'postinputList';
395             }
396             else {
397 0           $list = 'inputList';
398             }
399              
400 0           foreach $item (@{ $self->{$list} }) {
  0            
401 0 0         if ($item->{user}) {
    0          
402 0           push (@post, $item->{name}, $find);
403             }
404             elsif ($item->{mode} ne 'browser') {
405 0           push (@post, $item->{name}, $item->{value});
406             }
407             }
408 0           return @post;
409             }
410              
411             sub getGetData {
412 0     0 0   my ($self, $tag, $find) = @_;
413 0           my ($get, $item, $amp);
414 0           my $list;
415              
416 0 0         if ($tag eq 'prefetch') {
    0          
417 0           $list = 'preinputList';
418             }
419             elsif ($tag eq 'postfetch') {
420 0           $list = 'postinputList';
421             }
422             else {
423 0           $list = 'inputList';
424             }
425              
426 0           $get = "?";
427 0           $amp = "";
428 0           foreach $item (@{ $self->{$list} }) {
  0            
429 0 0         if ($item->{user}) {
    0          
430 0           $get .= $amp . $item->{name} . "=" . $find;
431 0           $amp = "&";
432             }
433             elsif ($item->{mode} ne 'browser') {
434 0           $get .= $amp . $item->{name} . "=" . $item->{value};
435 0           $amp = "&";
436             }
437             }
438 0           return $get;
439             }
440              
441             # The following methods are used to interpret results
442              
443             sub convertResults {
444 0     0 0   my ($self, $html, $search, $limit, $skip_href, $result_date, $resultStruct) = @_;
445 0   0       $limit ||= $self->{search}{querylimit};
446              
447             # It appears plugins can have more than one interpet tag
448             # I only use the first
449 0           my $interpret = $self->{interpretList}->[0];
450 0           my ($banner, @results, $bannerimageurl, $bannerurl, $pagenexturl);
451              
452 0 0         if (!$resultStruct) {
453 0           $resultStruct = WWW::SherlockSearch::Results->new;
454 0           $resultStruct->setServiceName($self->{search}{name});
455 0           $resultStruct->setServiceDescription($self->{search}{description});
456 0           $resultStruct->setBaseHREF($self->{basehref});
457 0           $resultStruct->setHost($self->{host});
458 0           $resultStruct->setPictureUrl($self->getPictureUrl);
459 0   0       $resultStruct->setChannelUrl(
460             $self->getChannelUrl ||
461             ($self->{search}{action} . $self->getGetData)
462             );
463 0           $resultStruct->setQueryAttr($self->getQueryAttr);
464             }
465              
466             # get that banner
467 0 0         if ($interpret->{bannerstart}) {
468 0           ($banner) = getDelimited(
469             \$html,
470             $interpret->{bannerstart},
471             $interpret->{bannerend}
472             );
473 0           ($bannerimageurl) = $self->getIMG($banner);
474 0           ($bannerurl) = $self->getHREF($banner);
475 0           $resultStruct->setBannerImage($bannerimageurl);
476 0           $resultStruct->setBannerLink($bannerurl);
477             }
478             else {
479 0           $resultStruct->setBannerLink(
480             $self->fixRef($self->{search}{bannerlink}));
481 0           $bannerimageurl = $self->getIMG($self->{search}{bannerimage});
482 0 0         if (!$bannerimageurl) {
483 0           $bannerimageurl = $self->fixRef($self->{search}{bannerimage});
484             }
485 0           $resultStruct->setBannerImage($bannerimageurl);
486             }
487              
488 0 0         if ($interpret->{pagenextstart}) {
489 0           ($pagenexturl) = getDelimited(
490             \$html,
491             $interpret->{pagenextstart},
492             $interpret->{pagenextend}
493             );
494 0           ($pagenexturl) = $self->getHREF($pagenexturl);
495             }
496              
497 0 0         if ($interpret->{resultliststart}) {
498 0           ($html) = getDelimited(
499             \$html,
500             $interpret->{resultliststart},
501             $interpret->{resultlistend}
502             );
503             }
504 0 0         if ($interpret->{resultitemstart}) {
505 0           @results = getDelimited(
506             \$html,
507             $interpret->{resultitemstart},
508             $interpret->{resultitemend}
509             );
510             }
511             else {
512 0           @results = ($html =~ /(<\s*A[^>]+HREF\s*=.*?(?:<\/A>|$))/sgi);
513             }
514              
515             # Find-and-Replace
516             # Thanks for mtve @ #perl for this.
517 0 0         if (length $interpret->{resultitemfind}) {
518 0           my $find = $interpret->{resultitemfind};
519 0           $find =~ s|\\Q(.*?)\\E|quotemeta($1)|eg;
  0            
520              
521 0           foreach (@results) {
522 0           s{$find}{"qq($interpret->{resultitemreplace})"}ees;
  0            
523             }
524             }
525              
526 0           my ($item, $temp, $relev, $itemurl, $content, $rest, $fulltext, $date);
527              
528 0           foreach $item (@results) {
529 0           require HTML::Entities;
530 0           $item =~ s/ / /g; # :-(~~~
531 0           HTML::Entities::decode_entities($item);
532              
533 0 0         if ($interpret->{relevancestart}) {
534 0           ($temp) = getDelimited(
535             \$item,
536             $interpret->{relevancestart},
537             $interpret->{relevanceend}
538             );
539 0           ($relev) = ($temp =~ /(\d+)/s);
540             }
541 0           ($itemurl, $content, $rest) = $self->getHREF($item);
542              
543 0 0         if ($interpret->{namestart}) {
544 0           ($temp) = getDelimited(\$item, $interpret->{namestart},
545             $interpret->{nameend});
546 0 0         $content = $temp if $temp;
547             }
548              
549 0 0         if ($interpret->{datestart}) {
550 0           ($temp) = getDelimited(\$item, $interpret->{datestart},
551             $interpret->{dateend});
552 0 0         $date = $temp if $temp;
553             }
554              
555             # The following strips tags, $content, line endings and relevance
556             # in the hope that removing this garbage will leave a nice summary
557              
558 0           stripTags(\$content);
559              
560 0 0         next if $content =~ /^\s*$/;
561              
562 0 0         $rest = $content unless ($rest);
563              
564 0           stripTags(\$rest);
565 0 0         $rest =~ s/$relev\%?//g if $relev;
566 0 0 0       next if $skip_href and exists $skip_href->{$itemurl};
567              
568 0 0         if ($interpret->{resultcontentstart}) {
569 0           my $result = $search->request(GET $itemurl);
570              
571 0 0         if ($result->is_success) {
572 0   0       my $item = _decode(
573             $self->{search}{querycharset} || $self->{search}{queryencoding},
574             $result->content,
575             );
576              
577 0           ($fulltext) = getDelimited(
578             \$item,
579             $interpret->{resultcontentstart},
580             $interpret->{resultcontentend}
581             );
582              
583 0           require HTML::Entities;
584 0           $fulltext =~ s/<[bB][rR]\b([^>]*)>/\n/gs;
585 0           $fulltext =~ s/ / /g; # :-(~~~
586 0           HTML::Entities::decode_entities($fulltext);
587              
588 0           stripTags(\$fulltext);
589 0   0       $date ||= $result->date;
590             }
591             }
592              
593             $resultStruct->add(
594 0   0       $itemurl, $content, $relev, $rest, $fulltext, $date || $result_date
595             );
596              
597 0 0 0       if ($limit and $resultStruct->getNumResults >= $limit) {
598 0           $pagenexturl = ''; # no next page, please
599 0           last;
600             }
601             }
602              
603 0           $self->{resultArray} = $resultStruct;
604              
605             return (
606 0 0         $resultStruct,
607             $pagenexturl ? ($search->request(GET $pagenexturl)) : ()
608             );
609             }
610              
611             sub stripTags {
612 0     0 0   my $var = shift;
613 0           $$var =~ s/<[bB][rR]\b([^>]*)>/\n/gs;
614 0           $$var =~ s/<([^>]+)(?:>|$)//gs;
615 0           $$var =~ s/^\s*//;
616 0           $$var =~ s/\s*$//;
617 0           $$var =~ s/\s+/ /g;
618             }
619              
620             sub getHREF {
621 0     0 0   my ($self, $html) = @_;
622 0           my ($itemurl, $content, $rest) =
623             ($html =~ /<\s*A[^>]+HREF\s*=\s*(.*?)>(.*?)(?:<\/A>|$)(.*)/si);
624 0           ($itemurl) = ($itemurl =~ /^\'?\"?(\S+)/);
625 0           $itemurl =~ s/\'?\"?$//;
626 0           $itemurl = $self->fixRef($itemurl);
627 0           return ($itemurl, $content, $rest);
628             }
629              
630             sub getIMG {
631 0     0 0   my ($self, $html) = @_;
632 0           my ($itemurl) = ($html =~ /<\s*IMG\s+SRC\s*=\s*(.*?)>/si);
633 0           ($itemurl) = ($itemurl =~ /^\'?\"?(\S+)/);
634 0           $itemurl =~ s/\'?\"?$//;
635 0           $itemurl = $self->fixRef($itemurl);
636 0           return $itemurl;
637             }
638              
639             sub fixRef {
640 0     0 0   my ($self, $url) = @_;
641 0           my ($basehref, $host);
642 0 0         if (!$url) { return; }
  0            
643 0           $basehref = $self->{basehref};
644 0           $host = $self->{host};
645              
646             # This doesn't work for relative links :-(
647 0 0         if ($url !~ m{^(?:\w+:)?//}) {
648 0 0         $url = ($url =~ m{^/}) ? $host . $url : $basehref . $url;
649             }
650 0 0 0       if ($url =~ m{^//} and $basehref =~ m{^(\w+:)}) {
651 0           $url = $1 . $url;
652             }
653 0           return $url;
654             }
655              
656             sub getResults {
657 0     0 0   my $self = shift;
658 0           return $self->{resultArray};
659             }
660              
661             sub getDelimited {
662 0     0 0   my ($list, $left, $right) = @_;
663 0           my @results;
664              
665 0           @results = ($$list =~ /\Q$left\E(.*?)\Q$right\E/gis);
666              
667 0           return @results;
668             }
669              
670             sub asString {
671 0     0 0   my $self = shift;
672 0           my $string = "Search :\n\n";
673              
674 0           $string .= "Base Href := " . $self->{basehref} . "\n";
675 0           $string .= "Host := " . $self->{host} . "\n";
676 0           foreach my $key (keys %{ $self->{search} }) {
  0            
677 0           $string .= "$key := " . $self->{search}{$key} . "\n";
678             }
679              
680 0           $string .= "\nInterpret :\n\n";
681              
682 0           foreach my $key (keys %{ $self->{interpretList}->[0] }) {
  0            
683 0           $string .= "$key := " . $self->{interpretList}->[0]->{$key} . "\n";
684             }
685              
686 0           foreach my $hash (@{ $self->{inputList} }) {
  0            
687 0           $string .= "\nInput :\n";
688 0           foreach my $key (keys %{$hash}) {
  0            
689 0           $string .= "\t$key := " . $hash->{$key} . "\n";
690             }
691             }
692 0           return $string;
693             }
694              
695             sub _fmt {
696 0     0     my ($tag, $attr) = @_;
697              
698 0 0 0       return '' unless UNIVERSAL::isa($attr, 'HASH') and %$attr;
699              
700 0           my $rv = "<\U$tag\E\n";
701 0 0         $tag = 'search' if $tag =~ /^(?:pre|post)fetch$/;
702              
703 0           foreach my $key (sort keys %$attr) {
704 0           my $val = $attr->{$key};
705 0 0         ($key) = grep {lc($_) eq lc($key)} @{$Attr{$tag}} or next;
  0            
  0            
706 0           $val =~ s/"/\\"/g;
707 0 0         $rv .= qq(\t$key="$val"\n) if length $val;
708             }
709              
710 0           return "$rv>\n";
711             }
712              
713             sub asSherlockString {
714 0     0 0   my $self = shift;
715 0           my $string = '';
716              
717 0           foreach my $stage ( qw(prefetch postfetch search) ) {
718 0 0         $string .= _fmt($stage => $self->{$stage})
719             if length $self->{$stage}{action};
720              
721 0 0         $stage =~ /^(.*?)(?:fetch)?(?:search)?$/ or next;
722              
723 0           foreach my $list (@{$self->{"$1inputList"}}) {
  0            
724 0           $string .= _fmt(input => $list);
725             }
726              
727 0 0         if ($stage eq 'search') {
728 0           foreach my $list (@{$self->{interpretList}}) {
  0            
729 0           $string .= _fmt(interpret => $list);
730             }
731             }
732              
733 0           $string .= "\n\n"
734 0 0 0       if $self->{$stage} and %{$self->{$stage}}
      0        
735             and length $self->{$stage}{action};
736             }
737              
738 0           return $string;
739             }
740              
741             sub asRssString {
742 0     0 0   my $self = shift;
743              
744 0           require XML::RSS;
745 0           my $rss = XML::RSS->(version => '1.0');
746              
747 0           $rss->channel(
748             title => fixEm($self->{search}{name}),
749             link => fixEm($self->getChannelUrl),
750             description => fixEm($self->{search}{description})
751             );
752              
753 0           $rss->image(
754             title => fixEm($self->{search}{name}),
755             url => fixEm($self->getPictureUrl),
756             link => fixEm($self->{host})
757             );
758              
759 0           $rss->textinput(
760             title => fixEm($self->{search}{name}),
761             description => "Search this site",
762             name => fixEm($self->getQueryAttr),
763             link => fixEm($self->getChannelUrl)
764             );
765              
766 0           return $rss->as_string;
767             }
768              
769             # This is a cludge to fix xml problems
770             # - bah, thought XML::RSS would do this
771              
772             sub fixEm {
773 0     0 0   my $text = shift;
774              
775 0           $text =~ s/&/&/gs;
776 0           $text =~ s/
777 0           $text =~ s/>/>/gs;
778 0           return $text;
779             }
780              
781             sub resultsAsRssString {
782 0     0 0   my $self = shift;
783 0           my $results = $self->getResults;
784 0           return $results->asRssString;
785             }
786              
787             sub resultsAsString {
788 0     0 0   my $self = shift;
789 0           my $results = $self->getResults;
790 0           return $results->asString;
791             }
792              
793             sub resultsAsHtmlString {
794 0     0 0   my $self = shift;
795 0           my $results = $self->getResults;
796 0           return $results->asHtmlString;
797             }
798              
799             1;
800              
801             =head1 SEE ALSO
802              
803             L
804              
805             L, L
806              
807             =head1 AUTHORS
808              
809             =over 4
810              
811             =item *
812              
813             Damian Steer ED.M.Steer@lse.ac.ukE
814              
815             =item *
816              
817             Kang-min Liu Egugod@gugod.org
818              
819             =item *
820              
821             Autrijus Tang Eautrijus@autrijus.orgE
822              
823             =back
824              
825             =head1 COPYRIGHT
826              
827             Copyright 1999, 2000, 2001 by Damian Steer.
828              
829             Copyright 2002, 2003 by Kang-min Liu.
830              
831             Copyright 2002, 2003, 2004 by Autrijus Tang.
832              
833             This program is free software; you can redistribute it and/or modify it
834             under the same terms as Perl itself.
835              
836             See L
837              
838             =cut