Index: chrome/content/elemHideEmulation.js |
=================================================================== |
--- a/chrome/content/elemHideEmulation.js |
+++ b/chrome/content/elemHideEmulation.js |
@@ -1,12 +1,51 @@ |
// 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 |
var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; |
+var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; |
+ |
+// polyfill. We should deal with this better, but PhantomJS doesn't |
+// have matches. At least not in the version we use. |
+// Chrome 34, Firefox 34, Opera 21 and Safari 7.1 do have it. |
+if (!Element.prototype.matches) { |
+ Element.prototype.matches = |
+ Element.prototype.matchesSelector || |
+ Element.prototype.mozMatchesSelector || |
+ Element.prototype.msMatchesSelector || |
+ Element.prototype.oMatchesSelector || |
+ Element.prototype.webkitMatchesSelector |
+} |
+ |
+// return the index were the simple-selector ends |
+function findFirstSelector(selector) |
+{ |
+ var sepIndex = selector.indexOf(' '); |
+ var nextIndex = selector.indexOf('>'); |
+ if (nextIndex != -1) |
+ sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); |
+ nextIndex = selector.indexOf('+'); |
+ if (nextIndex != -1) |
+ sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); |
+ nextIndex = selector.indexOf('~'); |
+ if (nextIndex != -1) |
+ sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); |
+ return sepIndex; |
+} |
+ |
+function extractFirstSelector(selector) |
+{ |
+ var sepIndex = findFirstSelector(selector); |
+ |
+ if (sepIndex == -1) |
+ return selector; |
+ |
+ return selector.substr(0, sepIndex); |
+} |
function splitSelector(selector) |
{ |
if (selector.indexOf(",") == -1) |
return [selector]; |
var selectors = []; |
var start = 0; |
@@ -36,132 +75,326 @@ function splitSelector(selector) |
} |
} |
} |
selectors.push(selector.substring(start)); |
return selectors; |
} |
-function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) |
+function selectChildren(e, selector) |
+{ |
+ var sel = selector; |
+ // XXX we should have a more elegant way |
+ // also startsWith isn't available in PhantomJS. |
+ var combinator = sel.substr(0, 1); |
+ var subElements; |
+ var nextEl = e; |
+ sel = sel.substr(1).trim(); |
+ switch (combinator) |
+ { |
+ case ">": |
+ subElements = e.querySelectorAll(sel); |
+ break; |
+ |
+ case "+": |
+ do |
+ { |
+ nextEl = nextEl.nextSibling; |
+ } |
+ while (nextEl && nextEl.nodeType != 1); |
+ |
+ var siblingSel = extractFirstSelector(sel); |
+ var idx = findFirstSelector(sel); |
+ var childSel = idx != -1 ? sel.substr(idx + 1).trim() : ""; |
+ |
+ if (nextEl && nextEl.matches(siblingSel)) |
+ { |
+ if (childSel != "") |
+ subElements = selectChildren(nextEl, childSel); |
+ else |
+ subElements = [ nextEl ]; |
+ } |
+ break; |
+ |
+ case "~": |
+ do |
+ { |
+ nextEl = nextEl.nextSibling; |
+ if (nextEl && nextEl.nodeType == 1 && nextEl.matches(sel)) |
+ { |
+ subElements = nextEl.querySelectorAll(sel); |
+ break; |
+ } |
+ } |
+ while (nextEl); |
+ |
+ break; |
+ } |
+ return subElements; |
+} |
+ |
+function parsePattern(pattern) |
+{ |
+ // we should catch the :has() pseudo class first. |
+ var match = pseudoClassHasSelectorRegExp.exec(pattern.selector); |
+ if (match) |
+ { |
+ return { |
+ type: "has", |
+ text: pattern.text, |
+ elementMatcher: new PseudoHasMatcher(match[1]), |
+ prefix: pattern.selector.substr(0, match.index).trim(), |
+ suffix: pattern.selector.substr(match.index + match[0].length).trim() |
+ }; |
+ } |
+ |
+ match = propertySelectorRegExp.exec(pattern.selector); |
+ 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: pattern.selector.substr(0, match.index), |
+ suffix: pattern.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); |
+ } |
+ } |
+} |
+ |
+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 }); |
+ if (this.parsed && this.parsed.type == "has") |
+ { |
+ console.log("unsupported :has() pattern", this.hasSelector); |
+ } |
+} |
+ |
+PseudoHasMatcher.prototype = { |
+ match: function(elem, stylesheets, firstOnly) |
+ { |
+ var matches = []; |
+ var selectors = []; |
+ |
+ if (this.parsed) |
+ { |
+ if (this.parsed.type == "has") |
+ return []; |
+ if (this.parsed.type == "props") |
+ { |
+ var filters = []; // don't need this |
+ findPropsSelectors(stylesheets[0], [this.parsed], selectors, filters); |
+ } |
+ } |
+ else |
+ { |
+ selectors = [this.hasSelector]; |
+ } |
+ |
+ // look up for all elements that match the :has(). |
+ var children = elem.children; |
+ for (var i = 0; i < children.length; i++) |
+ { |
+ for (var k = 0; k < selectors.length; k++) |
+ { |
+ try |
+ { |
+ var hasElem = elem.querySelector(selectors[k]); |
+ if (hasElem) |
+ { |
+ matches.push(hasElem); |
+ if (firstOnly) |
+ break; |
+ } |
+ } |
+ catch(e) |
+ { |
+ console.log("Exception with querySelector()", selectors[k]); |
+ } |
+ } |
+ } |
+ return matches; |
+ } |
+}; |
+ |
+function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElementsFunc) |
{ |
this.window = window; |
this.getFiltersFunc = getFiltersFunc; |
this.addSelectorsFunc = addSelectorsFunc; |
+ this.hideElementsFunc = hideElementsFunc; |
} |
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(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 pattern = this.pseudoHasPatterns[i]; |
- var style = this.stringifyStyle(rule.style); |
- for (var j = 0; j < this.patterns.length; j++) |
+ var haveEl = document.querySelectorAll(pattern.prefix); |
+ for (var j = 0; j < haveEl.length; j++) |
{ |
- var pattern = this.patterns[j]; |
- if (pattern.regexp.test(style)) |
+ var matched = pattern.elementMatcher.match(haveEl[j], stylesheets, !pattern.suffix); |
+ if (matched.length == 0) |
+ continue; |
+ |
+ if (pattern.suffix) |
{ |
- var subSelectors = splitSelector(rule.selectorText); |
- for (var k = 0; k < subSelectors.length; k++) |
+ matched.forEach(function(e) |
{ |
- var subSelector = subSelectors[k]; |
- selectors.push(pattern.prefix + subSelector + pattern.suffix); |
- filters.push(pattern.text); |
- } |
+ var subElements = selectChildren(e, pattern.suffix); |
+ if (subElements) |
+ { |
+ for (var k = 0; k < subElements.length; k++) |
+ { |
+ elements.push(subElements[i]); |
+ filters.push(pattern.text); |
+ } |
+ } |
+ }); |
+ } |
+ else |
+ { |
+ elements.push(haveEl[j]); |
+ filters.push(pattern.text); |
} |
} |
} |
}, |
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(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 = parsePattern(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)); |
} |
}; |