| 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,324 @@ 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 |
| + for (var i = 0; i < stylesheets.length; i++) |
| + findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); |
| + } |
| + } |
| + else |
| + { |
| + selectors = [this.hasSelector]; |
| + } |
| + |
| + // look up for all elements that match the :has(). |
| + for (var k = 0; k < selectors.length; k++) |
| + { |
| + try |
| + { |
| + console.log("querying", selectors[k]); |
| + 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)); |
| } |
| }; |