 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 | 
| @@ -54,172 +54,372 @@ | 
| } | 
| } | 
| } | 
| selectors.push(selector.substring(start)); | 
| return selectors; | 
| } | 
| -function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 
| +/** Return position of node from parent. | 
| + * @param {Node} node the node to find the position of. | 
| + * @return {number} One-based index like for :nth-child(), or 0 on error. | 
| + */ | 
| +function positionInParent(node) | 
| +{ | 
| + let {children} = node.parentNode; | 
| + for (let i = 0; i < children.length; i++) | 
| + if (children[i] == node) | 
| + return i + 1; | 
| + return 0; | 
| +} | 
| + | 
| +function makeSelector(node, selector) | 
| +{ | 
| + if (!node.parentElement) | 
| + { | 
| + let newSelector = ":root"; | 
| + if (selector) | 
| + newSelector += " > " + selector; | 
| + return newSelector; | 
| + } | 
| + let idx = positionInParent(node); | 
| + if (idx > 0) | 
| + { | 
| + let newSelector = `${node.tagName}:nth-child(${idx})`; | 
| + if (selector) | 
| + newSelector += " > " + selector; | 
| + return makeSelector(node.parentElement, newSelector); | 
| + } | 
| + | 
| + return selector; | 
| +} | 
| + | 
| +function parseSelectorContent(content, startIndex) | 
| +{ | 
| + let parens = 1; | 
| + let quote = null; | 
| + let i = startIndex; | 
| + for (; i < content.length; i++) | 
| + { | 
| + let c = content[i]; | 
| + if (c == "\\") | 
| + { | 
| + // Ignore escaped characters | 
| + i++; | 
| + } | 
| + else if (quote) | 
| + { | 
| + if (c == quote) | 
| + quote = null; | 
| + } | 
| + else if (c == "'" || c == '"') | 
| + quote = c; | 
| + else if (c == "(") | 
| + parens++; | 
| + else if (c == ")") | 
| + { | 
| + parens--; | 
| + if (parens == 0) | 
| + break; | 
| + } | 
| + } | 
| + | 
| + if (parens > 0) | 
| + return null; | 
| + return {text: content.substring(startIndex, i), end: i}; | 
| +} | 
| + | 
| +/** Parse the selector | 
| + * @param {string} selector the selector to parse | 
| + * @return {Object} selectors is an array of objects, | 
| + * or null in case of errors. hide is true if we'll hide | 
| + * elements instead of styles.. | 
| + */ | 
| +function parseSelector(selector) | 
| +{ | 
| + if (selector.length == 0) | 
| + return []; | 
| + | 
| + let match = abpSelectorRegexp.exec(selector); | 
| + if (!match) | 
| + return [new PlainSelector(selector)]; | 
| + | 
| + let selectors = []; | 
| + if (match.index > 0) | 
| + selectors.push(new PlainSelector(selector.substr(0, match.index))); | 
| + | 
| + let startIndex = match.index + match[0].length; | 
| + let content = parseSelectorContent(selector, startIndex); | 
| + if (content == null) | 
| 
Sebastian Noack
2017/06/08 08:28:47
Nit: Given that parseSelectorContent() either retu
 
hub
2017/06/08 15:45:47
Done.
 | 
| + { | 
| + console.error(new SyntaxError("Failed parsing AdBlock Plus " + | 
| 
Sebastian Noack
2017/06/08 08:28:48
Nit: It's Adblock Plus (with lower case "B"). But
 
hub
2017/06/08 15:45:48
Maybe we could change this to "Failed parsing cont
 
Felix Dahlke
2017/06/09 09:02:33
Well, we're actually parsing what we call an "ABP
 | 
| + `selector ${selector}, didn't ` + | 
| + "find closing parenthesis.")); | 
| + return null; | 
| + } | 
| + if (match[1] == "properties") | 
| + selectors.push(new PropsSelector(content.text)); | 
| + else if (match[1] == "has") | 
| + { | 
| + let hasSelector = new HasSelector(content.text); | 
| + if (!hasSelector.valid()) | 
| + return null; | 
| + selectors.push(hasSelector); | 
| + } | 
| + else | 
| + { | 
| + // this is an error, can't parse selector. | 
| + console.error(new SyntaxError("Failed parsing AdBlock Plus " + | 
| + `selector ${selector}, invalid ` + | 
| + `pseudo-class -abp-${match[1]}().`)); | 
| + return null; | 
| + } | 
| + | 
| + let suffix = parseSelector(selector.substr(content.end + 1)); | 
| + if (suffix == null) | 
| + return null; | 
| + | 
| + selectors.push(...suffix); | 
| + | 
| + return selectors; | 
| +} | 
| + | 
| +/** Stringified style objects | 
| + * @typedef {Object} StringifiedStyle | 
| + * @property {string} style CSS style represented a string. | 
| 
Wladimir Palant
2017/06/08 13:12:40
This doesn't seem to be quite English to me, also
 
hub
2017/06/08 15:45:47
Acknowledged.
 | 
| + * @property {string[]} subSelectors selectors for the rule. | 
| 
Wladimir Palant
2017/06/08 13:12:39
"selectors that the CSS properties apply to"?
 
hub
2017/06/08 15:45:46
Acknowledged.
 | 
| + */ | 
| + | 
| +/** | 
| + * Turn a CSSStyleRule into a StringifiedStyle | 
| 
Wladimir Palant
2017/06/08 13:12:39
You have the types listed below, no need to repeat
 
hub
2017/06/08 15:45:47
Acknowledged.
 | 
| + * @param {CSSStyleRule} rule the CSS style rule. | 
| + * @return {StringifiedStyle} the stringified style. | 
| + */ | 
| +function stringifyStyle(rule) | 
| +{ | 
| + let styles = []; | 
| + for (let i = 0; i < rule.style.length; i++) | 
| + { | 
| + let property = rule.style.item(i); | 
| + let value = rule.style.getPropertyValue(property); | 
| + let priority = rule.style.getPropertyPriority(property); | 
| + styles.push(property + ": " + value + (priority ? " !" + priority : "") + | 
| 
Sebastian Noack
2017/06/08 08:28:47
Nit: If you'd use a template literal here you woud
 
hub
2017/06/08 15:45:46
This code was simply moved and written pre-ES6. I'
 | 
| + ";"); | 
| + } | 
| + styles.sort(); | 
| + return { | 
| + style: styles.join(" "), | 
| + subSelectors: splitSelector(rule.selectorText) | 
| + }; | 
| +} | 
| + | 
| +function* evaluate(chain, index, prefix, subtree, styles) | 
| +{ | 
| + if (index >= chain.length) | 
| + { | 
| + yield prefix; | 
| + return; | 
| + } | 
| + for (let [selector, element] of | 
| + chain[index].getSelectors(prefix, subtree, styles)) | 
| + yield* evaluate(chain, index + 1, selector, element, styles); | 
| +} | 
| + | 
| +function PlainSelector(selector) | 
| +{ | 
| + this._selector = selector; | 
| +} | 
| + | 
| +PlainSelector.prototype = { | 
| + /** | 
| + * Generator function returning a pair of selector | 
| + * string and subtree. | 
| + * @param {string} prefix the prefix for the selector. | 
| + * @param {Node} subtree the subtree we work on. | 
| + * @param {StringifiedStyle[]} styles the stringified style objects. | 
| + */ | 
| + *getSelectors(prefix, subtree, styles) | 
| + { | 
| + yield [prefix + this._selector, subtree]; | 
| + } | 
| +}; | 
| + | 
| +const incompletePrefixRegexp = /[\s>+~]$/; | 
| + | 
| +function HasSelector(selector) | 
| +{ | 
| + this._innerSelectors = parseSelector(selector); | 
| +} | 
| + | 
| +HasSelector.prototype = { | 
| + requiresHiding: true, | 
| + | 
| + valid() | 
| + { | 
| + return this._innerSelectors != null; | 
| + }, | 
| + | 
| + *getSelectors(prefix, subtree, styles) | 
| + { | 
| + for (let element of this.getElements(prefix, subtree, styles)) | 
| + yield [makeSelector(element, ""), element]; | 
| + }, | 
| + | 
| + /** | 
| + * Generator function returning selected elements. | 
| + * @param {string} prefix the prefix for the selector. | 
| + * @param {Node} subtree the subtree we work on. | 
| + * @param {StringifiedStyle[]} styles the stringified style objects. | 
| + */ | 
| + *getElements(prefix, subtree, styles) | 
| + { | 
| + let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 
| + prefix + "*" : prefix; | 
| + let elements = subtree.querySelectorAll(actualPrefix); | 
| + for (let element of elements) | 
| + { | 
| + let newPrefix = makeSelector(element, ""); | 
| + let iter = evaluate(this._innerSelectors, 0, newPrefix + " ", | 
| + element, styles); | 
| + for (let selector of iter) | 
| + // we insert a space between the two. It becomes a no-op if selector | 
| + // doesn't have a combinator | 
| + if (subtree.querySelector(selector)) | 
| + yield element; | 
| + } | 
| + } | 
| +}; | 
| + | 
| +function PropsSelector(propertyExpression) | 
| +{ | 
| + let 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._regexp = new RegExp(regexpString, "i"); | 
| +} | 
| + | 
| +PropsSelector.prototype = { | 
| + *findPropsSelectors(styles, prefix, regexp) | 
| + { | 
| + for (let style of styles) | 
| + if (regexp.test(style.style)) | 
| + for (let subSelector of style.subSelectors) | 
| + yield prefix + subSelector; | 
| + }, | 
| + | 
| + *getSelectors(prefix, subtree, styles) | 
| + { | 
| + for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 
| + yield [selector, subtree]; | 
| + } | 
| +}; | 
| + | 
| +function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, | 
| + hideElemsFunc) | 
| { | 
| this.window = window; | 
| this.getFiltersFunc = getFiltersFunc; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| + this.hideElemsFunc = hideElemsFunc; | 
| } | 
| ElemHideEmulation.prototype = { | 
| - stringifyStyle(style) | 
| - { | 
| - let styles = []; | 
| - for (let i = 0; i < style.length; i++) | 
| - { | 
| - let property = style.item(i); | 
| - let value = style.getPropertyValue(property); | 
| - let priority = style.getPropertyPriority(property); | 
| - styles.push(property + ": " + value + (priority ? " !" + priority : "") + | 
| - ";"); | 
| - } | 
| - styles.sort(); | 
| - return styles.join(" "); | 
| - }, | 
| - | 
| isSameOrigin(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(stylesheet, selectors, filters) | 
| + addSelectors(stylesheets) | 
| { | 
| - // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| - // between Firefox and Chrome. | 
| - if (!this.isSameOrigin(stylesheet)) | 
| - return; | 
| + let selectors = []; | 
| + let selectorFilters = []; | 
| + | 
| + let elements = []; | 
| + let elementFilters = []; | 
| + | 
| + let cssStyles = []; | 
| - let rules = stylesheet.cssRules; | 
| - if (!rules) | 
| - return; | 
| + for (let stylesheet of stylesheets) | 
| + { | 
| + // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| + // between Firefox and Chrome. | 
| + if (!this.isSameOrigin(stylesheet)) | 
| + continue; | 
| - for (let rule of rules) | 
| - { | 
| - if (rule.type != rule.STYLE_RULE) | 
| + let rules = stylesheet.cssRules; | 
| + if (!rules) | 
| continue; | 
| - let style = this.stringifyStyle(rule.style); | 
| - for (let pattern of this.patterns) | 
| + for (let rule of rules) | 
| { | 
| - if (pattern.regexp.test(style)) | 
| + if (rule.type != rule.STYLE_RULE) | 
| + continue; | 
| + | 
| + cssStyles.push(stringifyStyle(rule)); | 
| + } | 
| + } | 
| + | 
| + for (let pattern of this.patterns) | 
| + { | 
| + for (let selector of evaluate(pattern.selectors, | 
| + 0, "", document, cssStyles)) | 
| + { | 
| + if (!pattern.selectors.some(s => s.requiresHiding)) | 
| { | 
| - let subSelectors = splitSelector(rule.selectorText); | 
| - for (let subSelector of subSelectors) | 
| + selectors.push(selector); | 
| + selectorFilters.push(pattern.text); | 
| + } | 
| + else | 
| + { | 
| + for (let element of document.querySelectorAll(selector)) | 
| { | 
| - selectors.push(pattern.prefix + subSelector + pattern.suffix); | 
| - filters.push(pattern.text); | 
| + elements.push(element); | 
| + elementFilters.push(pattern.text); | 
| } | 
| } | 
| } | 
| } | 
| - }, | 
| - addSelectors(stylesheets) | 
| - { | 
| - let selectors = []; | 
| - let filters = []; | 
| - for (let stylesheet of stylesheets) | 
| - this.findSelectors(stylesheet, selectors, filters); | 
| - this.addSelectorsFunc(selectors, filters); | 
| + this.addSelectorsFunc(selectors, selectorFilters); | 
| + this.hideElemsFunc(elements, elementFilters); | 
| }, | 
| onLoad(event) | 
| { | 
| let stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.addSelectors([stylesheet]); | 
| }, | 
| apply() | 
| { | 
| this.getFiltersFunc(patterns => | 
| { | 
| this.patterns = []; | 
| for (let pattern of patterns) | 
| { | 
| - let match = abpSelectorRegexp.exec(pattern.selector); | 
| - if (!match || match[1] != "properties") | 
| - { | 
| - console.error(new SyntaxError( | 
| - `Failed to parse Adblock Plus selector ${pattern.selector}, ` + | 
| - `invalid pseudo-class :-abp-${match[1]}().` | 
| - )); | 
| - continue; | 
| - } | 
| - | 
| - let expressionStart = match.index + match[0].length; | 
| - let parens = 1; | 
| - let quote = null; | 
| - let i; | 
| - for (i = expressionStart; i < pattern.selector.length; i++) | 
| - { | 
| - let c = pattern.selector[i]; | 
| - if (c == "\\") | 
| - { | 
| - // Ignore escaped characters | 
| - i++; | 
| - } | 
| - else if (quote) | 
| - { | 
| - if (c == quote) | 
| - quote = null; | 
| - } | 
| - else if (c == "'" || c == '"') | 
| - quote = c; | 
| - else if (c == "(") | 
| - parens++; | 
| - else if (c == ")") | 
| - { | 
| - parens--; | 
| - if (parens == 0) | 
| - break; | 
| - } | 
| - } | 
| - | 
| - if (parens > 0) | 
| - { | 
| - console.error(new SyntaxError( | 
| - `Failed to parse Adblock Plus selector ${pattern.selector} ` + | 
| - "due to unmatched parentheses." | 
| - )); | 
| - continue; | 
| - } | 
| - | 
| - let propertyExpression = pattern.selector.substring(expressionStart, i); | 
| - let 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(i + 1) | 
| - }); | 
| + let selectors = parseSelector(pattern.selector); | 
| + if (selectors != null && selectors.length > 0) | 
| + this.patterns.push({selectors, text: pattern.text}); | 
| } | 
| if (this.patterns.length > 0) | 
| { | 
| let {document} = this.window; | 
| this.addSelectors(document.styleSheets); | 
| document.addEventListener("load", this.onLoad.bind(this), true); | 
| } |