Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: chrome/content/elemHideEmulation.js

Issue 29383960: Issue 3143 - Filter elements with :-abp-has() (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore
Patch Set: Moved unwrap to filterClass, ES6 syntax, use :-abp-has(), don't modify DOM to select, rebased on 29… Created April 27, 2017, 4:10 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | lib/filterClasses.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/content/elemHideEmulation.js
===================================================================
--- a/chrome/content/elemHideEmulation.js
+++ b/chrome/content/elemHideEmulation.js
@@ -15,16 +15,18 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
/* globals filterToRegExp */
"use strict";
let propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/;
+let pseudoClassHasSelectorRegExp = /:-abp-has\((.*)\)/;
+let pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/;
function splitSelector(selector)
{
if (selector.indexOf(",") == -1)
return [selector];
let selectors = [];
let start = 0;
@@ -54,131 +56,332 @@
}
}
}
selectors.push(selector.substring(start));
return selectors;
}
-function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc)
+// 1 base index like for :nth-child()
+function positionInParent(node)
+{
+ let parentNode = node ? node.parentNode : null;
+ if (parentNode == null)
+ return 0;
+
+ let {children} = parentNode;
+ if (!children)
+ return 0;
+ let i = 0;
+ for (i = 0; i < children.length; i++)
+ if (children[i] == node)
+ break;
+ return i + 1;
+}
+
+function makeSelector(node, selector)
+{
+ let idx = positionInParent(node);
+ if (idx > 0)
+ {
+ let newSelector = `:nth-child(${idx}) `;
+ if (selector != "")
+ newSelector += "> ";
+ return makeSelector(node.parentNode, newSelector + selector);
+ }
+
+ return selector;
+}
+
+function matchChildren(e, selector)
+{
+ let newSelector = makeSelector(e, "");
+ newSelector += selector;
+
+ return document.querySelector(newSelector) != null;
+}
+
+function selectChildren(e, selector)
+{
+ let newSelector = makeSelector(e, "");
+ newSelector += selector;
+
+ return document.querySelectorAll(newSelector);
+}
+
+function parsePattern(pattern)
+{
+ let {selector} = pattern;
+ let match = pseudoClassHasSelectorRegExp.exec(selector);
+ 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)
+ {
+ let regexpString;
+ let 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))
+ {
+ let subSelectors = splitSelector(rule.selectorText);
+ for (let i = 0; i < subSelectors.length; i++)
+ {
+ let subSelector = subSelectors[i];
+ selectors.push(pattern.prefix + subSelector + pattern.suffix);
+ filters.push(pattern.text);
+ }
+ }
+}
+
+function findPropsSelectors(stylesheet, patterns, selectors, filters)
+{
+ let rules = stylesheet.cssRules;
+ if (!rules)
+ return;
+
+ for (let rule of rules)
+ {
+ if (rule.type != rule.STYLE_RULE)
+ continue;
+
+ let style = stringifyStyle(rule.style);
+ for (let pattern of patterns)
+ {
+ matchStyleProps(style, rule, pattern, selectors, filters);
+ }
+ }
+}
+
+/**
+ * Match the selector @pattern containing :-abp-has() starting from @node.
+ * @param {string} pattern - the pattern to match
+ * @param {object} node - the top level node.
+ * @param {object} stylesheets - the stylesheets to check from.
+ * @param {array} elements - elements that match
+ * @param {array} filters - filters that match
+ */
+function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters)
+{
+ // select element for the prefix pattern. Or just use node.
+ let haveElems = pattern.prefix ? node.querySelectorAll(pattern.prefix) :
+ [node];
+ for (let elem of haveElems)
+ {
+ let matched = pattern.elementMatcher.match(elem, stylesheets);
+ if (!matched)
+ continue;
+
+ if (pattern.suffix)
+ {
+ let subElements = selectChildren(elem, pattern.suffix);
+ if (subElements)
+ {
+ for (let subElement of subElements)
+ {
+ elements.push(subElement);
+ filters.push(pattern.text);
+ }
+ }
+ }
+ else
+ {
+ elements.push(elem);
+ filters.push(pattern.text);
+ }
+ }
+}
+
+function 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(" ");
+}
+
+/** matcher for the pseudo class :-abp-has()
+ * For those browser that don't have it yet.
+ * @param {string} selector - the inner selector.
+ */
+function PseudoHasMatcher(selector)
+{
+ this.hasSelector = selector;
+ this.parsed = parsePattern({selector});
+}
+
+PseudoHasMatcher.prototype = {
+ match(elem, stylesheets)
+ {
+ let selectors = [];
+
+ if (this.parsed)
+ {
+ let filters = []; // don't need this, we have a partial filter.
+ if (this.parsed.type == "has")
+ {
+ let child = elem.firstChild;
+ while (child)
+ {
+ if (child.nodeType === Node.ELEMENT_NODE)
+ {
+ let 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 (let stylesheet of stylesheets)
+ findPropsSelectors(stylesheet, [this.parsed], selectors, filters);
+ }
+ else
+ selectors = [this.hasSelector];
+
+ let matched = false;
+ // look up for all elements that match the :-abp-has().
+ for (let selector of selectors)
+ try
+ {
+ matched = matchChildren(elem, selector);
+ if (matched)
+ break;
+ }
+ catch (e)
+ {
+ console.error("Exception with querySelector()", selector, e);
+ }
+
+ return matched;
+ }
+};
+
+function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
+ hideElementsFunc)
{
this.window = window;
this.getFiltersFunc = getFiltersFunc;
this.addSelectorsFunc = addSelectorsFunc;
+ this.hideElementsFunc = hideElementsFunc;
}
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)
+ findPseudoClassHasElements(node, stylesheets, elements, filters)
{
- // Explicitly ignore third-party stylesheets to ensure consistent behavior
- // between Firefox and Chrome.
- if (!this.isSameOrigin(stylesheet))
- return;
-
- let rules = stylesheet.cssRules;
- if (!rules)
- return;
-
- for (let rule of rules)
- {
- if (rule.type != rule.STYLE_RULE)
- continue;
-
- let style = this.stringifyStyle(rule.style);
- for (let pattern of this.patterns)
- {
- if (pattern.regexp.test(style))
- {
- let subSelectors = splitSelector(rule.selectorText);
- for (let subSelector of subSelectors)
- {
- selectors.push(pattern.prefix + subSelector + pattern.suffix);
- filters.push(pattern.text);
- }
- }
- }
- }
+ for (let pattern of this.pseudoHasPatterns)
+ pseudoClassHasMatch(pattern, node, stylesheets, elements, filters);
},
addSelectors(stylesheets)
{
let selectors = [];
let filters = [];
for (let stylesheet of stylesheets)
- this.findSelectors(stylesheet, selectors, filters);
+ {
+ // Explicitly ignore third-party stylesheets to ensure consistent behavior
+ // between Firefox and Chrome.
+ if (!this.isSameOrigin(stylesheet))
+ continue;
+ findPropsSelectors(stylesheet, this.propSelPatterns, selectors, filters);
+ }
this.addSelectorsFunc(selectors, filters);
},
+ hideElements(stylesheets)
+ {
+ let elements = [];
+ let filters = [];
+ this.findPseudoClassHasElements(document, stylesheets, elements, filters);
+ this.hideElementsFunc(elements, filters);
+ },
+
onLoad(event)
{
let stylesheet = event.target.sheet;
if (stylesheet)
this.addSelectors([stylesheet]);
+ this.hideElements([stylesheet]);
},
apply()
{
this.getFiltersFunc(patterns =>
{
- this.patterns = [];
+ this.propSelPatterns = [];
+ this.pseudoHasPatterns = [];
+
for (let pattern of patterns)
{
- let match = propertySelectorRegExp.exec(pattern.selector);
- if (!match)
+ let parsed = parsePattern(pattern);
+ if (parsed == undefined)
continue;
- let propertyExpression = match[2];
- let regexpString;
- if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
- propertyExpression[propertyExpression.length - 1] == "/")
+ if (parsed.type == "props")
{
- regexpString = propertyExpression.slice(1, -1)
- .replace("\\x7B ", "{").replace("\\x7D ", "}");
+ this.propSelPatterns.push(parsed);
}
- 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)
- });
+ else if (parsed.type == "has")
+ {
+ this.pseudoHasPatterns.push(parsed);
+ }
}
- if (this.patterns.length > 0)
+ if (this.pseudoHasPatterns.length > 0 || this.propSelPatterns.length > 0)
{
let {document} = this.window;
this.addSelectors(document.styleSheets);
+ this.hideElements(document.styleSheets);
document.addEventListener("load", this.onLoad.bind(this), true);
}
});
}
};
« no previous file with comments | « no previous file | lib/filterClasses.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld