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