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); |
} |
}); |
} |
}; |