| Index: chrome/content/elemHideEmulation.js |
| =================================================================== |
| --- a/chrome/content/elemHideEmulation.js |
| +++ b/chrome/content/elemHideEmulation.js |
| @@ -14,18 +14,16 @@ |
| * You should have received a copy of the GNU General Public License |
| * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| /* globals filterToRegExp */ |
| "use strict"; |
| -let propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/; |
| - |
| function splitSelector(selector) |
| { |
| if (selector.indexOf(",") == -1) |
| return [selector]; |
| let selectors = []; |
| let start = 0; |
| let level = 0; |
| @@ -54,131 +52,324 @@ |
| } |
| } |
| } |
| selectors.push(selector.substring(start)); |
| return selectors; |
| } |
| +// Return position of node from parent. |
| +// 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) |
| +{ |
| + if (node && node.id && node.id != "") |
| + { |
| + let newSelector = "#" + node.id; |
| + if (selector != "") |
| + newSelector += " > "; |
| + return newSelector + selector; |
| + } |
| + let idx = positionInParent(node); |
| + if (idx > 0) |
| + { |
| + let newSelector = `${node.tagName}:nth-child(${idx}) `; |
| + if (selector != "") |
| + newSelector += "> "; |
| + return makeSelector(node.parentNode, newSelector + selector); |
| + } |
| + |
| + return selector; |
| +} |
| + |
| +// return the regexString for the properties |
| +function parsePropSelPattern(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); |
| + return regexpString; |
| +} |
| + |
| +function parseSelector(selector) |
| +{ |
| + if (selector.length == 0) |
| + return []; |
| + |
| + let abpSelectorIndex = selector.indexOf(":-abp-"); |
| + if (abpSelectorIndex == -1) |
| + return [new PlainSelector(selector)]; |
| + |
| + let selectors = []; |
| + if (abpSelectorIndex > 0) |
| + selectors.push(new PlainSelector(selector.substr(0, abpSelectorIndex))); |
| + |
| + let suffixStart = abpSelectorIndex; |
| + |
| + if (selector.indexOf(":-abp-properties(", abpSelectorIndex) == |
| + abpSelectorIndex) |
| + { |
| + let startIndex = abpSelectorIndex + 17; |
| + let endquoteIndex = selector.indexOf(selector[startIndex], startIndex + 1); |
| + if ((endquoteIndex == -1) || (selector[endquoteIndex + 1] != ")")) |
| + return null; |
| + |
| + selectors.push(new PropsSelector( |
| + selector.substr(startIndex + 1, endquoteIndex - startIndex - 1))); |
| + suffixStart = endquoteIndex + 2; |
| + } |
| + else if (selector.indexOf(":-abp-has(", abpSelectorIndex) == |
| + abpSelectorIndex) |
| + { |
| + let startIndex = abpSelectorIndex + 10; |
| + let parens = 1; |
| + let i; |
| + for (i = startIndex; i < selector.length; i++) |
| + { |
| + if (selector[i] == "(") |
| + parens++; |
| + else if (selector[i] == ")") |
| + parens--; |
| + |
| + if (parens == 0) |
| + break; |
| + } |
| + if (parens != 0) |
| + return null; |
| + selectors.push(new HasSelector( |
| + selector.substr(startIndex, i - startIndex))); |
| + suffixStart = i + 1; |
| + } |
| + |
| + let suffix = parseSelector(selector.substr(suffixStart)); |
| + if (suffix) |
| + selectors.push(...suffix); |
| + |
| + return selectors; |
| +} |
| + |
| +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, pattern, 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); |
| + matchStyleProps(style, rule, pattern, selectors, filters); |
| + } |
| +} |
| + |
| +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(" "); |
| +} |
| + |
| +function* evaluate(chain, index, prefix, subtree, stylesheet) |
| +{ |
| + if (index >= chain.length) |
| + { |
| + yield prefix; |
| + return; |
| + } |
| + for (let [selector, element] of |
| + chain[index].getSelectors(prefix, subtree, stylesheet)) |
| + yield* evaluate(chain, index + 1, selector, element, stylesheet); |
| +} |
| + |
| +/* |
| + * getSelector() is a generator function returning a pair of selector |
| + * string and subtree. |
| + * getElements() is a generator function returning elements selected. |
| + */ |
| +function PlainSelector(selector) |
| +{ |
| + this._selector = selector; |
| +} |
| + |
| +PlainSelector.prototype = { |
| + *getSelectors(prefix, subtree, stylesheet) |
| + { |
| + yield [prefix + this._selector, subtree]; |
| + }, |
| + |
| + *getElements(prefix, subtree, stylesheet) |
| + { |
| + for (let selector of this.getSelectors(prefix, subtree, stylesheet)) |
| + for (let element of subtree.querySelectorAll(selector[0])) |
| + yield element; |
| + } |
| +}; |
| + |
| +function HasSelector(selector) |
| +{ |
| + this._innerSelectors = parseSelector(selector); |
| +} |
| + |
| +HasSelector.prototype = { |
| + *getSelectors(prefix, subtree, stylesheet) |
| + { |
| + for (let element of this.getElements(prefix, subtree, stylesheet)) |
| + yield [prefix + makeSelector(element, ""), subtree]; |
| + }, |
| + |
| + *getElements(prefix, subtree, stylesheet) |
| + { |
| + let elements = subtree.querySelectorAll(prefix ? prefix : "*"); |
| + for (let element of elements) |
| + { |
| + let newPrefix = makeSelector(element, ""); |
| + let iter = evaluate(this._innerSelectors, 0, "", element, stylesheet); |
| + 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(newPrefix + " " + selector)) |
| + yield element; |
| + } |
| + } |
| +}; |
| + |
| +function PropsSelector(selector) |
| +{ |
| + this._regexp = new RegExp(parsePropSelPattern(selector), "i"); |
| +} |
| + |
| +PropsSelector.prototype = { |
| + *getSelectors(prefix, subtree, stylesheet) |
| + { |
| + let selectors = []; |
| + let filters = []; |
| + let selPattern = { |
| + prefix, |
| + suffix: "", |
| + regexp: this._regexp |
| + }; |
| + |
| + findPropsSelectors(stylesheet, selPattern, selectors, filters); |
| + for (let selector of selectors) |
| + yield [selector, subtree]; |
| + }, |
| + |
| + *getElements(prefix, subtree, stylesheet) |
| + { |
| + for (let [selector, element] of |
| + this.getSelectors(prefix, subtree, stylesheet)) |
| + for (let subElement of element.querySelectorAll(selector)) |
| + yield subElement; |
| + } |
| +}; |
| + |
| function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) |
| { |
| this.window = window; |
| this.getFiltersFunc = getFiltersFunc; |
| this.addSelectorsFunc = addSelectorsFunc; |
| } |
| 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(stylesheet) |
| { |
| + let selectors = []; |
| + let 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; |
| + for (let patterns of this.selPatterns) |
| + selectors.push(...evaluate(patterns, 0, "", document, stylesheet)); |
| - 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); |
| - } |
| - } |
| - } |
| - } |
| - }, |
| - |
| - addSelectors(stylesheets) |
| - { |
| - let selectors = []; |
| - let filters = []; |
| - for (let stylesheet of stylesheets) |
| - this.findSelectors(stylesheet, selectors, filters); |
| this.addSelectorsFunc(selectors, filters); |
| }, |
| onLoad(event) |
| { |
| let stylesheet = event.target.sheet; |
| if (stylesheet) |
| - this.addSelectors([stylesheet]); |
| + this.addSelectors(stylesheet); |
| }, |
| apply() |
| { |
| this.getFiltersFunc(patterns => |
| { |
| - this.patterns = []; |
| + this.selPatterns = []; |
| + |
| for (let pattern of patterns) |
| { |
| - let match = propertySelectorRegExp.exec(pattern.selector); |
| - if (!match) |
| - continue; |
| - |
| - let propertyExpression = match[2]; |
| - 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(match.index + match[0].length) |
| - }); |
| + let selectors = parseSelector(pattern.selector); |
| + if (selectors != null && selectors.length > 0) |
| + this.selPatterns.push(selectors); |
| } |
| - if (this.patterns.length > 0) |
| + if (this.selPatterns.length > 0) |
| { |
| let {document} = this.window; |
| - this.addSelectors(document.styleSheets); |
| + for (let stylesheet of document.styleSheets) |
| + this.addSelectors(stylesheet); |
| document.addEventListener("load", this.onLoad.bind(this), true); |
| } |
| }); |
| } |
| }; |