| Index: chrome/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/chrome/content/elemHideEmulation.js | 
| +++ b/chrome/content/elemHideEmulation.js | 
| @@ -1,12 +1,51 @@ | 
| // 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\((.*)\)/; | 
| + | 
| +// polyfill. We should deal with this better, but PhantomJS doesn't | 
| +// have matches. At least not in the version we use. | 
| +// Chrome 34, Firefox 34, Opera 21 and Safari 7.1 do have it. | 
| +if (!Element.prototype.matches) { | 
| +  Element.prototype.matches = | 
| +    Element.prototype.matchesSelector || | 
| +    Element.prototype.mozMatchesSelector || | 
| +    Element.prototype.msMatchesSelector || | 
| +    Element.prototype.oMatchesSelector || | 
| +    Element.prototype.webkitMatchesSelector | 
| +} | 
| + | 
| +// return the index were the simple-selector ends | 
| +function findFirstSelector(selector) | 
| +{ | 
| +  var sepIndex = selector.indexOf(' '); | 
| +  var nextIndex = selector.indexOf('>'); | 
| +  if (nextIndex != -1) | 
| +    sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | 
| +  nextIndex = selector.indexOf('+'); | 
| +  if (nextIndex != -1) | 
| +    sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | 
| +  nextIndex = selector.indexOf('~'); | 
| +  if (nextIndex != -1) | 
| +    sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | 
| +  return sepIndex; | 
| +} | 
| + | 
| +function extractFirstSelector(selector) | 
| +{ | 
| +  var sepIndex = findFirstSelector(selector); | 
| + | 
| +  if (sepIndex == -1) | 
| +    return selector; | 
| + | 
| +  return selector.substr(0, sepIndex); | 
| +} | 
|  | 
| function splitSelector(selector) | 
| { | 
| if (selector.indexOf(",") == -1) | 
| return [selector]; | 
|  | 
| var selectors = []; | 
| var start = 0; | 
| @@ -36,132 +75,326 @@ function splitSelector(selector) | 
| } | 
| } | 
| } | 
|  | 
| selectors.push(selector.substring(start)); | 
| return selectors; | 
| } | 
|  | 
| -function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 
| +function selectChildren(e, selector) | 
| +{ | 
| +  var sel = selector; | 
| +  // XXX we should have a more elegant way | 
| +  // also startsWith isn't available in PhantomJS. | 
| +  var combinator = sel.substr(0, 1); | 
| +  var subElements; | 
| +  var nextEl = e; | 
| +  sel = sel.substr(1).trim(); | 
| +  switch (combinator) | 
| +  { | 
| +  case ">": | 
| +    subElements = e.querySelectorAll(sel); | 
| +    break; | 
| + | 
| +  case "+": | 
| +    do | 
| +    { | 
| +      nextEl = nextEl.nextSibling; | 
| +    } | 
| +    while (nextEl && nextEl.nodeType != 1); | 
| + | 
| +    var siblingSel = extractFirstSelector(sel); | 
| +    var idx = findFirstSelector(sel); | 
| +    var childSel = idx != -1 ? sel.substr(idx + 1).trim() : ""; | 
| + | 
| +    if (nextEl && nextEl.matches(siblingSel)) | 
| +    { | 
| +      if (childSel != "") | 
| +        subElements = selectChildren(nextEl, childSel); | 
| +      else | 
| +        subElements = [ nextEl ]; | 
| +    } | 
| +    break; | 
| + | 
| +  case "~": | 
| +    do | 
| +    { | 
| +      nextEl = nextEl.nextSibling; | 
| +      if (nextEl && nextEl.nodeType == 1 && nextEl.matches(sel)) | 
| +      { | 
| +        subElements = nextEl.querySelectorAll(sel); | 
| +        break; | 
| +      } | 
| +    } | 
| +    while (nextEl); | 
| + | 
| +    break; | 
| +  } | 
| +  return subElements; | 
| +} | 
| + | 
| +function parsePattern(pattern) | 
| +{ | 
| +  // we should catch the :has() pseudo class first. | 
| +  var match = pseudoClassHasSelectorRegExp.exec(pattern.selector); | 
| +  if (match) | 
| +  { | 
| +    return { | 
| +      type: "has", | 
| +      text: pattern.text, | 
| +      elementMatcher: new PseudoHasMatcher(match[1]), | 
| +      prefix: pattern.selector.substr(0, match.index).trim(), | 
| +      suffix: pattern.selector.substr(match.index + match[0].length).trim() | 
| +    }; | 
| +  } | 
| + | 
| +  match = propertySelectorRegExp.exec(pattern.selector); | 
| +  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); | 
| +    return { | 
| +      type: "props", | 
| +      text: pattern.text, | 
| +      regexp: new RegExp(regexpString, "i"), | 
| +      prefix: pattern.selector.substr(0, match.index), | 
| +      suffix: pattern.selector.substr(match.index + match[0].length) | 
| +    }; | 
| +  } | 
| +} | 
| + | 
| +function matchStyleProps(style, rule, pattern, selectors, filters) | 
| +{ | 
| +  if (pattern.regexp.test(style)) | 
| +  { | 
| +    var subSelectors = splitSelector(rule.selectorText); | 
| +    for (var i = 0; i < subSelectors.length; i++) | 
| +    { | 
| +      var subSelector = subSelectors[i]; | 
| +      selectors.push(pattern.prefix + subSelector + pattern.suffix); | 
| +      filters.push(pattern.text); | 
| +    } | 
| +  } | 
| +} | 
| + | 
| +function findPropsSelectors(stylesheet, patterns, selectors, filters) | 
| +{ | 
| +  var rules = stylesheet.cssRules; | 
| +  if (!rules) | 
| +    return; | 
| + | 
| +  for (var i = 0; i < rules.length; i++) | 
| +  { | 
| +    var rule = rules[i]; | 
| +    if (rule.type != rule.STYLE_RULE) | 
| +      continue; | 
| + | 
| +    var style = stringifyStyle(rule.style); | 
| +    for (var j = 0; j < patterns.length; j++) | 
| +    { | 
| +      matchStyleProps(style, rule, patterns[j], selectors, filters); | 
| +    } | 
| +  } | 
| +} | 
| + | 
| +function stringifyStyle(style) | 
| +{ | 
| +  var styles = []; | 
| +  for (var i = 0; i < style.length; i++) | 
| +  { | 
| +    var property = style.item(i); | 
| +    var value    = style.getPropertyValue(property); | 
| +    var priority = style.getPropertyPriority(property); | 
| +    styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";"); | 
| +  } | 
| +  styles.sort(); | 
| +  return styles.join(" "); | 
| +} | 
| + | 
| +// matcher for the pseudo CSS4 class :has | 
| +// For those browser that don't have it yet. | 
| +function PseudoHasMatcher(selector) | 
| +{ | 
| +  this.hasSelector = selector; | 
| +  this.parsed = parsePattern({ selector: this.hasSelector }); | 
| +  if (this.parsed && this.parsed.type == "has") | 
| +  { | 
| +    console.log("unsupported :has() pattern", this.hasSelector); | 
| +  } | 
| +} | 
| + | 
| +PseudoHasMatcher.prototype = { | 
| +  match: function(elem, stylesheets, firstOnly) | 
| +  { | 
| +    var matches = []; | 
| +    var selectors = []; | 
| + | 
| +    if (this.parsed) | 
| +    { | 
| +      if (this.parsed.type == "has") | 
| +        return []; | 
| +      if (this.parsed.type == "props") | 
| +      { | 
| +        var filters = []; // don't need this | 
| +        findPropsSelectors(stylesheets[0], [this.parsed], selectors, filters); | 
| +      } | 
| +    } | 
| +    else | 
| +    { | 
| +      selectors = [this.hasSelector]; | 
| +    } | 
| + | 
| +    // look up for all elements that match the :has(). | 
| +    var children = elem.children; | 
| +    for (var i = 0; i < children.length; i++) | 
| +    { | 
| +      for (var k = 0; k < selectors.length; k++) | 
| +      { | 
| +        try | 
| +        { | 
| +          var hasElem = elem.querySelector(selectors[k]); | 
| +          if (hasElem) | 
| +          { | 
| +            matches.push(hasElem); | 
| +            if (firstOnly) | 
| +              break; | 
| +          } | 
| +        } | 
| +        catch(e) | 
| +        { | 
| +          console.log("Exception with querySelector()", selectors[k]); | 
| +        } | 
| +      } | 
| +    } | 
| +    return matches; | 
| +  } | 
| +}; | 
| + | 
| +function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElementsFunc) | 
| { | 
| this.window = window; | 
| this.getFiltersFunc = getFiltersFunc; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| +  this.hideElementsFunc = hideElementsFunc; | 
| } | 
|  | 
| ElemHideEmulation.prototype = { | 
| -  stringifyStyle: function(style) | 
| -  { | 
| -    var styles = []; | 
| -    for (var i = 0; i < style.length; i++) | 
| -    { | 
| -      var property = style.item(i); | 
| -      var value    = style.getPropertyValue(property); | 
| -      var priority = style.getPropertyPriority(property); | 
| -      styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";"); | 
| -    } | 
| -    styles.sort(); | 
| -    return styles.join(" "); | 
| -  }, | 
|  | 
| isSameOrigin: function(stylesheet) | 
| { | 
| try | 
| { | 
| return new URL(stylesheet.href).origin == this.window.location.origin; | 
| } | 
| catch (e) | 
| { | 
| // Invalid URL, assume that it is first-party. | 
| return true; | 
| } | 
| }, | 
|  | 
| -  findSelectors: function(stylesheet, selectors, filters) | 
| +  findPseudoClassHasElements: function(stylesheets, elements, filters) | 
| { | 
| -    // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| -    // between Firefox and Chrome. | 
| -    if (!this.isSameOrigin(stylesheet)) | 
| -      return; | 
| - | 
| -    var rules = stylesheet.cssRules; | 
| -    if (!rules) | 
| -      return; | 
| - | 
| -    for (var i = 0; i < rules.length; i++) | 
| +    for (var i = 0; i < this.pseudoHasPatterns.length; i++) | 
| { | 
| -      var rule = rules[i]; | 
| -      if (rule.type != rule.STYLE_RULE) | 
| -        continue; | 
| +      var pattern = this.pseudoHasPatterns[i]; | 
|  | 
| -      var style = this.stringifyStyle(rule.style); | 
| -      for (var j = 0; j < this.patterns.length; j++) | 
| +      var haveEl = document.querySelectorAll(pattern.prefix); | 
| +      for (var j = 0; j < haveEl.length; j++) | 
| { | 
| -        var pattern = this.patterns[j]; | 
| -        if (pattern.regexp.test(style)) | 
| +        var matched = pattern.elementMatcher.match(haveEl[j], stylesheets, !pattern.suffix); | 
| +        if (matched.length == 0) | 
| +          continue; | 
| + | 
| +        if (pattern.suffix) | 
| { | 
| -          var subSelectors = splitSelector(rule.selectorText); | 
| -          for (var k = 0; k < subSelectors.length; k++) | 
| +          matched.forEach(function(e) | 
| { | 
| -            var subSelector = subSelectors[k]; | 
| -            selectors.push(pattern.prefix + subSelector + pattern.suffix); | 
| -            filters.push(pattern.text); | 
| -          } | 
| +            var subElements = selectChildren(e, pattern.suffix); | 
| +            if (subElements) | 
| +            { | 
| +              for (var k = 0; k < subElements.length; k++) | 
| +              { | 
| +                elements.push(subElements[i]); | 
| +                filters.push(pattern.text); | 
| +              } | 
| +            } | 
| +          }); | 
| +        } | 
| +        else | 
| +        { | 
| +          elements.push(haveEl[j]); | 
| +          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); | 
| +    { | 
| +      // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| +      // between Firefox and Chrome. | 
| +      if (!this.isSameOrigin(stylesheets[i])) | 
| +        continue; | 
| +      findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filters); | 
| +    } | 
| this.addSelectorsFunc(selectors, filters); | 
| }, | 
|  | 
| +  hideElements: function(stylesheets) | 
| +  { | 
| +    var elements = []; | 
| +    var filters = []; | 
| +    this.findPseudoClassHasElements(stylesheets, elements, filters); | 
| +    this.hideElementsFunc(elements, filters); | 
| +  }, | 
| + | 
| onLoad: function(event) | 
| { | 
| var stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.addSelectors([stylesheet]); | 
| +    this.hideElements([stylesheet]); | 
| }, | 
|  | 
| apply: function() | 
| { | 
| this.getFiltersFunc(function(patterns) | 
| { | 
| -      this.patterns = []; | 
| +      this.propSelPatterns = []; | 
| +      this.pseudoHasPatterns = []; | 
| for (var i = 0; i < patterns.length; i++) | 
| { | 
| var pattern = patterns[i]; | 
| -        var match = propertySelectorRegExp.exec(pattern.selector); | 
| -        if (!match) | 
| +        var parsed = parsePattern(pattern); | 
| +        if (parsed == undefined) | 
| 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); | 
| - | 
| -        this.patterns.push({ | 
| -          text: pattern.text, | 
| -          regexp: new RegExp(regexpString, "i"), | 
| -          prefix: pattern.selector.substr(0, match.index), | 
| -          suffix: pattern.selector.substr(match.index + match[0].length) | 
| -        }); | 
| +        if (parsed.type == "props") | 
| +        { | 
| +          this.propSelPatterns.push(parsed); | 
| +        } | 
| +        else if (parsed.type == "has") | 
| +        { | 
| +          this.pseudoHasPatterns.push(parsed); | 
| +        } | 
| } | 
|  | 
| -      if (this.patterns.length > 0) | 
| +      if (this.pseudoHasPatterns.length > 0 || this.propSelPatterns.length > 0) | 
| { | 
| var document = this.window.document; | 
| this.addSelectors(document.styleSheets); | 
| +        this.hideElements(document.styleSheets); | 
| document.addEventListener("load", this.onLoad.bind(this), true); | 
| } | 
| }.bind(this)); | 
| } | 
| }; | 
|  |