| Index: chrome/content/elemHideEmulation.js |
| =================================================================== |
| --- a/chrome/content/elemHideEmulation.js |
| +++ b/chrome/content/elemHideEmulation.js |
| @@ -70,16 +70,18 @@ |
| for (let i = 0; i < children.length; i++) |
| if (children[i] == node) |
| return i + 1; |
| return 0; |
| } |
| function makeSelector(node, selector) |
| { |
| + if (node == null) |
| + return null; |
| if (!node.parentElement) |
| { |
| let newSelector = ":root"; |
| if (selector) |
| newSelector += " > " + selector; |
| return newSelector; |
| } |
| let idx = positionInParent(node); |
| @@ -159,19 +161,32 @@ |
| function* evaluate(chain, index, prefix, subtree, styles) |
| { |
| if (index >= chain.length) |
| { |
| yield prefix; |
| return; |
| } |
| + if (prefix == null) |
|
Wladimir Palant
2017/08/10 10:12:20
How would we get into this situation? From what I
hub
2017/08/11 16:26:51
Acknowledged.
|
| + { |
| + yield null; |
| + return; |
| + } |
| + let count = 0; |
| for (let [selector, element] of |
| chain[index].getSelectors(prefix, subtree, styles)) |
| + { |
| + if (selector == null) |
| + continue; |
|
Wladimir Palant
2017/08/10 10:12:21
getSelectors() generator might be performing non-t
hub
2017/08/11 16:26:50
Acknowledged.
|
| + count++; |
| yield* evaluate(chain, index + 1, selector, element, styles); |
| + } |
| + if (count == 0) |
| + yield null; |
|
Wladimir Palant
2017/08/10 10:12:22
I don't think we need this counter, doing this unc
hub
2017/08/11 16:26:50
Acknowledged.
|
| } |
| function PlainSelector(selector) |
| { |
| this._selector = selector; |
| } |
| PlainSelector.prototype = { |
| @@ -216,26 +231,37 @@ |
| * @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); |
| + if (elements.length == 0) |
| + yield null; |
|
Wladimir Palant
2017/08/10 10:12:20
We don't need to protect against this scenario mor
hub
2017/08/11 16:26:48
Acknowledged.
|
| for (let element of elements) |
| { |
| + let count = 0; |
| let iter = evaluate(this._innerSelectors, 0, "", element, styles); |
| for (let selector of iter) |
| { |
| + count++; |
| + if (selector == null) |
| + { |
| + yield null; |
| + continue; |
| + } |
| if (relativeSelector.test(selector)) |
| selector = ":scope" + selector; |
| if (element.querySelector(selector)) |
| yield element; |
| } |
| + if (count == 0) |
| + yield null; |
|
Wladimir Palant
2017/08/10 10:12:22
Like above, we don't need the counter - an uncondi
hub
2017/08/11 16:26:50
Acknowledged.
|
| } |
| } |
| }; |
| function ContainsSelector(textContent) |
| { |
| this._text = textContent; |
| } |
| @@ -249,19 +275,30 @@ |
| yield [makeSelector(element, ""), subtree]; |
| }, |
| *getElements(prefix, subtree, stylesheet) |
| { |
| let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
| prefix + "*" : prefix; |
| let elements = subtree.querySelectorAll(actualPrefix); |
| - for (let element of elements) |
| - if (element.textContent.includes(this._text)) |
| - yield element; |
| + if (elements.length == 0) |
| + yield null; |
|
Wladimir Palant
2017/08/10 10:12:21
We don't need to protect against this scenario mor
hub
2017/08/11 16:26:50
Acknowledged.
|
| + else |
| + { |
| + let count = 0; |
| + for (let element of elements) |
| + if (element.textContent.includes(this._text)) |
| + { |
| + count++; |
| + yield element; |
| + } |
|
Wladimir Palant
2017/08/10 10:12:21
We don't need this counter, just make sure to yiel
hub
2017/08/11 16:26:50
Acknowledged.
|
| + if (count == 0) |
| + yield null; |
| + } |
| } |
| }; |
| function PropsSelector(propertyExpression) |
| { |
| let regexpString; |
| if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
| propertyExpression[propertyExpression.length - 1] == "/") |
| @@ -301,23 +338,30 @@ |
| *getSelectors(prefix, subtree, styles) |
| { |
| for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
| yield [selector, subtree]; |
| } |
| }; |
| +function isSelectorHidingOnlyPattern(pattern) |
|
Wladimir Palant
2017/08/10 10:12:22
Note that this function is called exactly once, so
hub
2017/08/11 16:26:48
There was a time it was called twice. but that was
|
| +{ |
| + return pattern.selectors.some(s => s.preferHideWithSelector) && |
| + !pattern.selectors.some(s => s.requiresHiding); |
| +} |
| + |
| function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, |
| hideElemsFunc) |
| { |
| this.window = window; |
| this.getFiltersFunc = getFiltersFunc; |
| this.addSelectorsFunc = addSelectorsFunc; |
| this.hideElemsFunc = hideElemsFunc; |
| + this.observer = new window.MutationObserver(this.observe.bind(this)); |
| } |
| ElemHideEmulation.prototype = { |
| isSameOrigin(stylesheet) |
| { |
| try |
| { |
| return new URL(stylesheet.href).origin == this.window.location.origin; |
| @@ -399,30 +443,34 @@ |
| /** |
| * Processes the current document and applies all rules to it. |
| * @param {CSSStyleSheet[]} [stylesheets] |
| * The list of new stylesheets that have been added to the document and |
| * made reprocessing necessary. This parameter shouldn't be passed in for |
| * the initial processing, all of document's stylesheets will be considered |
| * then and all rules, including the ones not dependent on styles. |
| + * @param {boolean} [domUpdate] |
| + * Indicate this is a DOM update. |
|
Wladimir Palant
2017/08/10 10:12:21
Why do we need this parameter? Its purpose seems t
hub
2017/08/11 16:26:51
It doesn't mean "ignore the stylesheets". Both can
|
| + * @param {function} [done] |
| + * Callback to call when done. |
| */ |
| - addSelectors(stylesheets) |
| + addSelectors(stylesheets, domUpdate, done) |
| { |
| this._lastInvocation = Date.now(); |
|
Wladimir Palant
2017/08/10 10:12:22
With this method being asynchronous now, I think t
hub
2017/08/11 16:26:48
Ah right, Date.now() isn't monotonic.
|
| let selectors = []; |
| let selectorFilters = []; |
| let elements = []; |
| let elementFilters = []; |
| let cssStyles = []; |
| - let stylesheetOnlyChange = !!stylesheets; |
| + let stylesheetOnlyChange = !!stylesheets && !domUpdate; |
| if (!stylesheets) |
| stylesheets = this.window.document.styleSheets; |
| // Chrome < 51 doesn't have an iterable StyleSheetList |
| // https://issues.adblockplus.org/ticket/5381 |
| for (let i = 0; i < stylesheets.length; i++) |
| { |
| let stylesheet = stylesheets[i]; |
| @@ -440,72 +488,164 @@ |
| if (rule.type != rule.STYLE_RULE) |
| continue; |
| cssStyles.push(stringifyStyle(rule)); |
| } |
| } |
| let {document} = this.window; |
| - for (let pattern of this.patterns) |
| + |
| + let lastCycle = Date.now(); |
| + |
| + let processPatterns = function(patternIterator) |
| { |
| - if (stylesheetOnlyChange && |
| - !pattern.selectors.some(selector => selector.dependsOnStyles)) |
| + for (let pattern of patternIterator) |
| { |
| - continue; |
| + if (stylesheetOnlyChange && |
| + !pattern.selectors.some(selector => selector.dependsOnStyles)) |
| + { |
| + continue; |
| + } |
| + |
| + for (let selector of evaluate(pattern.selectors, 0, "", document, |
| + cssStyles)) |
| + { |
| + if (selector == null) |
| + continue; |
| + if (isSelectorHidingOnlyPattern(pattern)) |
| + { |
| + selectors.push(selector); |
| + selectorFilters.push(pattern.text); |
| + } |
| + else |
| + { |
| + for (let element of document.querySelectorAll(selector)) |
| + { |
| + elements.push(element); |
| + elementFilters.push(pattern.text); |
| + } |
| + } |
| + } |
| + |
| + let now = Date.now(); |
| + if (now - lastCycle > 50) |
|
Wladimir Palant
2017/08/10 10:12:21
No magic numbers please, you should declare a cons
hub
2017/08/11 16:26:47
Done.
|
| + { |
| + lastCycle = now; |
| + this.window.setTimeout(() => |
| + { |
| + processPatterns(patternIterator); |
| + }, 0); |
| + return; |
|
Wladimir Palant
2017/08/10 10:12:20
So, why did we go through all the effort yielding
hub
2017/08/11 16:26:50
Done.
|
| + } |
| } |
| - for (let selector of evaluate(pattern.selectors, |
| - 0, "", document, cssStyles)) |
| - { |
| - if (pattern.selectors.some(s => s.preferHideWithSelector) && |
| - !pattern.selectors.some(s => s.requiresHiding)) |
| - { |
| - selectors.push(selector); |
| - selectorFilters.push(pattern.text); |
| - } |
| - else |
| - { |
| - for (let element of document.querySelectorAll(selector)) |
| - { |
| - elements.push(element); |
| - elementFilters.push(pattern.text); |
| - } |
| - } |
| - } |
| - } |
| + this.addSelectorsFunc(selectors, selectorFilters); |
| + this.hideElemsFunc(elements, elementFilters); |
| + if (typeof done == "function") |
| + done(); |
| + }.bind(this); |
|
Wladimir Palant
2017/08/10 10:12:20
Don't call bind(), use an arrow function instead.
hub
2017/08/11 16:26:51
Done.
|
| - this.addSelectorsFunc(selectors, selectorFilters); |
| - this.hideElemsFunc(elements, elementFilters); |
| + processPatterns(this.patterns[Symbol.iterator]()); |
| }, |
| _stylesheetQueue: null, |
| + _domUpdate: false, |
| + _filteringInProgress: false, |
| + _nextRun: null, |
| + |
| + /** Filtering reason |
|
Wladimir Palant
2017/08/10 10:12:21
Nit: We usually have /** on a line of its own and
hub
2017/08/11 16:26:47
Acknowledged.
|
| + * @typedef {Object} FilteringReason |
| + * @property {boolean} dom Indicate the DOM changed (tree or attributes) |
|
Wladimir Palant
2017/08/10 10:12:21
I get what this sentence means, but I think that i
hub
2017/08/11 16:26:49
Acknowledged.
|
| + * @property {CSSStyleSheet[]} [stylesheets] |
| + * Indicate the stylesheets that needs refresh |
|
Wladimir Palant
2017/08/10 10:12:22
Nit: need, not needs
However, it's not the styles
hub
2017/08/11 16:26:47
Acknowledged.
|
| + */ |
| + |
| + /** Re-run filtering either immediately or queued. |
| + * @param {FilteringReason} reason why the filtering must be queued. |
| + */ |
| + queueFiltering(reason) |
| + { |
| + if (!this._stylesheetQueue && |
| + Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
|
Wladimir Palant
2017/08/10 10:12:20
It seems that we will go into this case if addSele
hub
2017/08/11 16:26:48
Oops. Done.
|
| + { |
| + this._stylesheetQueue = []; |
|
Wladimir Palant
2017/08/10 10:12:22
This should be `reason.stylesheets || []` I guess?
hub
2017/08/11 16:26:51
Done.
|
| + this._domUpdate = reason.dom; |
| + this.window.setTimeout(() => |
| + { |
| + this._filteringInProgress = true; |
| + let domUpdate = this._domUpdate; |
| + this._domUpdate = false; |
| + let stylesheets = this._stylesheetQueue; |
| + this._stylesheetQueue = null; |
| + this.addSelectors(stylesheets, domUpdate, () => |
| + { |
| + this._filteringInProgress = false; |
| + if (this._nextRun) |
| + { |
| + let nextReason = this._nextRun; |
| + this._nextRun = null; |
| + this.queueFiltering(nextReason); |
| + } |
| + }); |
| + }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
| + } |
| + if (this._filteringInProgress) |
|
Wladimir Palant
2017/08/10 10:12:21
This should be `else if` I guess? As it is now, th
hub
2017/08/11 16:26:48
Done.
|
| + { |
| + if (!this._nextRun) |
|
Wladimir Palant
2017/08/10 10:12:22
Why use a new variable here instead of reusing _st
hub
2017/08/11 16:26:47
Done.
hub
2017/08/11 16:26:47
Done.
|
| + this._nextRun = reason; |
| + else |
| + { |
| + this._nextRun.dom = this._nextRun.dom || reason.dom; |
| + if (!this._nextRun.stylesheets) |
| + this._nextRun.stylesheets = []; |
| + this._nextRun.stylesheets.push(...reason.stylesheets); |
|
Wladimir Palant
2017/08/10 10:12:23
reason.stylesheets is optional, this will fail if
hub
2017/08/11 16:26:47
Acknowledged.
|
| + } |
| + } |
| + else if (this._stylesheetQueue) |
| + { |
| + if (reason.stylesheets) |
| + this._stylesheetQueue.push(...reason.stylesheets); |
| + this._domUpdate = this._domUpdate || reason.domUpdate; |
| + } |
| + else |
| + { |
| + let stylesheets = reason.stylesheets || []; |
| + this.addSelectors(stylesheets, reason.domUpdate); |
| + } |
| + }, |
| onLoad(event) |
| { |
| let stylesheet = event.target.sheet; |
| if (stylesheet) |
| + this.queueFiltering({stylesheets: [stylesheet]}); |
| + }, |
| + |
| + observe(mutations) |
| + { |
| + let reason = {}; |
| + reason.dom = true; |
| + let stylesheets = []; |
| + for (let mutation of mutations) |
| { |
| - if (!this._stylesheetQueue && |
| - Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
| + if (mutation.type == "childList") |
|
Wladimir Palant
2017/08/10 10:12:21
What if the website adds the style element first a
hub
2017/08/11 16:26:47
It might not work.
I think we should observe for
|
| { |
| - this._stylesheetQueue = []; |
| - this.window.setTimeout(() => |
| + for (let added of mutation.addedNodes) |
| { |
| - let stylesheets = this._stylesheetQueue; |
| - this._stylesheetQueue = null; |
| - this.addSelectors(stylesheets); |
| - }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); |
| + if (added.nodeType == this.window.Node.ELEMENT_NODE && |
| + (added.tagName == "STYLE" || added.tagName == "style") && |
|
Wladimir Palant
2017/08/10 10:12:22
This should be checking for `added.localName == "s
hub
2017/08/11 16:26:51
Done.
|
| + added.styesheet) |
|
Wladimir Palant
2017/08/10 10:12:22
Typo: added.stylesheet
I guess that this code pat
hub
2017/08/11 16:26:49
oops.
|
| + stylesheets.push(added.stylesheet); |
| + } |
| } |
| - |
| - if (this._stylesheetQueue) |
| - this._stylesheetQueue.push(stylesheet); |
| - else |
| - this.addSelectors([stylesheet]); |
| } |
| + if (stylesheets.length > 0) |
| + reason.stylesheets = stylesheets; |
| + this.queueFiltering(reason); |
| }, |
| apply() |
| { |
| this.getFiltersFunc(patterns => |
| { |
| this.patterns = []; |
| for (let pattern of patterns) |
| @@ -513,14 +653,24 @@ |
| let selectors = this.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.addEventListener("load", this.onLoad.bind(this), true); |
| + this.addSelectors(null, true, () => { |
|
Wladimir Palant
2017/08/10 10:12:22
If you want queueFiltering() to set important stat
hub
2017/08/11 16:26:47
I'll call queueFiltering() instead. Totally make s
|
| + this.observer.observe( |
| + document, |
| + { |
| + childList: true, |
| + attributes: true, |
| + subtree: true, |
| + attributeFilter: ["class", "id"] |
|
Wladimir Palant
2017/08/10 10:12:20
We need to observe characterData changes as well.
hub
2017/08/11 16:26:51
I kept them deliberately a bit more restricted as
|
| + } |
| + ); |
| + document.addEventListener("load", this.onLoad.bind(this), true); |
| + }); |
|
Wladimir Palant
2017/08/10 10:12:21
The logic here is wrong, we should register the mu
hub
2017/08/11 16:26:50
make sense. Done.
|
| } |
| }); |
| } |
| }; |