 Issue 29383960:
  Issue 3143 - Filter elements with :-abp-has()  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore
    
  
    Issue 29383960:
  Issue 3143 - Filter elements with :-abp-has()  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore| Index: chrome/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/chrome/content/elemHideEmulation.js | 
| +++ b/chrome/content/elemHideEmulation.js | 
| @@ -1,12 +1,15 @@ | 
| // 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 propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/; | 
| +var abpSelectorRegExp = /\[-abp-selector=(["'])(.+)\1\]/; | 
| +var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; | 
| +var pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/; | 
| function splitSelector(selector) | 
| { | 
| if (selector.indexOf(",") == -1) | 
| return [selector]; | 
| var selectors = []; | 
| var start = 0; | 
| @@ -36,132 +39,332 @@ | 
| } | 
| } | 
| } | 
| selectors.push(selector.substring(start)); | 
| return selectors; | 
| } | 
| -function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 
| +const ABP_ATTR = "abp-0ac791f0-a03b-4f2c-a935-a285bc2e668e"; | 
| +const ABP_ATTR_SEL = "[" + ABP_ATTR + "] "; | 
| + | 
| +function matchChildren(e, selector) | 
| +{ | 
| + var subElement; | 
| + var newSelector = ABP_ATTR_SEL + selector; | 
| + var parentNode = e.parentNode || document; | 
| + | 
| + e.setAttribute(ABP_ATTR, ""); | 
| + subElement = parentNode.querySelector(newSelector); | 
| + e.removeAttribute(ABP_ATTR); | 
| + | 
| + return subElement != null; | 
| +} | 
| + | 
| +function selectChildren(e, selector) | 
| +{ | 
| + var subElements; | 
| + var newSelector = ABP_ATTR_SEL + selector; | 
| + var parentNode = e.parentNode || document; | 
| + | 
| + e.setAttribute(ABP_ATTR, ""); | 
| + subElements = parentNode.querySelectorAll(newSelector); | 
| + e.removeAttribute(ABP_ATTR); | 
| + | 
| + return subElements; | 
| +} | 
| + | 
| +/** Unwrap the pattern out of a [-abp-selector=''] if necessary | 
| + * @param {object} pattern - The pattern object to unwrap | 
| + * @return {object} the parsed pattern object or undefined if nothing parse. | 
| + */ | 
| +function unwrapPattern(pattern) | 
| +{ | 
| + var match = abpSelectorRegExp.exec(pattern.selector); | 
| + if (match) | 
| + { | 
| + var prefix = pattern.selector.substr(0, match.index).trim(); | 
| + var suffix = pattern.selector.substr(match.index + match[0].length).trim(); | 
| + var selector = prefix + match[2] + suffix; | 
| + return parsePattern({selector: selector}); | 
| + } | 
| + | 
| + return parsePattern(pattern); | 
| +} | 
| + | 
| +function parsePattern(pattern) | 
| +{ | 
| + var selector = pattern.selector; | 
| + | 
| + var match = pseudoClassHasSelectorRegExp.exec(selector); | 
| 
Felix Dahlke
2017/04/10 10:55:12
How I understand the code, :has and :-abp-properti
 
hub
2017/04/10 12:29:27
My question is do we still support [-abp-propertie
 
Felix Dahlke
2017/04/10 12:35:41
Lonely :-abp-properties (pseudo class syntax!) sho
 
hub
2017/04/10 12:41:02
Ah right. The pseudo class syntax. Good catch.
 | 
| + if (match) | 
| + return { | 
| + type: "has", | 
| + text: pattern.text, | 
| + elementMatcher: new PseudoHasMatcher(match[1]), | 
| + prefix: selector.substr(0, match.index).trim(), | 
| + suffix: selector.substr(match.index + match[0].length).trim() | 
| + }; | 
| + | 
| + match = pseudoClassPropsSelectorRegExp.exec(selector); | 
| + if (!match) | 
| + match = propertySelectorRegExp.exec(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: selector.substr(0, match.index), | 
| + suffix: 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); | 
| + } | 
| + } | 
| +} | 
| + | 
| +/** | 
| + * Match the selector @pattern containing :has() starting from @node. | 
| + * @param {string} pattern - the pattern to match | 
| + * @param {object} node - the top level node. | 
| + */ | 
| +function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) | 
| +{ | 
| + // select element for the prefix pattern. Or just use node. | 
| + var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ]; | 
| + for (var j = 0; j < haveEl.length; j++) | 
| + { | 
| + var matched = pattern.elementMatcher.match(haveEl[j], stylesheets); | 
| + if (!matched) | 
| + continue; | 
| + | 
| + if (pattern.suffix) | 
| + { | 
| + var subElements = selectChildren(haveEl[j], pattern.suffix); | 
| + if (subElements) | 
| + { | 
| + for (var k = 0; k < subElements.length; k++) | 
| + { | 
| + elements.push(subElements[k]); | 
| + filters.push(pattern.text); | 
| + } | 
| + } | 
| + } | 
| + else | 
| + { | 
| + elements.push(haveEl[j]); | 
| + filters.push(pattern.text); | 
| + } | 
| + } | 
| +} | 
| + | 
| +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}); | 
| +} | 
| + | 
| +PseudoHasMatcher.prototype = { | 
| + match: function(elem, stylesheets) | 
| + { | 
| + var selectors = []; | 
| + | 
| + if (this.parsed) | 
| + { | 
| + var filters = []; // don't need this, we have a partial filter. | 
| + if (this.parsed.type == "has") | 
| + { | 
| + var child = elem.firstChild; | 
| + while (child) | 
| + { | 
| + if (child.nodeType === Node.ELEMENT_NODE) | 
| + { | 
| + var matches = []; | 
| + pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filters); | 
| + if (matches.length > 0) | 
| + return true; | 
| + } | 
| + child = child.nextSibling; | 
| + } | 
| + return false; | 
| + } | 
| + if (this.parsed.type == "props") | 
| + { | 
| + for (var i = 0; i < stylesheets.length; i++) | 
| + findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); | 
| + } | 
| + } | 
| + else | 
| + { | 
| + selectors = [this.hasSelector]; | 
| + } | 
| + | 
| + var matched = false; | 
| + // look up for all elements that match the :has(). | 
| + for (var k = 0; k < selectors.length; k++) | 
| + { | 
| + try | 
| + { | 
| + matched = matchChildren(elem, selectors[k]); | 
| + if (matched) | 
| + break; | 
| + } | 
| + catch (e) | 
| + { | 
| + console.error("Exception with querySelector()", selectors[k], e); | 
| + } | 
| + } | 
| + return matched; | 
| + } | 
| +}; | 
| + | 
| +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(node, 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 style = this.stringifyStyle(rule.style); | 
| - for (var j = 0; j < this.patterns.length; j++) | 
| - { | 
| - var pattern = this.patterns[j]; | 
| - if (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); | 
| - } | 
| - } | 
| - } | 
| + pseudoClassHasMatch(this.pseudoHasPatterns[i], node, stylesheets, elements, filters); | 
| } | 
| }, | 
| 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(document, 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 = unwrapPattern(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)); | 
| } | 
| }; |