| Index: lib/content/elemHideEmulation.js | 
| =================================================================== | 
| --- a/lib/content/elemHideEmulation.js | 
| +++ b/lib/content/elemHideEmulation.js | 
| @@ -35,30 +35,33 @@ | 
| return i + 1; | 
| return 0; | 
| } | 
| function makeSelector(node, selector) | 
| { | 
| if (node == null) | 
| return null; | 
| - if (!node.parentElement) | 
| + | 
| + // If this is the topmost element in a shadow DOM, climb up one more level | 
| + // and then use a ":host" prefix. | 
| + if (!node.parentElement && !(node.parentNode instanceof ShadowRoot)) | 
| { | 
| - let newSelector = ":root"; | 
| + let newSelector = node instanceof ShadowRoot ? ":host" : ":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 makeSelector(node.parentElement || node.parentNode, newSelector); | 
| } | 
| return selector; | 
| } | 
| function parseSelectorContent(content, startIndex) | 
| { | 
| let parens = 1; | 
| @@ -138,16 +141,77 @@ | 
| yield* evaluate(chain, index + 1, selector, element, styles); | 
| } | 
| // Just in case the getSelectors() generator above had to run some heavy | 
| // document.querySelectorAll() call which didn't produce any results, make | 
| // sure there is at least one point where execution can pause. | 
| yield null; | 
| } | 
| +function removeRedundantNodes(nodes) | 
| +{ | 
| + let nodesInfo = []; | 
| + for (let node of nodes) | 
| + { | 
| + let nodeInfo = {node}; | 
| + nodesInfo.push(nodeInfo); | 
| + | 
| + let link = node; | 
| + while (link = link.parentNode) | 
| + { | 
| + // Since a node's ancestors are always added to the DOM before it, they | 
| + // will likely also appear before it in the list (at least on Chromium). | 
| + // If we encounter an ancestor here that has already been marked | 
| + // redundant, we can mark this node redundant too. This is an | 
| + // optimization that saves us having to do a lookup in the set and keep | 
| + // walking up the tree. | 
| + if (link.redundant || nodes.has(link)) | 
| + { | 
| + nodeInfo.redundant = true; | 
| 
 
Sebastian Noack
2017/10/19 00:43:08
Why don't you just keep track of redundant nodes i
 
Manish Jethani
2017/10/19 02:13:44
You mean "node.redundant = true" rather than "node
 
Manish Jethani
2017/10/19 02:19:06
By setting a redundant flag we don't just avoid wa
 
Sebastian Noack
2017/10/19 03:51:01
How about using a Set() object containing only red
 
Manish Jethani
2017/10/19 09:57:14
How do you do lookups efficiently in an array?
Pe
 
Sebastian Noack
2017/10/19 18:35:39
I was basically talking about what lainverse is su
 
Manish Jethani
2017/10/19 23:14:58
OK, I did not realize Node.contains was so efficie
 
 | 
| + break; | 
| + } | 
| + } | 
| + } | 
| + | 
| + for (let nodeInfo of nodesInfo) | 
| + { | 
| + if (nodeInfo.redundant) | 
| + nodes.delete(nodeInfo.node); | 
| + } | 
| + | 
| + return nodes; | 
| +} | 
| + | 
| +function* traverse(nodes) | 
| +{ | 
| + for (let node of nodes) | 
| + { | 
| + yield* traverse(node.children); | 
| + yield node; | 
| + } | 
| +} | 
| + | 
| +function niceLoop(iterator, callback) | 
| +{ | 
| + let loop = () => | 
| + { | 
| + let cycleStart = performance.now(); | 
| + | 
| + for (let next = iterator.next(); !next.done; next = iterator.next()) | 
| + { | 
| + callback(next.value); | 
| + | 
| + if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) | 
| + setTimeout(loop, 0); | 
| 
 
Sebastian Noack
2017/10/19 00:43:08
Don't you have to terminate the loop here?
 
Manish Jethani
2017/10/19 02:13:44
Right, I forgot to terminate it.
Done.
 
 | 
| + } | 
| + }; | 
| + | 
| + loop(); | 
| +} | 
| + | 
| function PlainSelector(selector) | 
| { | 
| this._selector = selector; | 
| } | 
| PlainSelector.prototype = { | 
| /** | 
| * Generator function returning a pair of selector | 
| @@ -297,22 +361,32 @@ | 
| }; | 
| function isSelectorHidingOnlyPattern(pattern) | 
| { | 
| return pattern.selectors.some(s => s.preferHideWithSelector) && | 
| !pattern.selectors.some(s => s.requiresHiding); | 
| } | 
| -function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 
| +function ElemHideEmulation(document, root, addSelectorsFunc, hideElemsFunc) | 
| { | 
| this.document = document; | 
| + this.root = root || document; | 
| this.addSelectorsFunc = addSelectorsFunc; | 
| this.hideElemsFunc = hideElemsFunc; | 
| + this.patterns = []; | 
| this.observer = new MutationObserver(this.observe.bind(this)); | 
| + this.shadowEmulations = new WeakMap(); | 
| + | 
| + if (this.root == this.document) | 
| + { | 
| + this.document.addEventListener("load", this.onLoad.bind(this), true); | 
| + this.document.addEventListener("shadowAttached", | 
| 
 
Sebastian Noack
2017/10/19 00:43:08
So I assume you still intend to patch attachShadow
 
Manish Jethani
2017/10/19 02:13:44
Ack.
Web pages won't get the event because we're
 
 | 
| + this.onShadowAttached.bind(this), true); | 
| + } | 
| } | 
| ElemHideEmulation.prototype = { | 
| isSameOrigin(stylesheet) | 
| { | 
| try | 
| { | 
| return new URL(stylesheet.href).origin == this.document.location.origin; | 
| @@ -404,17 +478,17 @@ | 
| let elements = []; | 
| let elementFilters = []; | 
| let cssStyles = []; | 
| let stylesheetOnlyChange = !!stylesheets; | 
| if (!stylesheets) | 
| - stylesheets = this.document.styleSheets; | 
| + stylesheets = this.root.styleSheets; | 
| for (let stylesheet of stylesheets) | 
| { | 
| // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| // between Firefox and Chrome. | 
| if (!this.isSameOrigin(stylesheet)) | 
| continue; | 
| @@ -454,30 +528,30 @@ | 
| if (stylesheetOnlyChange && | 
| !pattern.selectors.some(selector => selector.dependsOnStyles)) | 
| { | 
| pattern = null; | 
| return processPatterns(); | 
| } | 
| generator = evaluate(pattern.selectors, 0, "", | 
| - this.document, cssStyles); | 
| + this.root, cssStyles); | 
| } | 
| for (let selector of generator) | 
| { | 
| if (selector != null) | 
| { | 
| if (isSelectorHidingOnlyPattern(pattern)) | 
| { | 
| selectors.push(selector); | 
| selectorFilters.push(pattern.text); | 
| } | 
| else | 
| { | 
| - for (let element of this.document.querySelectorAll(selector)) | 
| + for (let element of this.root.querySelectorAll(selector)) | 
| { | 
| elements.push(element); | 
| elementFilters.push(pattern.text); | 
| } | 
| } | 
| } | 
| if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) | 
| { | 
| @@ -555,46 +629,101 @@ | 
| { | 
| this._filteringInProgress = true; | 
| this._addSelectors(stylesheets, completion); | 
| } | 
| }, | 
| onLoad(event) | 
| { | 
| + if (this.patterns.length == 0) | 
| + return; | 
| + | 
| let stylesheet = event.target.sheet; | 
| if (stylesheet) | 
| this.queueFiltering([stylesheet]); | 
| }, | 
| + onShadowAttached(event) | 
| + { | 
| + event.stopImmediatePropagation(); | 
| + | 
| + if (this.patterns.length == 0) | 
| + return; | 
| + | 
| + // The shadow root may not be available if it's a closed shadow root. | 
| + let shadowRoot = event.target.shadowRoot; | 
| + if (!shadowRoot) | 
| + return; | 
| + | 
| + this.addShadowRoot(shadowRoot); | 
| + }, | 
| + | 
| + addShadowRoot(shadowRoot) | 
| + { | 
| + if (!this.shadowEmulations.has(shadowRoot)) | 
| + { | 
| + let emulation = new ElemHideEmulation(this.document, | 
| + shadowRoot, | 
| + this.addSelectorsFunc, | 
| + this.hideElemsFunc); | 
| + this.shadowEmulations.set(shadowRoot, emulation); | 
| + emulation.apply(this.patterns, true); | 
| + } | 
| + }, | 
| + | 
| observe(mutations) | 
| { | 
| + let allAddedElements = new Set(); | 
| + for (let mutation of mutations) | 
| + { | 
| + for (let node of mutation.addedNodes) | 
| + { | 
| + if (node instanceof Element) | 
| + allAddedElements.add(node); | 
| + } | 
| + } | 
| + | 
| + // Find any preattached shadows. | 
| + niceLoop(traverse(removeRedundantNodes(allAddedElements)), node => | 
| + { | 
| + let shadowRoot = node.shadowRoot; | 
| + if (shadowRoot) | 
| + this.addShadowRoot(shadowRoot); | 
| + }); | 
| + | 
| this.queueFiltering(); | 
| }, | 
| - apply(patterns) | 
| + apply(patterns, parsed) | 
| { | 
| - this.patterns = []; | 
| - for (let pattern of patterns) | 
| + if (parsed) | 
| + { | 
| + this.patterns = patterns; | 
| + } | 
| + else | 
| { | 
| - let selectors = this.parseSelector(pattern.selector); | 
| - if (selectors != null && selectors.length > 0) | 
| - this.patterns.push({selectors, text: pattern.text}); | 
| + this.patterns = []; | 
| + for (let pattern of patterns) | 
| + { | 
| + let selectors = this.parseSelector(pattern.selector); | 
| + if (selectors != null && selectors.length > 0) | 
| + this.patterns.push({selectors, text: pattern.text}); | 
| + } | 
| } | 
| if (this.patterns.length > 0) | 
| { | 
| this.queueFiltering(); | 
| this.observer.observe( | 
| - this.document, | 
| + this.root, | 
| { | 
| childList: true, | 
| attributes: true, | 
| characterData: true, | 
| subtree: true | 
| } | 
| ); | 
| - this.document.addEventListener("load", this.onLoad.bind(this), true); | 
| } | 
| } | 
| }; | 
| exports.ElemHideEmulation = ElemHideEmulation; |