Catch unmatched "[" in selector parser with a helpful error
[catagits/HTML-Zoom.git] / lib / HTML / Zoom / SelectorParser.pm
index 32688fc..9bc7bc3 100644 (file)
@@ -7,6 +7,8 @@ use Carp qw(confess);
 
 my $sel_char = '-\w_';
 my $sel_re = qr/([$sel_char]+)/;
+my $match_value_re = qr/"?$sel_re"?/;
+
 
 sub new { bless({}, shift) }
 
@@ -18,75 +20,6 @@ sub _raw_parse_simple_selector {
     /\G\*/gc and
       return sub { 1 };
 
-     # 'el[attr~="foo"]
-
-    /\G$sel_re\[$sel_re~="$sel_re"\]/gc and
-      return do {
-        my $name = $1;
-        my $attr = $2;
-        my $val = $3;
-        sub {
-          if (
-            $_[0]->{name} && $_[0]->{name} eq $name and
-            $_[0]->{attrs}{$attr}
-          ) {
-            my %vals = map { $_ => 1 } split /\s+/, $_[0]->{attrs}{$attr};
-            return $vals{$val}
-          }
-          return undef
-        }
-      };
-
-     # 'el[attr^="foo"]
-
-    /\G$sel_re\[$sel_re\^="$sel_re"\]/gc and
-      return do {
-        my $name = $1;
-        my $attr = $2;
-        my $val = $3;
-        sub {
-           $_[0]->{name} && $_[0]->{name} eq $name and
-           $_[0]->{attrs}{$attr} && $_[0]->{attrs}{$attr} =~ /^\Q$val\E/
-        }
-      };
-
-     # 'el[attr$="foo"]
-
-    /\G$sel_re\[$sel_re\$="$sel_re"\]/gc and
-      return do {
-        my $name = $1;
-        my $attr = $2;
-        my $val = $3;
-        sub {
-           $_[0]->{name} && $_[0]->{name} eq $name and
-           $_[0]->{attrs}{$attr} && $_[0]->{attrs}{$attr} =~ /\Q$val\E$/
-        }
-      };
-
-     # 'el[attr="foo"]
-
-    /\G$sel_re\[$sel_re="$sel_re"\]/gc and
-      return do {
-        my $name = $1;
-        my $attr = $2;
-        my $val = $3;
-        sub {
-           $_[0]->{name} && $_[0]->{name} eq $name and
-           $_[0]->{attrs}{$attr} && $_[0]->{attrs}{$attr} eq $val
-        }
-      };
-
-     # 'el[attr]
-
-    /\G$sel_re\[$sel_re\]/gc and
-      return do {
-        my $name = $1;
-        my $attr = $2;
-        sub {
-           $_[0]->{name} && $_[0]->{name} eq $name && $_[0]->{attrs}{$attr}
-        }
-      };
-
     # 'element' - match on tag name
 
     /\G$sel_re/gc and
@@ -115,31 +48,63 @@ sub _raw_parse_simple_selector {
         }
       };
 
-    # 'el.class1' - element + class
+    # '[attr^=foo]' - match attribute with ^ anchored regex
+    /\G\[$sel_re\^=$match_value_re\]/gc and
+      return do {
+        my $attribute = $1;
+        my $value = $2;
+        sub {
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E/;
+        }
+      };
 
-    /\G$sel_re\.$sel_re/gc and
+    # '[attr$=foo]' - match attribute with $ anchored regex
+    /\G\[$sel_re\$=$match_value_re\]/gc and
       return do {
-        my $cls = $1;
-        my $name = $2;
+        my $attribute = $1;
+        my $value = $2;
         sub {
-           $_[0]->{name} && $_[0]->{name} eq $name and
-           $_[0]->{attrs}{class} && $_[0]->{attrs}{class} eq $cls
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E$/;
         }
       };
 
-    # 'el#id' - element + id
+    # '[attr*=foo] - match attribute with regex:
+    /\G\[$sel_re\*=$match_value_re\]/gc and
+      return do {
+        my $attribute = $1;
+        my $value = $2;
+        sub {
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E/;
+        }
+      };
 
-    /\G$sel_re#$sel_re/gc and
+    # '[attr=bar]' - match attributes
+    /\G\[$sel_re=$match_value_re\]/gc and
       return do {
-        my $id = $1;
-        my $name = $2;
+        my $attribute = $1;
+        my $value = $2;
         sub {
-           $_[0]->{name} && $_[0]->{name} eq $name and
-           $_[0]->{attrs}{id} && $_[0]->{attrs}{id} eq $id
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} eq $value;
         }
       };
 
-    confess "Couldn't parse $_ as starting with simple selector";
+    # '[attr] - match attribute being present:
+    /\G\[$sel_re\]/gc and
+      return do {
+        my $attribute = $1;
+        sub {
+          exists $_[0]->{attrs}{$attribute};
+        }
+    };
+    
+    # none of the above matched, try catching some obvious errors:
+
+    # indicate unmatched square bracket:
+    /\G\[[^\]]*/gc and $_[0]->_blam('Unmatched [');
   }
 }
 
@@ -151,10 +116,40 @@ sub parse_selector {
   for ($sel) {
     my @sub;
     PARSE: { do {
-      push(@sub, $self->_raw_parse_simple_selector($_));
-      last PARSE if (pos == length);
-      /\G\s*,\s*/gc or confess "Selectors not comma separated";
-    } until (pos == length) };
+
+      my @this_chain;
+
+      # slurp selectors until we find something else:
+      while( my $sel = $self->_raw_parse_simple_selector($_) ){
+        push @this_chain, $sel;
+      }
+
+      if( @this_chain == 1 )
+      {
+        push @sub, @this_chain;
+      }
+      else{
+        # make a compound match closure of everything
+        # in this chain of selectors:
+        push @sub, sub{
+          my $r;
+          for my $inner ( @this_chain ){
+            if( ! ($r = $inner->( @_ )) ){
+              return $r;
+            }
+          }
+          return $r;
+        }
+      }
+
+      # now we're at the end or a delimiter:
+      last PARSE if( pos == length );
+      /\G\s*,\s*/gc or do {
+        /\G(.*)/;
+        $self->_blam( "Selectors not comma separated." );
+      }
+
+     } until (pos == length) };
     return $sub[0] if (@sub == 1);
     return sub {
       foreach my $inner (@sub) {
@@ -165,4 +160,12 @@ sub parse_selector {
 }
 
 
+sub _blam {
+  my ($self, $error) = @_;
+  my $hat = (' ' x (pos||0)).'^';
+  die "Error parsing dispatch specification: ${error}\n
+${_}
+${hat} here\n";
+}
+
 1;