Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/content/elemHideEmulation.js

Issue 29559743: Issue 5650 - Apply emulation filters to shadow DOMs Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Find shadow roots semi-asynchronously Created Oct. 18, 2017, 11 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | test/browser/elemHideEmulation.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH 3 * Copyright (C) 2006-present eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
(...skipping 22 matching lines...) Expand all
33 for (let i = 0; i < children.length; i++) 33 for (let i = 0; i < children.length; i++)
34 if (children[i] == node) 34 if (children[i] == node)
35 return i + 1; 35 return i + 1;
36 return 0; 36 return 0;
37 } 37 }
38 38
39 function makeSelector(node, selector) 39 function makeSelector(node, selector)
40 { 40 {
41 if (node == null) 41 if (node == null)
42 return null; 42 return null;
43 if (!node.parentElement) 43
44 // If this is the topmost element in a shadow DOM, climb up one more level
45 // and then use a ":host" prefix.
46 if (!node.parentElement && !(node.parentNode instanceof ShadowRoot))
44 { 47 {
45 let newSelector = ":root"; 48 let newSelector = node instanceof ShadowRoot ? ":host" : ":root";
46 if (selector) 49 if (selector)
47 newSelector += " > " + selector; 50 newSelector += " > " + selector;
48 return newSelector; 51 return newSelector;
49 } 52 }
50 let idx = positionInParent(node); 53 let idx = positionInParent(node);
51 if (idx > 0) 54 if (idx > 0)
52 { 55 {
53 let newSelector = `${node.tagName}:nth-child(${idx})`; 56 let newSelector = `${node.tagName}:nth-child(${idx})`;
54 if (selector) 57 if (selector)
55 newSelector += " > " + selector; 58 newSelector += " > " + selector;
56 return makeSelector(node.parentElement, newSelector); 59 return makeSelector(node.parentElement || node.parentNode, newSelector);
57 } 60 }
58 61
59 return selector; 62 return selector;
60 } 63 }
61 64
62 function parseSelectorContent(content, startIndex) 65 function parseSelectorContent(content, startIndex)
63 { 66 {
64 let parens = 1; 67 let parens = 1;
65 let quote = null; 68 let quote = null;
66 let i = startIndex; 69 let i = startIndex;
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
136 yield null; 139 yield null;
137 else 140 else
138 yield* evaluate(chain, index + 1, selector, element, styles); 141 yield* evaluate(chain, index + 1, selector, element, styles);
139 } 142 }
140 // Just in case the getSelectors() generator above had to run some heavy 143 // Just in case the getSelectors() generator above had to run some heavy
141 // document.querySelectorAll() call which didn't produce any results, make 144 // document.querySelectorAll() call which didn't produce any results, make
142 // sure there is at least one point where execution can pause. 145 // sure there is at least one point where execution can pause.
143 yield null; 146 yield null;
144 } 147 }
145 148
149 function removeRedundantNodes(nodes)
Manish Jethani 2017/10/18 23:07:59 We have to do this, because often (too often) when
150 {
151 let nodesInfo = [];
152 for (let node of nodes)
153 {
154 let nodeInfo = {node};
155 nodesInfo.push(nodeInfo);
156
157 let link = node;
158 while (link = link.parentNode)
159 {
160 // Since a node's ancestors are always added to the DOM before it, they
161 // will likely also appear before it in the list (at least on Chromium).
162 // If we encounter an ancestor here that has already been marked
163 // redundant, we can mark this node redundant too. This is an
164 // optimization that saves us having to do a lookup in the set and keep
165 // walking up the tree.
166 if (link.redundant || nodes.has(link))
167 {
168 nodeInfo.redundant = true;
169 break;
170 }
171 }
172 }
173
174 for (let nodeInfo of nodesInfo)
175 {
176 if (nodeInfo.redundant)
177 nodes.delete(nodeInfo.node);
178 }
179
180 return nodes;
181 }
182
183 function* traverse(nodes)
184 {
185 for (let node of nodes)
186 {
187 yield* traverse(node.children, node);
188 yield node;
189 }
190 }
191
192 function niceLoop(iterator, callback)
Manish Jethani 2017/10/18 23:07:59 This is basically an abstraction of what is happen
193 {
194 let loop = () =>
195 {
196 let cycleStart = performance.now();
197
198 for (let next = iterator.next(); !next.done; next = iterator.next())
Manish Jethani 2017/10/18 23:07:59 We cannot simply do "for (let item of iterator) ..
199 {
200 callback(next.value);
201
202 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
203 setTimeout(loop, 0);
204 }
205 };
206
207 loop();
208 }
209
146 function PlainSelector(selector) 210 function PlainSelector(selector)
147 { 211 {
148 this._selector = selector; 212 this._selector = selector;
149 } 213 }
150 214
151 PlainSelector.prototype = { 215 PlainSelector.prototype = {
152 /** 216 /**
153 * Generator function returning a pair of selector 217 * Generator function returning a pair of selector
154 * string and subtree. 218 * string and subtree.
155 * @param {string} prefix the prefix for the selector. 219 * @param {string} prefix the prefix for the selector.
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
295 yield [selector, subtree]; 359 yield [selector, subtree];
296 } 360 }
297 }; 361 };
298 362
299 function isSelectorHidingOnlyPattern(pattern) 363 function isSelectorHidingOnlyPattern(pattern)
300 { 364 {
301 return pattern.selectors.some(s => s.preferHideWithSelector) && 365 return pattern.selectors.some(s => s.preferHideWithSelector) &&
302 !pattern.selectors.some(s => s.requiresHiding); 366 !pattern.selectors.some(s => s.requiresHiding);
303 } 367 }
304 368
305 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) 369 function ElemHideEmulation(document, root, addSelectorsFunc, hideElemsFunc)
306 { 370 {
307 this.document = document; 371 this.document = document;
372 this.root = root || document;
308 this.addSelectorsFunc = addSelectorsFunc; 373 this.addSelectorsFunc = addSelectorsFunc;
309 this.hideElemsFunc = hideElemsFunc; 374 this.hideElemsFunc = hideElemsFunc;
375 this.patterns = [];
Manish Jethani 2017/10/18 23:07:59 This needs to be initialized.
310 this.observer = new MutationObserver(this.observe.bind(this)); 376 this.observer = new MutationObserver(this.observe.bind(this));
377 this.shadowEmulations = new WeakMap();
378
379 if (this.root == this.document)
380 {
381 this.document.addEventListener("load", this.onLoad.bind(this), true);
382 this.document.addEventListener("shadowAttached",
383 this.onShadowAttached.bind(this), true);
384 }
311 } 385 }
312 386
313 ElemHideEmulation.prototype = { 387 ElemHideEmulation.prototype = {
314 isSameOrigin(stylesheet) 388 isSameOrigin(stylesheet)
315 { 389 {
316 try 390 try
317 { 391 {
318 return new URL(stylesheet.href).origin == this.document.location.origin; 392 return new URL(stylesheet.href).origin == this.document.location.origin;
319 } 393 }
320 catch (e) 394 catch (e)
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
402 let selectors = []; 476 let selectors = [];
403 let selectorFilters = []; 477 let selectorFilters = [];
404 478
405 let elements = []; 479 let elements = [];
406 let elementFilters = []; 480 let elementFilters = [];
407 481
408 let cssStyles = []; 482 let cssStyles = [];
409 483
410 let stylesheetOnlyChange = !!stylesheets; 484 let stylesheetOnlyChange = !!stylesheets;
411 if (!stylesheets) 485 if (!stylesheets)
412 stylesheets = this.document.styleSheets; 486 stylesheets = this.root.styleSheets;
413 487
414 for (let stylesheet of stylesheets) 488 for (let stylesheet of stylesheets)
415 { 489 {
416 // Explicitly ignore third-party stylesheets to ensure consistent behavior 490 // Explicitly ignore third-party stylesheets to ensure consistent behavior
417 // between Firefox and Chrome. 491 // between Firefox and Chrome.
418 if (!this.isSameOrigin(stylesheet)) 492 if (!this.isSameOrigin(stylesheet))
419 continue; 493 continue;
420 494
421 let rules = stylesheet.cssRules; 495 let rules = stylesheet.cssRules;
422 if (!rules) 496 if (!rules)
(...skipping 29 matching lines...) Expand all
452 526
453 pattern = patterns.shift(); 527 pattern = patterns.shift();
454 528
455 if (stylesheetOnlyChange && 529 if (stylesheetOnlyChange &&
456 !pattern.selectors.some(selector => selector.dependsOnStyles)) 530 !pattern.selectors.some(selector => selector.dependsOnStyles))
457 { 531 {
458 pattern = null; 532 pattern = null;
459 return processPatterns(); 533 return processPatterns();
460 } 534 }
461 generator = evaluate(pattern.selectors, 0, "", 535 generator = evaluate(pattern.selectors, 0, "",
462 this.document, cssStyles); 536 this.root, cssStyles);
463 } 537 }
464 for (let selector of generator) 538 for (let selector of generator)
465 { 539 {
466 if (selector != null) 540 if (selector != null)
467 { 541 {
468 if (isSelectorHidingOnlyPattern(pattern)) 542 if (isSelectorHidingOnlyPattern(pattern))
469 { 543 {
470 selectors.push(selector); 544 selectors.push(selector);
471 selectorFilters.push(pattern.text); 545 selectorFilters.push(pattern.text);
472 } 546 }
473 else 547 else
474 { 548 {
475 for (let element of this.document.querySelectorAll(selector)) 549 for (let element of this.root.querySelectorAll(selector))
476 { 550 {
477 elements.push(element); 551 elements.push(element);
478 elementFilters.push(pattern.text); 552 elementFilters.push(pattern.text);
479 } 553 }
480 } 554 }
481 } 555 }
482 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) 556 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
483 { 557 {
484 setTimeout(processPatterns, 0); 558 setTimeout(processPatterns, 0);
485 return; 559 return;
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
553 } 627 }
554 else 628 else
555 { 629 {
556 this._filteringInProgress = true; 630 this._filteringInProgress = true;
557 this._addSelectors(stylesheets, completion); 631 this._addSelectors(stylesheets, completion);
558 } 632 }
559 }, 633 },
560 634
561 onLoad(event) 635 onLoad(event)
562 { 636 {
637 if (this.patterns.length == 0)
638 return;
639
563 let stylesheet = event.target.sheet; 640 let stylesheet = event.target.sheet;
564 if (stylesheet) 641 if (stylesheet)
565 this.queueFiltering([stylesheet]); 642 this.queueFiltering([stylesheet]);
566 }, 643 },
567 644
645 onShadowAttached(event)
646 {
647 event.stopImmediatePropagation();
648
649 if (this.patterns.length == 0)
650 return;
651
652 // The shadow root may not be available if it's a closed shadow root.
653 let shadowRoot = event.target.shadowRoot;
654 if (!shadowRoot)
655 return;
656
657 this.addShadowRoot(shadowRoot);
658 },
659
660 addShadowRoot(shadowRoot)
661 {
662 if (!this.shadowEmulations.has(shadowRoot))
663 {
664 let emulation = new ElemHideEmulation(this.document,
665 shadowRoot,
666 this.addSelectorsFunc,
667 this.hideElemsFunc);
668 this.shadowEmulations.set(shadowRoot, emulation);
669 emulation.apply(this.patterns, true);
670 }
671 },
672
568 observe(mutations) 673 observe(mutations)
569 { 674 {
675 let allAddedElements = new Set();
676 for (let mutation of mutations)
677 {
678 for (let node of mutation.addedNodes)
679 {
680 if (node instanceof Element)
681 allAddedElements.add(node);
682 }
683 }
684
685 // Find any preattached shadows.
686 niceLoop(traverse(removeRedundantNodes(allAddedElements)), node =>
687 {
688 let shadowRoot = node.shadowRoot;
689 if (shadowRoot)
690 this.addShadowRoot(shadowRoot);
691 });
692
570 this.queueFiltering(); 693 this.queueFiltering();
571 }, 694 },
572 695
573 apply(patterns) 696 apply(patterns, parsed)
574 { 697 {
575 this.patterns = []; 698 if (parsed)
576 for (let pattern of patterns)
577 { 699 {
578 let selectors = this.parseSelector(pattern.selector); 700 this.patterns = patterns;
579 if (selectors != null && selectors.length > 0) 701 }
580 this.patterns.push({selectors, text: pattern.text}); 702 else
703 {
704 this.patterns = [];
705 for (let pattern of patterns)
706 {
707 let selectors = this.parseSelector(pattern.selector);
708 if (selectors != null && selectors.length > 0)
709 this.patterns.push({selectors, text: pattern.text});
710 }
581 } 711 }
582 712
583 if (this.patterns.length > 0) 713 if (this.patterns.length > 0)
584 { 714 {
585 this.queueFiltering(); 715 this.queueFiltering();
586 this.observer.observe( 716 this.observer.observe(
587 this.document, 717 this.root,
588 { 718 {
589 childList: true, 719 childList: true,
590 attributes: true, 720 attributes: true,
591 characterData: true, 721 characterData: true,
592 subtree: true 722 subtree: true
593 } 723 }
594 ); 724 );
595 this.document.addEventListener("load", this.onLoad.bind(this), true);
596 } 725 }
597 } 726 }
598 }; 727 };
599 728
600 exports.ElemHideEmulation = ElemHideEmulation; 729 exports.ElemHideEmulation = ElemHideEmulation;
OLDNEW
« no previous file with comments | « no previous file | test/browser/elemHideEmulation.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld