File Coverage

blib/lib/XML/XSH/Completion.pm
Criterion Covered Total %
statement 9 52 17.3
branch 0 18 0.0
condition 0 3 0.0
subroutine 3 8 37.5
pod n/a
total 12 81 14.8


line stmt bran cond sub pod time code
1             # $Id: Completion.pm,v 1.20 2003/09/10 13:36:26 pajas Exp $
2              
3             package XML::XSH::Completion;
4              
5 4     4   1918 use XML::XSH::CompletionList;
  4         9  
  4         102  
6 4     4   23 use XML::XSH::Functions qw();
  4         7  
  4         64  
7 4     4   20 use strict;
  4         7  
  4         9822  
8              
9             our @PATH_HASH;
10             our $M=qr/(?:^|[;}]|\s+{)\s*/;
11             our $match_sv=qr/\$([a-zA-Z0-9_]*)$/; # scalar variable completion
12             our $match_nv=qr/\%([a-zA-Z0-9_]*)$/; # node-list variable completion
13             our $match_command=qr/${M}[^=\s]*$/; # command completion
14             our $match_func=qr/${M}(?:call|undef|undefine)\s+(\S*)$/; # function name completion
15             our $match_nodetype=qr/${M}x?(?:insert|add)\s+(\S*)$/; # node-type completion
16             our $match_doc=qr/${M}(?:close|doc[-_]info|dtd|enc)\s+(\S*)$|${M}clone\s+[a-zA-Z0-9_]*\s*=\s*[a-zA-Z0-9_]*$|${M}create\s+[a-zA-Z0-9_]*/; # docid completion
17             our $match_clone_doc=qr/${M}clone\s+[a-zA-Z0-9_]*$/;
18             our $match_help=qr/${M}(?:\?|help)\s+(\S*)$/; # help topic completion
19             our $match_open_flag1=qr/${M}open(?:\s+|([-_]))([A-Z]*)$/;
20             our $match_open_flag2=qr/${M}(?:open\s+|(open[-_]))(html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
21             our $match_open_doc=qr/${M}(open)(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
22             our $match_open_filename=qr/${M}(?:open(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?)?[a-zA-Z0-9_]+\s*=\s*(\S*)$/;
23              
24             our $match_save_flag1=qr/${M}save(?:\s+|([-_]))([A-Z]*)$/;
25             our $match_save_flag2=qr/${M}(?:save\s+|(save[-_]))(html|xml|xinclude|HTML|XML|XINCLUDE|XInclude)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
26             our $match_save_doc=qr/${M}(save)(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
27             our $match_save_filename=qr/${M}save(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?[a-zA-Z0-9_]+\s+(\S*)$/;
28             our $match_filename=qr/${M}(?:\.|include)\s+(\S*)$/;
29             our $match_dir=qr/${M}(?:lcd)\s+(\S*)$/;
30             our $match_path_filename=qr/${M}(?:system\s|exec\s|\!)\s*\S*$|\s\|\s*\S*$/;
31             our $match_no_xpath=join '|',@XML::XSH::CompletionList::XSH_NOXPATH_COMMANDS;
32             our $match_no=qr/${M}(?:${match_no_xpath}|create\s+[a-zA-Z0-9_]*\s)\s*$/;
33              
34             # PATH-completion: system, !, exec, |,
35              
36             our @nodetypes = qw(element attribute attributes text cdata pi comment chunk entity_reference);
37             our @openflags1 = qw(HTML XML DOCBOOK);
38             our @openflags2 = qw(FILE PIPE STRING);
39             our @saveflags1 = qw(HTML XML XINCLUDE);
40             our @saveflags2 = qw(FILE PIPE STRING);
41              
42             sub perl_complete {
43 0     0     my($word,$line,$pos) = @_;
44 0           my $endpos=$pos+length($word);
45 0           cpl('perl',$word,$line,$pos,$endpos);
46             }
47              
48             sub gnu_complete {
49 0     0     my($text, $line, $start, $endpos) = @_;
50 0           &main::_term()->Attribs->{completion_append_character} = ' ';
51 0           my @result=cpl('gnu',$text,$line,$start,$endpos);
52             # find longest common match. Can anybody show me how to persuade
53             # T::R::Gnu to do this automatically? Seems expensive.
54 0 0         return () unless @result;
55 0           my($newtext) = $text;
56 0           for (my $i = length($text)+1;;$i++) {
57 0 0 0       last unless length($result[0]) && length($result[0]) >= $i;
58 0           my $try = substr($result[0],0,$i);
59 0           my @tries = grep {substr($_,0,$i) eq $try} @result;
  0            
60             # warn "try[$try]tries[@tries]";
61 0 0         if (@tries == @result) {
62 0           $newtext = $try;
63             } else {
64 0           last;
65             }
66             }
67 0           ($newtext,@result);
68             }
69              
70             sub complete_set_term_char {
71 0     0     my ($type,$char)=@_;
72 0 0         if ($type eq 'perl') {
73 0           $readline::rl_completer_terminator_character = $char;
74             } else {
75 0           &main::_term()->Attribs->{completion_append_character} = $char;
76             }
77             }
78              
79             sub complete_filename {
80 0     0     my ($type,$word)=@_;
81 0 0         if ($type eq 'perl') {
82 0           return eval { map { s:\@$::; $_ } readline::rl_filename_list($word); };
  0            
  0            
  0            
83             } else {
84 0           return eval { map { s:\@$::; $_ } Term::ReadLine::Gnu::XS::rl_filename_list($word) };
  0            
  0            
  0            
85             }
86             }
87              
88             sub rehash_path_hash {
89 0     0     my %result;
90             my $dh;
91 0 0         my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
92 0 0         my $delim=($^O eq 'MSWin32' ? ';' : ':');
93 0           my @path=grep /\S/,split($delim,$ENV{PATH});
94 0           foreach my $dir (@path) {
95 0           local *DIR;
96 0 0         if (opendir DIR, $dir) {
97 0 0         my @files=grep { -f "$dir$pdelim$_" and -x "$dir$pdelim$_" } readdir(DIR);
  0            
98 0           @result{@files}=();
99 0           closedir DIR;
100             }
101             }
102 0           @PATH_HASH=sort keys %result;
103             }
104              
105             sub complete_system_command {
106             my ($type,$word)=@_;
107             my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
108             if (index($word,$pdelim)>=0) {
109             return grep -x,complete_filename($type,$word);
110             }
111             unless (defined @PATH_HASH) {
112             rehash_path_hash();
113             }
114             return grep {index($_,$word)==0} @PATH_HASH;
115             }
116              
117             sub cpl {
118             my($type,$word,$line,$pos,$endpos) = @_;
119             if (substr($line,0,$endpos)=~$match_sv) {
120             return map {'$'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::string_vars;
121             } elsif (substr($line,0,$endpos)=~$match_nv) {
122             return map {'%'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::nodelist_vars;
123             } elsif (substr($line,0,$endpos)=~$match_func) {
124             return grep { index($_,$1)==0 } XML::XSH::Functions::defs;
125             } elsif (substr($line,0,$endpos)=~$match_nodetype) {
126             return grep { index($_,$1)==0 } @nodetypes;
127             } elsif (substr($line,0,$endpos)=~$match_help) {
128             return grep { index($_,$1)==0 } keys %XML::XSH::Help::HELP;
129             } elsif (substr($line,0,$endpos)=~$match_open_flag1) {
130             my $prefix;
131             $prefix='open'.$1 if ($1 ne "");
132             return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags1, @openflags2;
133             } elsif (substr($line,0,$endpos)=~$match_open_flag2) {
134             my $prefix;
135             if ($3 ne "") {
136             $prefix=$1.uc($2).$3;
137             return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags2;
138             } else {
139             return grep { index($_,uc($word))==0 } @openflags2;
140             }
141             } elsif (substr($line,0,$endpos)=~$match_save_flag1) {
142             if ($1) {
143             my $prefix;
144             $prefix='save'.$1;
145             return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags1, @saveflags2;
146             } else {
147             return grep { index($_,uc($word))==0 } @saveflags1, @saveflags2, XML::XSH::Functions::docs();
148             }
149             } elsif (substr($line,0,$endpos)=~$match_save_flag2) {
150             my $prefix;
151             if ($3 ne "") {
152             $prefix=$1.uc($2).$3;
153             return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags2;
154             } else {
155             return grep { index($_,uc($word))==0 } @saveflags2, XML::XSH::Functions::docs();
156             }
157             } elsif (substr($line,0,$pos)=~$match_command) {
158             return grep { index($_,$word)==0 } @XML::XSH::CompletionList::XSH_COMMANDS;
159             } elsif (substr($line,0,$endpos)=~$match_doc ||
160             substr($line,0,$endpos)=~$match_save_doc) {
161             return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
162             } elsif (substr($line,0,$endpos)=~$match_clone_doc ||
163             substr($line,0,$endpos)=~$match_open_doc) {
164             complete_set_term_char($type,'=');
165             return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
166             } elsif (substr($line,0,$endpos)=~$match_open_filename ||
167             substr($line,0,$endpos)=~$match_save_filename ||
168             substr($line,0,$endpos)=~$match_filename) {
169             my @result=complete_filename($type,$word);
170             if (@result==1 and -d $result[0]) {
171             complete_set_term_char($type,'');
172             } else {
173             complete_set_term_char($type,' ');
174             }
175             return @result;
176             } elsif (substr($line,0,$endpos)=~$match_dir) {
177             my @result=grep -d, complete_filename($type,$word);
178             if (@result==1) {
179             complete_set_term_char($type,' ');
180             } else {
181             complete_set_term_char($type,'');
182             }
183             return @result;
184             } elsif (substr($line,0,$endpos)=~$match_path_filename) {
185             my @result=complete_system_command($type,$word);
186             if (@result==1 and -d $result[0]) {
187             complete_set_term_char($type,'');
188             } else {
189             complete_set_term_char($type,' ');
190             }
191             return @result;
192              
193             } elsif (substr($line,0,$endpos)=~$match_no) {
194             return ();
195             } else {
196             complete_set_term_char($type,'');
197             return xpath_complete($line,$word,$pos);
198             }
199             }
200              
201             sub xpath_complete_str {
202             my $str = reverse($_[0]);
203             my $debug = $_[1];
204             my $result="";
205             my $NAMECHAR = '[-_.[:alnum:]]';
206             my $NNAMECHAR = '[-:_.[:alnum:]]';
207             my $NAME = "${NAMECHAR}*${NNAMECHAR}*[_.[:alpha:]]";
208              
209             my $WILDCARD = '\*(?!\*|${NAME}|\)|\]|\.)';
210             my $OPER = qr/(?:[,=<>\+\|]|-(?!${NAME})|(?:vid|dom|dna|ro)(?=\s*\]|\s*\)|\s*[0-9]+(?!${NNAMECHAR})|\s+{$NAMECHAR}|\s+\*))/;
211              
212             print "'$str'\n" if $debug;
213             my $localmatch;
214              
215             STEP0:
216             if ($str =~ /\G\s*[\]\)]/gsco) {
217             print "No completions after ] or )\n" if $debug;
218             return;
219             }
220              
221             STEP1:
222             if ( $str =~ /\G(${NAMECHAR}+)?(?::(${NAMECHAR}+))?/gsco ) {
223             if ($2 ne "") {
224             $localmatch=reverse($2).":".reverse($1);
225             if ($1 ne "") {
226             $result=reverse($2).':*[starts-with(local-name(),"'.reverse($1).'")]'.$result;
227             } else {
228             $result=reverse($2).':*'.$result;
229             }
230             } else {
231             $localmatch=reverse($1);
232             $result='*[starts-with(name(),"'.$localmatch.'")]'.$result;
233             }
234             } else {
235             $result='*'.$result;
236             }
237             if ($str =~ /\G\@/gsco) {
238             $result="@".$result;
239             }
240              
241             STEP2:
242             print "STEP2-LOCALMATCH: $localmatch\n" if $debug;
243             print "STEP2: $result\n" if $debug;
244             print "STEP2-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
245             while ($str =~ m/\G(::|:|\@|${NAME}\$?|\/\/|\/|${WILDCARD}|\)|\])/gsco) {
246             print "STEP2-MATCH: '$1'\n" if $debug;
247             if ($1 eq ')' or $1 eq ']') {
248             # eat ballanced upto $1
249             my @ballance=(($1 eq ')' ? '(' : '['));
250             $result=$1.$result;
251             print "STEP2: Ballanced $1\n" if $debug;
252             do {
253             $result=reverse($1).$result if $str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
254             return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
255             if ($1 eq $ballance[$#ballance]) {
256             pop @ballance;
257             } elsif ($1 eq ')') {
258             push @ballance, '(';
259             } elsif ($1 eq ']') {
260             push @ballance, '[';
261             } elsif ($1 eq '"') {
262             push @ballance, '"';
263             } elsif ($1 eq "'") {
264             push @ballance, "'";
265             } else {
266             print STDERR "Error 2: lost in an expression on '$1' ";
267             print STDERR reverse(substr($str,pos()))."\n";
268             print "-> $result\n";
269             return undef;
270             }
271             $result=$1.$result;
272             } while (@ballance);
273             } else {
274             $result=reverse($1).$result;
275             }
276             }
277              
278             STEP3:
279             print "STEP3: $result\n" if $debug;
280             print "STEP3-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
281             if (substr($result,0,1) eq '/') {
282             if ($str =~ /\G['"]/gsco) {
283             print STDERR "Error 1: unballanced '$1'\n";
284             return undef;
285             } elsif ($str =~ /\G(?:\s+['"]|\(|\[|${OPER})/gsco) {
286             return ($result,$localmatch);
287             }
288             return ($result,$localmatch); # uncertain!!!
289             } else {
290             return ($result,$localmatch) if ($str=~/\G\s+(?=${OPER})/gsco);
291             }
292              
293             STEP4:
294             print "STEP4: $result\n" if $debug;
295             print "STEP4-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
296             my @ballance;
297             do {
298             $str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
299             print "STEP4-MATCH '".reverse($1)."'\n" if $debug;
300             return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
301             print "STEP4-BALLANCED '$1'\n" if $debug;
302             if (@ballance and $1 eq $ballance[$#ballance]) {
303             pop @ballance;
304             } elsif ($1 eq ')') {
305             push @ballance, '(';
306             } elsif ($1 eq ']') {
307             push @ballance, '[';
308             } elsif ($1 eq '"') {
309             push @ballance, '"';
310             } elsif ($1 eq "'") {
311             push @ballance, "'";
312             } elsif (not(@ballance) and $1 eq '[') {
313             print "STEP4-PRED2STEP '$1'\n" if $debug;
314             $result='/'.$result;
315             goto STEP2;
316             }
317             } while (@ballance);
318             goto STEP4;
319             }
320              
321             sub xpath_complete {
322             my ($line, $word,$pos)=@_;
323             return () unless $XML::XSH::Functions::XPATH_COMPLETION;
324             my $str=XML::XSH::Functions::toUTF8($XML::XSH::Functions::QUERY_ENCODING,
325             substr($line,0,$pos).$word);
326             my ($xp,$local) = xpath_complete_str($str,0);
327             # XML::XSH::Functions::__debug("COMPLETING $_[0] local $local as $xp\n");
328             return () if $xp eq "";
329             my ($docid,$q) = ($xp=~/^(?:([a-zA-Z_][a-zA-Z0-9_]*):(?!:))?((?:.|\n)*)$/);
330             if ($docid ne "" and not XML::XSH::Functions::_doc($docid)) {
331             $q=$docid.":".$q;
332             $docid="";
333             }
334             my ($id,$query,$doc)=XML::XSH::Functions::_xpath([$docid,$q]);
335             return () unless (ref($doc));
336             my $ql= eval { XML::XSH::Functions::find_nodes([$id,$query]) };
337             return () if $@;
338             my %names;
339             @names{ map {
340             XML::XSH::Functions::fromUTF8($XML::XSH::Functions::QUERY_ENCODING,
341             substr(substr($str,0,
342             length($str)
343             -length($local)).
344             $_->nodeName(),$pos))
345             } @$ql}=();
346              
347             my @completions = sort { $a cmp $b } keys %names;
348             # print "completions so far: @completions\n";
349              
350             if (($XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'always' or
351             $XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'when-empty' and !@completions)
352             and $str =~ /[ \n\t\r|([=<>+-\/]([[:alpha:]][-:[:alnum:]]*)?$/ and $1 !~ /::/) {
353             # complete XML axis
354             my ($pre,$axpart)=($word =~ /^(.*[^[:alnum:]])?([[:alpha:]][-[:alnum:]:]*)/);
355             # print "\nWORD: $word\nPRE: $pre\nPART: $axpart\nSTR:$str\n";
356             foreach my $axis (qw(following preceding following-sibling
357             preceding-sibling
358             parent ancestor ancestor-or-self descendant self
359             descendant-or-self child attribute namespace)) {
360             if ($axis =~ /^${axpart}/) {
361             push @completions, "${pre}${axis}::";
362             }
363             }
364             }
365             return @completions;
366             }
367              
368             1;
369