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: Reworked patch following the feedback Created May 25, 2017, 12:58 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
@@ -14,18 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
/* globals filterToRegExp */
"use strict";
-let propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/;
-
function splitSelector(selector)
{
if (selector.indexOf(",") == -1)
return [selector];
let selectors = [];
let start = 0;
let level = 0;
@@ -54,131 +52,391 @@
}
}
}
selectors.push(selector.substring(start));
return selectors;
}
+/** Return position of node from parent.
+ * @param {Node} node - the node to find the position of.
+ * @return {number} 1 base index like for :nth-child(), or 0 on error.
+ */
+function positionInParent(node)
+{
+ if (!node)
+ return 0;
+ 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 += " > ";
+ return newSelector + selector;
+ }
+ let idx = positionInParent(node);
+ if (idx > 0)
+ {
+ let newSelector = `${node.tagName}:nth-child(${idx})`;
+ if (selector)
+ newSelector += " > ";
+ return makeSelector(node.parentElement, newSelector + selector);
+ }
+
+ return selector;
+}
+
+const abpSelectorRegexp = /:-abp-(properties|has|[A-Za-z\d-]*)\(/i;
+
+function parseSelectorContent(content, quoted = false)
+{
+ let parens = 1;
+ let i = 0;
+ let quote = null;
+ let originalLength = content.length;
+ if (quoted)
+ content = content.trim();
+ while (i < content.length)
+ {
+ let c = content[i];
+ if (quoted && i == 0)
+ {
+ if (c != "'" && c != '"')
+ return null;
+ }
+ if (c == "\\")
+ 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;
+ }
+ i++;
+ }
+ if (parens > 0)
+ return null;
+ if (quoted)
+ {
+ let end = content.substr(0, i).lastIndexOf(content[0]);
+ return {text: content.substr(1, end - 1),
+ end: i + (originalLength - content.length)};
+ }
+ return {text: content.substr(0, i), end: i};
+}
+
+function parseSelector(selector, level = 0)
+{
+ if (selector.length == 0)
+ return [];
+
+ let match = abpSelectorRegexp.exec(selector);
+ if (!match)
+ return [new PlainSelector(selector)];
+
+ let selectors = [];
+ let suffixStart = match.index;
+ if (suffixStart > 0)
+ selectors.push(new PlainSelector(selector.substr(0, suffixStart)));
+
+ let startIndex = match.index + match[0].length;
+ let content = null;
+ if (match[1] == "properties")
+ {
+ content = parseSelectorContent(selector.substr(startIndex), true);
+ if (content == null)
+ {
+ console.error(new SyntaxError("Failed to parse AdBlock Plus " +
+ `selector ${selector}, invalid ` +
+ "properties string."));
+ return null;
+ }
+
+ selectors.push(new PropsSelector(content.text));
+ }
+ else if (match[1] == "has")
+ {
+ if (level > 0)
+ {
+ console.error(new SyntaxError("Failed to parse AdBlock Plus " +
+ `selector ${selector}, invalid ` +
+ "nested :-abp-has()."));
+ return null;
+ }
+
+ content = parseSelectorContent(selector.substr(startIndex));
+ if (content == null)
+ {
+ console.error(new SyntaxError("Failed parsing AdBlock Plus " +
+ `selector ${selector}, didn't ` +
+ "find closing parenthesis."));
+ return null;
+ }
+
+ 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;
+ }
+
+ suffixStart = startIndex + content.end + 1;
+
+ let suffix = parseSelector(selector.substr(suffixStart), level);
+ if (suffix == null)
+ return null;
+
+ selectors.push(...suffix);
+
+ return selectors;
+}
+
+function *findPropsSelectors(styles, prefix, regexp)
+{
+ for (let style of styles)
+ if (regexp.test(style.style))
+ for (let subSelector of style.subSelectors)
+ yield prefix + subSelector;
+}
+
+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(" ");
+}
+
+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 {Array} styles - the stringified stylesheets.
+ */
+ *getSelectors(prefix, subtree, styles)
+ {
+ yield [prefix + this._selector, subtree];
+ },
+
+ /**
+ * Generator function returning selected elements.
+ * @param {String} prefix - the prefix for the selector.
+ * @param {Node} subtree - the subtree we work on.
+ * @param {Array} styles - the stringified stylesheets.
+ */
+ *getElements(prefix, subtree, styles)
+ {
+ for (let [selector] of this.getSelectors(prefix, subtree, styles))
+ for (let element of subtree.querySelectorAll(selector))
+ yield element;
+ }
+};
+
+const prefixEndRegexp = /[\s>+~]$/;
+
+function HasSelector(selector, level = 0)
+{
+ this._innerSelectors = parseSelector(selector, level + 1);
+}
+
+HasSelector.prototype = {
+ valid()
+ {
+ return this._innerSelectors != null;
+ },
+
+ *getSelectors(prefix, subtree, styles)
+ {
+ for (let element of this.getElements(prefix, subtree, styles))
+ yield [makeSelector(element, ""), element];
+ },
+
+ *getElements(prefix, subtree, styles)
+ {
+ let actualPrefix = (!prefix || prefixEndRegexp.test(prefix)) ?
+ prefix + "*" : prefix;
+ let elements = subtree.querySelectorAll(actualPrefix);
+ for (let element of elements)
+ {
+ let newPrefix = makeSelector(element, "");
+ let iter = evaluate(this._innerSelectors, 0, "", 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(newPrefix + " " + 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 = {
+ *getSelectors(prefix, subtree, styles)
+ {
+ for (let selector of findPropsSelectors(styles, prefix, this._regexp))
+ yield [selector, subtree];
+ },
+
+ *getElements(prefix, subtree, styles)
+ {
+ for (let [selector] of this.getSelectors(prefix, subtree, styles))
+ for (let subElement of subtree.querySelectorAll(selector))
+ yield subElement;
+ }
+};
+
function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc)
{
this.window = window;
this.getFiltersFunc = getFiltersFunc;
this.addSelectorsFunc = addSelectorsFunc;
}
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)
- {
- // 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);
- }
- }
- }
- }
- },
-
addSelectors(stylesheets)
{
let selectors = [];
let filters = [];
+
+ let cssStyles = [];
+
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;
+
+ let rules = stylesheet.cssRules;
+ if (!rules)
+ continue;
+
+ for (let rule of rules)
+ {
+ if (rule.type != rule.STYLE_RULE)
+ continue;
+
+ let style = stringifyStyle(rule.style);
+ let subSelectors = splitSelector(rule.selectorText);
+ cssStyles.push({style, subSelectors});
+ }
+ }
+
+ for (let patterns of this.selPatterns)
+ for (let selector of evaluate(patterns.selectors,
+ 0, "", document, cssStyles))
+ {
+ selectors.push(selector);
+ filters.push(patterns.text);
+ }
+
this.addSelectorsFunc(selectors, filters);
},
onLoad(event)
{
let stylesheet = event.target.sheet;
if (stylesheet)
this.addSelectors([stylesheet]);
},
apply()
{
this.getFiltersFunc(patterns =>
{
- this.patterns = [];
+ this.selPatterns = [];
+
for (let pattern of patterns)
{
- let match = propertySelectorRegExp.exec(pattern.selector);
- if (!match)
- continue;
-
- let propertyExpression = match[2];
- 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(match.index + match[0].length)
- });
+ let selectors = parseSelector(pattern.selector);
+ if (selectors != null && selectors.length > 0)
+ this.selPatterns.push({selectors, text: pattern.text});
}
- if (this.patterns.length > 0)
+ if (this.selPatterns.length > 0)
{
let {document} = this.window;
this.addSelectors(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