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: Validate the syntax, remove unused flags Created April 10, 2017, 1:12 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
@@ -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
Wladimir Palant 2017/04/25 10:57:52 This is really a PITA. Given that https://issues.a
hub 2017/04/25 19:42:47 If issue 4796 lands before this, then I'll fix it.
Wladimir Palant 2017/04/26 00:03:09 As I suggested on IRC, it would probably be easies
hub 2017/04/27 17:43:04 Newer patch is now ES6 based on top of your patch.
-var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/;
+var propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/;
+var abpSelectorRegExp = /\[-abp-selector=(["'])(.+)\1\]/;
+var pseudoClassHasSelectorRegExp = /:has\((.*)\)/;
+var pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/;
Wladimir Palant 2017/04/25 11:30:46 Having :has() but :-abp-properties() is inconsiste
hub 2017/04/25 19:42:47 OK.
function splitSelector(selector)
{
if (selector.indexOf(",") == -1)
return [selector];
var selectors = [];
var start = 0;
@@ -36,132 +39,341 @@
}
}
}
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);
Wladimir Palant 2017/04/25 10:57:52 This hack is problematic. Even if we make the attr
hub 2017/04/25 19:42:47 I was thinking we could generate an actually rando
hub 2017/04/27 17:43:04 On 2017/04/25 19:42:47, hub wrote: Newer patch re
+
+ 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)
Wladimir Palant 2017/04/25 10:57:53 This unwrapping belongs into lib/filterClasses.js
hub 2017/04/25 19:42:47 ok, moving it.
+{
+ 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}, true);
+ }
+
+ return parsePattern(pattern, false);
+}
+
+function parsePattern(pattern, unwrapped)
+{
+ var selector = pattern.selector;
+ var match;
+
+ if (unwrapped)
+ {
+ // Seems that the pattern should be unwrapped.
+ // This is a fundamental error.
+ if (selector.indexOf("[-abp-selector=") != -1)
+ return;
+
+ match = pseudoClassHasSelectorRegExp.exec(selector);
Wladimir Palant 2017/04/25 10:57:52 The syntax is getting too complex, parsing CSS wit
+ 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);
Wladimir Palant 2017/04/25 10:57:52 I don't think that we should have this special han
hub 2017/04/25 19:42:47 good idea. Like unwrap above.
+ 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 ];
Wladimir Palant 2017/04/25 10:57:53 This will work correctly for selectors like "div >
hub 2017/04/25 19:42:47 I naively assumed that since these are filters, we
Wladimir Palant 2017/04/26 00:03:09 Feel free to suggest a meaningful subset of the sy
+ 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}, true);
+}
+
+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);
Wladimir Palant 2017/04/25 10:57:52 We will be going through the stylesheets multiple
+ }
+ }
+ 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;
Wladimir Palant 2017/04/25 10:57:53 I guess that hideElementsFunc should do some kind
}
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));
}
};
« 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