| Index: chrome/content/elemHideEmulation.js |
| =================================================================== |
| --- a/chrome/content/elemHideEmulation.js |
| +++ b/chrome/content/elemHideEmulation.js |
| @@ -16,16 +16,18 @@ |
| */ |
| /* globals filterToRegExp */ |
| "use strict"; |
| const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
| +let reportError = () => {}; |
| + |
| function splitSelector(selector) |
| { |
| if (selector.indexOf(",") == -1) |
| return [selector]; |
| let selectors = []; |
| let start = 0; |
| let level = 0; |
| @@ -54,172 +56,373 @@ |
| } |
| } |
| } |
| selectors.push(selector.substring(start)); |
| return selectors; |
| } |
| -function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) |
| +/** Return position of node from parent. |
| + * @param {Node} node the node to find the position of. |
| + * @return {number} One-based index like for :nth-child(), or 0 on error. |
| + */ |
| +function positionInParent(node) |
| +{ |
| + 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 += " > " + selector; |
| + return newSelector; |
| + } |
| + let idx = positionInParent(node); |
| + if (idx > 0) |
| + { |
| + let newSelector = `${node.tagName}:nth-child(${idx})`; |
| + if (selector) |
| + newSelector += " > " + selector; |
| + return makeSelector(node.parentElement, newSelector); |
| + } |
| + |
| + return selector; |
| +} |
| + |
| +function parseSelectorContent(content, startIndex) |
| +{ |
| + let parens = 1; |
| + let quote = null; |
| + let i = startIndex; |
| + for (; i < content.length; i++) |
| + { |
| + let c = content[i]; |
| + if (c == "\\") |
| + { |
| + // Ignore escaped characters |
| + 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; |
| + } |
| + } |
| + |
| + if (parens > 0) |
| + return null; |
| + return {text: content.substring(startIndex, i), end: i}; |
| +} |
| + |
| +/** Parse the selector |
| + * @param {string} selector the selector to parse |
| + * @return {Object} selectors is an array of objects, |
| + * or null in case of errors. hide is true if we'll hide |
| + * elements instead of styles.. |
| + */ |
| +function parseSelector(selector) |
| +{ |
| + if (selector.length == 0) |
| + return []; |
| + |
| + let match = abpSelectorRegexp.exec(selector); |
| + if (!match) |
| + return [new PlainSelector(selector)]; |
| + |
| + let selectors = []; |
| + if (match.index > 0) |
| + selectors.push(new PlainSelector(selector.substr(0, match.index))); |
| + |
| + let startIndex = match.index + match[0].length; |
| + let content = parseSelectorContent(selector, startIndex); |
| + if (!content) |
| + { |
| + reportError(new SyntaxError("Failed parsing content filter " + |
| + `selector ${selector}, didn't ` + |
| + "find closing parenthesis.")); |
| + return null; |
| + } |
| + if (match[1] == "properties") |
| + selectors.push(new PropsSelector(content.text)); |
| + else if (match[1] == "has") |
| + { |
| + let hasSelector = new HasSelector(content.text); |
| + if (!hasSelector.valid()) |
| + return null; |
| + selectors.push(hasSelector); |
| + } |
| + else |
| + { |
| + // this is an error, can't parse selector. |
| + reportError(new SyntaxError("Failed parsing content filter " + |
| + `selector ${selector}, invalid ` + |
| + `pseudo-class -abp-${match[1]}().`)); |
| + return null; |
| + } |
| + |
| + let suffix = parseSelector(selector.substr(content.end + 1)); |
| + if (suffix == null) |
| + return null; |
| + |
| + selectors.push(...suffix); |
| + |
| + return selectors; |
| +} |
| + |
| +/** Stringified style objects |
| + * @typedef {Object} StringifiedStyle |
| + * @property {string} style CSS style represented by a string. |
| + * @property {string[]} subSelectors selectors the CSS properties apply to. |
| + */ |
| + |
| +/** |
| + * Produce a string representation of the stylesheet entry. |
| + * @param {CSSStyleRule} rule the CSS style rule. |
| + * @return {StringifiedStyle} the stringified style. |
| + */ |
| +function stringifyStyle(rule) |
| +{ |
| + let styles = []; |
| + for (let i = 0; i < rule.style.length; i++) |
| + { |
| + let property = rule.style.item(i); |
| + let value = rule.style.getPropertyValue(property); |
| + let priority = rule.style.getPropertyPriority(property); |
| + styles.push(`${property}: ${value}${priority ? " !" + priority : ""};`); |
| + } |
| + styles.sort(); |
| + return { |
| + style: styles.join(" "), |
| + subSelectors: splitSelector(rule.selectorText) |
| + }; |
| +} |
| + |
| +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 {StringifiedStyle[]} styles the stringified style objects. |
| + */ |
| + *getSelectors(prefix, subtree, styles) |
| + { |
| + yield [prefix + this._selector, subtree]; |
| + } |
| +}; |
| + |
| +const incompletePrefixRegexp = /[\s>+~]$/; |
| + |
| +function HasSelector(selector) |
| +{ |
| + this._innerSelectors = parseSelector(selector); |
| +} |
| + |
| +HasSelector.prototype = { |
| + requiresHiding: true, |
| + |
| + valid() |
| + { |
| + return this._innerSelectors != null; |
| + }, |
| + |
| + *getSelectors(prefix, subtree, styles) |
| + { |
| + for (let element of this.getElements(prefix, subtree, styles)) |
| + yield [makeSelector(element, ""), element]; |
| + }, |
| + |
| + /** |
| + * Generator function returning selected elements. |
| + * @param {string} prefix the prefix for the selector. |
| + * @param {Node} subtree the subtree we work on. |
| + * @param {StringifiedStyle[]} styles the stringified style objects. |
| + */ |
| + *getElements(prefix, subtree, styles) |
| + { |
| + let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
| + prefix + "*" : prefix; |
| + let elements = subtree.querySelectorAll(actualPrefix); |
| + for (let element of elements) |
| + { |
| + let newPrefix = makeSelector(element, ""); |
| + let iter = evaluate(this._innerSelectors, 0, newPrefix + " ", |
| + 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(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 = { |
| + *findPropsSelectors(styles, prefix, regexp) |
| + { |
| + for (let style of styles) |
| + if (regexp.test(style.style)) |
| + for (let subSelector of style.subSelectors) |
| + yield prefix + subSelector; |
| + }, |
| + |
| + *getSelectors(prefix, subtree, styles) |
| + { |
| + for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
| + yield [selector, subtree]; |
| + } |
| +}; |
| + |
| +function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, |
| + hideElemsFunc) |
| { |
| this.window = window; |
| this.getFiltersFunc = getFiltersFunc; |
| this.addSelectorsFunc = addSelectorsFunc; |
| + this.hideElemsFunc = hideElemsFunc; |
| + reportError = error => this.window.console.error(error); |
|
Wladimir Palant
2017/06/13 13:16:25
This won't work. There might be multiple windows l
hub
2017/06/13 13:53:33
doing 1)
|
| } |
| 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) |
| + addSelectors(stylesheets) |
| { |
| - // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| - // between Firefox and Chrome. |
| - if (!this.isSameOrigin(stylesheet)) |
| - return; |
| + let selectors = []; |
| + let selectorFilters = []; |
| + |
| + let elements = []; |
| + let elementFilters = []; |
| + |
| + let cssStyles = []; |
| - let rules = stylesheet.cssRules; |
| - if (!rules) |
| - return; |
| + for (let stylesheet of stylesheets) |
| + { |
| + // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| + // between Firefox and Chrome. |
| + if (!this.isSameOrigin(stylesheet)) |
| + continue; |
| - for (let rule of rules) |
| - { |
| - if (rule.type != rule.STYLE_RULE) |
| + let rules = stylesheet.cssRules; |
| + if (!rules) |
| continue; |
| - let style = this.stringifyStyle(rule.style); |
| - for (let pattern of this.patterns) |
| + for (let rule of rules) |
| { |
| - if (pattern.regexp.test(style)) |
| + if (rule.type != rule.STYLE_RULE) |
| + continue; |
| + |
| + cssStyles.push(stringifyStyle(rule)); |
| + } |
| + } |
| + |
| + let {document} = this.window; |
| + for (let pattern of this.patterns) |
| + { |
| + for (let selector of evaluate(pattern.selectors, |
| + 0, "", document, cssStyles)) |
| + { |
| + if (!pattern.selectors.some(s => s.requiresHiding)) |
| { |
| - let subSelectors = splitSelector(rule.selectorText); |
| - for (let subSelector of subSelectors) |
| + selectors.push(selector); |
| + selectorFilters.push(pattern.text); |
| + } |
| + else |
| + { |
| + for (let element of document.querySelectorAll(selector)) |
| { |
| - selectors.push(pattern.prefix + subSelector + pattern.suffix); |
| - filters.push(pattern.text); |
| + elements.push(element); |
| + elementFilters.push(pattern.text); |
| } |
| } |
| } |
| } |
| - }, |
| - addSelectors(stylesheets) |
| - { |
| - let selectors = []; |
| - let filters = []; |
| - for (let stylesheet of stylesheets) |
| - this.findSelectors(stylesheet, selectors, filters); |
| - this.addSelectorsFunc(selectors, filters); |
| + this.addSelectorsFunc(selectors, selectorFilters); |
| + this.hideElemsFunc(elements, elementFilters); |
| }, |
| onLoad(event) |
| { |
| let stylesheet = event.target.sheet; |
| if (stylesheet) |
| this.addSelectors([stylesheet]); |
| }, |
| apply() |
| { |
| this.getFiltersFunc(patterns => |
| { |
| this.patterns = []; |
| for (let pattern of patterns) |
| { |
| - let match = abpSelectorRegexp.exec(pattern.selector); |
| - if (!match || match[1] != "properties") |
| - { |
| - console.error(new SyntaxError( |
| - `Failed to parse Adblock Plus selector ${pattern.selector}, ` + |
| - `invalid pseudo-class :-abp-${match[1]}().` |
| - )); |
| - continue; |
| - } |
| - |
| - let expressionStart = match.index + match[0].length; |
| - let parens = 1; |
| - let quote = null; |
| - let i; |
| - for (i = expressionStart; i < pattern.selector.length; i++) |
| - { |
| - let c = pattern.selector[i]; |
| - if (c == "\\") |
| - { |
| - // Ignore escaped characters |
| - 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; |
| - } |
| - } |
| - |
| - if (parens > 0) |
| - { |
| - console.error(new SyntaxError( |
| - `Failed to parse Adblock Plus selector ${pattern.selector} ` + |
| - "due to unmatched parentheses." |
| - )); |
| - continue; |
| - } |
| - |
| - let propertyExpression = pattern.selector.substring(expressionStart, i); |
| - 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(i + 1) |
| - }); |
| + let selectors = parseSelector(pattern.selector); |
| + if (selectors != null && selectors.length > 0) |
| + this.patterns.push({selectors, text: pattern.text}); |
| } |
| if (this.patterns.length > 0) |
| { |
| let {document} = this.window; |
| this.addSelectors(document.styleSheets); |
| document.addEventListener("load", this.onLoad.bind(this), true); |
| } |