| Index: chrome/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/chrome/content/elemHideEmulation.js | 
| +++ b/chrome/content/elemHideEmulation.js | 
| @@ -1,12 +1,13 @@ | 
| // We are currently limited to ECMAScript 5 in this file, because it is being | 
| // used in the browser tests. See https://issues.adblockplus.org/ticket/4796 | 
|  | 
| var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; | 
| +var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; | 
|  | 
| function splitSelector(selector) | 
| { | 
| if (selector.indexOf(",") == -1) | 
| return [selector]; | 
|  | 
| var selectors = []; | 
| var start = 0; | 
| @@ -36,16 +37,41 @@ function splitSelector(selector) | 
| } | 
| } | 
| } | 
|  | 
| selectors.push(selector.substring(start)); | 
| return selectors; | 
| } | 
|  | 
| +// matcher for the pseudo CSS4 class :has | 
| +// For those browser that don't have it yet. | 
| +function PseudoHasMatcher(selector) | 
| +{ | 
| +  this.hasSelector = selector; | 
| +} | 
| + | 
| +PseudoHasMatcher.prototype = { | 
| +  match: function(elem) | 
| +  { | 
| +    var matches = []; | 
| +    // look up for all elements that match the :has(). | 
| +    var children = elem.children; | 
| +    for (var i = 0; i < children.length; i++) | 
| +    { | 
| +      var hasElem = elem.querySelector(this.hasSelector); | 
| +      if (hasElem != null) | 
| +      { | 
| +        matches.push(hasElem); | 
| +      } | 
| +    } | 
| +    return matches; | 
| +  } | 
| +}; | 
| + | 
| function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 
| { | 
| this.window = window; | 
| this.getFiltersFunc = getFiltersFunc; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| } | 
|  | 
| ElemHideEmulation.prototype = { | 
| @@ -92,28 +118,73 @@ ElemHideEmulation.prototype = { | 
| var rule = rules[i]; | 
| if (rule.type != rule.STYLE_RULE) | 
| continue; | 
|  | 
| var style = this.stringifyStyle(rule.style); | 
| for (var j = 0; j < this.patterns.length; j++) | 
| { | 
| var pattern = this.patterns[j]; | 
| -        if (pattern.regexp.test(style)) | 
| +        if (pattern.type == "prop" && pattern.regexp.test(style)) | 
| { | 
| var subSelectors = splitSelector(rule.selectorText); | 
| for (var k = 0; k < subSelectors.length; k++) | 
| { | 
| var subSelector = subSelectors[k]; | 
| selectors.push(pattern.prefix + subSelector + pattern.suffix); | 
| filters.push(pattern.text); | 
| } | 
| } | 
| } | 
| } | 
| +    this.findPseudoClassHasSelectors(selectors, filters); | 
| +  }, | 
| + | 
| +  findPseudoClassHasSelectors: function(selectors, filters) | 
| +  { | 
| +    // XXX shall we split the patterns? | 
| +    for (var i = 0; i < this.patterns.length; i++) | 
| +    { | 
| +      var pattern = this.patterns[i]; | 
| +      if (pattern.type != ":has") | 
| +        continue; | 
| + | 
| +      var prefixes = document.querySelectorAll(pattern.prefix); | 
| +      for (var j = 0; j < prefixes.length; j++) | 
| +      { | 
| +        var matched = pattern.elementMatcher.match(prefixes[j]); | 
| +        if (matched.length == 0) | 
| +        { | 
| +          continue; | 
| +        } | 
| +        // XXX make sure we don't have performance problems here | 
| +        matched.forEach(function(e) | 
| +        { | 
| +          var subSelector = e.id; | 
| +          if (!subSelector) | 
| +          { | 
| +            var findUniqueId = function() | 
| +            { | 
| +              var id = "elemHideEmulationHide-" + | 
| +                  Math.floor(Math.random() * 10000); | 
| +              if (!document.getElementById(id)) | 
| +                return id; | 
| +              return findUniqueId(); | 
| +            }; | 
| +            subSelector = findUniqueId(); | 
| +            e.id = subSelector; | 
| +          } | 
| +          var newSelector = pattern.prefix ? pattern.prefix + "> " : ""; | 
| +          newSelector += "#" + subSelector; | 
| +          newSelector += pattern.suffix ? " > " + pattern.suffix : ""; | 
| +          selectors.push(newSelector); | 
| +          filters.push(pattern.text); | 
| +        }); | 
| +      } | 
| +    } | 
| }, | 
|  | 
| addSelectors: function(stylesheets) | 
| { | 
| var selectors = []; | 
| var filters = []; | 
| for (var i = 0; i < stylesheets.length; i++) | 
| this.findSelectors(stylesheets[i], selectors, filters); | 
| @@ -129,33 +200,50 @@ ElemHideEmulation.prototype = { | 
|  | 
| apply: function() | 
| { | 
| this.getFiltersFunc(function(patterns) | 
| { | 
| this.patterns = []; | 
| for (var i = 0; i < patterns.length; i++) | 
| { | 
| +        var regexpMatcher; | 
| +        var elementMatcher; | 
| var pattern = patterns[i]; | 
| var match = propertySelectorRegExp.exec(pattern.selector); | 
| -        if (!match) | 
| -          continue; | 
| +        var type; | 
| +        if (match) | 
| +        { | 
| +          var regexpString; | 
| +          var propertyExpression = match[2]; | 
| +          if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 
| +              propertyExpression[propertyExpression.length - 1] == "/") | 
| +            regexpString = propertyExpression.slice(1, -1) | 
| +                .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 
| +          else | 
| +            regexpString = filterToRegExp(propertyExpression); | 
| +          regexpMatcher = new RegExp(regexpString, "i"); | 
| +          type = "prop"; | 
| +        } | 
| +        else | 
| +        { | 
| +          match = pseudoClassHasSelectorRegExp.exec(pattern.selector); | 
| +          if (!match) | 
| +            continue; | 
|  | 
| -        var propertyExpression = match[2]; | 
| -        var regexpString; | 
| -        if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 
| -            propertyExpression[propertyExpression.length - 1] == "/") | 
| -          regexpString = propertyExpression.slice(1, -1) | 
| -              .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 
| -        else | 
| -          regexpString = filterToRegExp(propertyExpression); | 
| +          var pseudoHasSelector = match[1]; | 
| +          elementMatcher = new PseudoHasMatcher(pseudoHasSelector); | 
| +          type = ":has"; | 
| +        } | 
|  | 
| this.patterns.push({ | 
| +          type: type, | 
| text: pattern.text, | 
| -          regexp: new RegExp(regexpString, "i"), | 
| +          regexp: regexpMatcher, | 
| +          elementMatcher: elementMatcher, | 
| prefix: pattern.selector.substr(0, match.index), | 
| suffix: pattern.selector.substr(match.index + match[0].length) | 
| }); | 
| } | 
|  | 
| if (this.patterns.length > 0) | 
| { | 
| var document = this.window.document; | 
|  |