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: Run code only on if Shadow DOM is supported Created Oct. 20, 2017, 4:13 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 18 matching lines...) Expand all
29 */ 29 */
30 function positionInParent(node) 30 function positionInParent(node)
31 { 31 {
32 let {children} = node.parentNode; 32 let {children} = node.parentNode;
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 isShadowRoot(node)
40 {
41 return typeof ShadowRoot != "undefined" && node instanceof ShadowRoot;
42 }
43
39 function makeSelector(node, selector) 44 function makeSelector(node, selector)
40 { 45 {
41 if (node == null) 46 if (node == null)
42 return null; 47 return null;
43 if (!node.parentElement) 48
49 // If this is the topmost element in a shadow DOM, climb up one more level
50 // and then use a ":host" prefix.
51 if (!node.parentElement && !isShadowRoot(node.parentNode))
44 { 52 {
45 let newSelector = ":root"; 53 let newSelector = isShadowRoot(node) ? ":host" : ":root";
46 if (selector) 54 if (selector)
47 newSelector += " > " + selector; 55 newSelector += " > " + selector;
48 return newSelector; 56 return newSelector;
49 } 57 }
50 let idx = positionInParent(node); 58 let idx = positionInParent(node);
51 if (idx > 0) 59 if (idx > 0)
52 { 60 {
53 let newSelector = `${node.tagName}:nth-child(${idx})`; 61 let newSelector = `${node.tagName}:nth-child(${idx})`;
54 if (selector) 62 if (selector)
55 newSelector += " > " + selector; 63 newSelector += " > " + selector;
56 return makeSelector(node.parentElement, newSelector); 64 return makeSelector(node.parentElement || node.parentNode, newSelector);
57 } 65 }
58 66
59 return selector; 67 return selector;
60 } 68 }
61 69
62 function parseSelectorContent(content, startIndex) 70 function parseSelectorContent(content, startIndex)
63 { 71 {
64 let parens = 1; 72 let parens = 1;
65 let quote = null; 73 let quote = null;
66 let i = startIndex; 74 let i = startIndex;
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
136 yield null; 144 yield null;
137 else 145 else
138 yield* evaluate(chain, index + 1, selector, element, styles); 146 yield* evaluate(chain, index + 1, selector, element, styles);
139 } 147 }
140 // Just in case the getSelectors() generator above had to run some heavy 148 // Just in case the getSelectors() generator above had to run some heavy
141 // document.querySelectorAll() call which didn't produce any results, make 149 // document.querySelectorAll() call which didn't produce any results, make
142 // sure there is at least one point where execution can pause. 150 // sure there is at least one point where execution can pause.
143 yield null; 151 yield null;
144 } 152 }
145 153
154 function extractSubtrees(nodes)
155 {
156 return nodes.filter(
157 (node, index) => !nodes.some(
158 // The index comparison is based on the fact that a node always appears
159 // in the list before any of its descendants.
160 (otherNode, otherIndex) => otherIndex < index && otherNode.contains(node)
161 )
162 );
163 }
164
165 function* traverse(nodes)
166 {
167 for (let node of nodes)
168 {
169 yield* traverse(node.children);
170 yield node;
171 }
172 }
173
174 function niceLoop(iterator, callback)
175 {
176 let loop = () =>
177 {
178 let cycleStart = performance.now();
179
180 for (let next = iterator.next(); !next.done; next = iterator.next())
181 {
182 callback(next.value);
183
184 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
185 {
186 setTimeout(loop, 0);
187 return;
188 }
189 }
190 };
191
192 loop();
193 }
194
146 function PlainSelector(selector) 195 function PlainSelector(selector)
147 { 196 {
148 this._selector = selector; 197 this._selector = selector;
149 } 198 }
150 199
151 PlainSelector.prototype = { 200 PlainSelector.prototype = {
152 /** 201 /**
153 * Generator function returning a pair of selector 202 * Generator function returning a pair of selector
154 * string and subtree. 203 * string and subtree.
155 * @param {string} prefix the prefix for the selector. 204 * @param {string} prefix the prefix for the selector.
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
295 yield [selector, subtree]; 344 yield [selector, subtree];
296 } 345 }
297 }; 346 };
298 347
299 function isSelectorHidingOnlyPattern(pattern) 348 function isSelectorHidingOnlyPattern(pattern)
300 { 349 {
301 return pattern.selectors.some(s => s.preferHideWithSelector) && 350 return pattern.selectors.some(s => s.preferHideWithSelector) &&
302 !pattern.selectors.some(s => s.requiresHiding); 351 !pattern.selectors.some(s => s.requiresHiding);
303 } 352 }
304 353
305 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) 354 function ElemHideEmulation(document, root, addSelectorsFunc, hideElemsFunc,
355 shadowAttachedEventType)
306 { 356 {
307 this.document = document; 357 this.document = document;
358 this.root = root || document;
308 this.addSelectorsFunc = addSelectorsFunc; 359 this.addSelectorsFunc = addSelectorsFunc;
309 this.hideElemsFunc = hideElemsFunc; 360 this.hideElemsFunc = hideElemsFunc;
361 this.shadowAttachedEventType = shadowAttachedEventType;
362 this.patterns = [];
310 this.observer = new MutationObserver(this.observe.bind(this)); 363 this.observer = new MutationObserver(this.observe.bind(this));
364 this.shadowEmulations = new WeakMap();
365
366 if (shadowAttachedEventType)
367 {
368 this.root.addEventListener(shadowAttachedEventType,
369 this.onShadowAttached.bind(this), true);
370 }
371
372 if (this.root == this.document)
373 this.document.addEventListener("load", this.onLoad.bind(this), true);
374 else
375 this.findShadowRoots(this.root.children);
311 } 376 }
312 377
313 ElemHideEmulation.prototype = { 378 ElemHideEmulation.prototype = {
314 isSameOrigin(stylesheet) 379 isSameOrigin(stylesheet)
315 { 380 {
316 try 381 try
317 { 382 {
318 return new URL(stylesheet.href).origin == this.document.location.origin; 383 return new URL(stylesheet.href).origin == this.document.location.origin;
319 } 384 }
320 catch (e) 385 catch (e)
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
402 let selectors = []; 467 let selectors = [];
403 let selectorFilters = []; 468 let selectorFilters = [];
404 469
405 let elements = []; 470 let elements = [];
406 let elementFilters = []; 471 let elementFilters = [];
407 472
408 let cssStyles = []; 473 let cssStyles = [];
409 474
410 let stylesheetOnlyChange = !!stylesheets; 475 let stylesheetOnlyChange = !!stylesheets;
411 if (!stylesheets) 476 if (!stylesheets)
412 stylesheets = this.document.styleSheets; 477 stylesheets = this.root.styleSheets;
413 478
414 for (let stylesheet of stylesheets) 479 for (let stylesheet of stylesheets)
415 { 480 {
416 // Explicitly ignore third-party stylesheets to ensure consistent behavior 481 // Explicitly ignore third-party stylesheets to ensure consistent behavior
417 // between Firefox and Chrome. 482 // between Firefox and Chrome.
418 if (!this.isSameOrigin(stylesheet)) 483 if (!this.isSameOrigin(stylesheet))
419 continue; 484 continue;
420 485
421 let rules = stylesheet.cssRules; 486 let rules = stylesheet.cssRules;
422 if (!rules) 487 if (!rules)
(...skipping 29 matching lines...) Expand all
452 517
453 pattern = patterns.shift(); 518 pattern = patterns.shift();
454 519
455 if (stylesheetOnlyChange && 520 if (stylesheetOnlyChange &&
456 !pattern.selectors.some(selector => selector.dependsOnStyles)) 521 !pattern.selectors.some(selector => selector.dependsOnStyles))
457 { 522 {
458 pattern = null; 523 pattern = null;
459 return processPatterns(); 524 return processPatterns();
460 } 525 }
461 generator = evaluate(pattern.selectors, 0, "", 526 generator = evaluate(pattern.selectors, 0, "",
462 this.document, cssStyles); 527 this.root, cssStyles);
463 } 528 }
464 for (let selector of generator) 529 for (let selector of generator)
465 { 530 {
466 if (selector != null) 531 if (selector != null)
467 { 532 {
468 if (isSelectorHidingOnlyPattern(pattern)) 533 if (isSelectorHidingOnlyPattern(pattern))
469 { 534 {
470 selectors.push(selector); 535 selectors.push(selector);
471 selectorFilters.push(pattern.text); 536 selectorFilters.push(pattern.text);
472 } 537 }
473 else 538 else
474 { 539 {
475 for (let element of this.document.querySelectorAll(selector)) 540 for (let element of this.root.querySelectorAll(selector))
476 { 541 {
477 elements.push(element); 542 elements.push(element);
478 elementFilters.push(pattern.text); 543 elementFilters.push(pattern.text);
479 } 544 }
480 } 545 }
481 } 546 }
482 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) 547 if (performance.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
483 { 548 {
484 setTimeout(processPatterns, 0); 549 setTimeout(processPatterns, 0);
485 return; 550 return;
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
553 } 618 }
554 else 619 else
555 { 620 {
556 this._filteringInProgress = true; 621 this._filteringInProgress = true;
557 this._addSelectors(stylesheets, completion); 622 this._addSelectors(stylesheets, completion);
558 } 623 }
559 }, 624 },
560 625
561 onLoad(event) 626 onLoad(event)
562 { 627 {
628 this.findShadowRoots(this.root.children);
629
630 if (this.patterns.length == 0)
631 return;
632
563 let stylesheet = event.target.sheet; 633 let stylesheet = event.target.sheet;
564 if (stylesheet) 634 if (stylesheet)
565 this.queueFiltering([stylesheet]); 635 this.queueFiltering([stylesheet]);
566 }, 636 },
567 637
638 onShadowAttached(event)
639 {
640 event.stopImmediatePropagation();
641
642 if (this.patterns.length == 0)
643 return;
644
645 // The shadow root may not be available if it's a closed shadow root.
646 let shadowRoot = event.target.shadowRoot;
647 if (!shadowRoot)
648 return;
649
650 this.addShadowRoot(shadowRoot);
651 },
652
653 addShadowRoot(shadowRoot)
654 {
655 if (!this.shadowEmulations.has(shadowRoot))
656 {
657 let emulation = new ElemHideEmulation(this.document,
658 shadowRoot,
659 this.addSelectorsFunc,
660 this.hideElemsFunc,
661 this.shadowAttachedEventType);
662 this.shadowEmulations.set(shadowRoot, emulation);
663 emulation.apply(this.patterns, true);
664 }
665 },
666
667 findShadowRoots(nodes)
668 {
669 niceLoop(traverse(nodes), node =>
670 {
671 let shadowRoot = node.shadowRoot;
672 if (shadowRoot)
673 this.addShadowRoot(shadowRoot);
674 });
675 },
676
568 observe(mutations) 677 observe(mutations)
569 { 678 {
679 if (typeof ShadowRoot != "undefined")
Manish Jethani 2017/10/20 16:16:42 Of course we only have to run this code on Chrome.
hub 2017/10/20 19:12:50 Maybe we should set a different mutation observer
Manish Jethani 2017/10/22 20:01:35 But both the observers would listen for the same D
hub 2017/10/23 13:30:34 not saying to observe twice. but when we set up th
Manish Jethani 2017/10/23 23:04:28 OK, I see what you mean. I think we're going to en
680 {
681 let allAddedElements = [];
682 for (let mutation of mutations)
683 {
684 for (let node of mutation.addedNodes)
685 {
686 if (node instanceof Element)
687 allAddedElements.push(node);
688 }
689 }
690
691 // Find any preattached shadows.
692 this.findShadowRoots(extractSubtrees(allAddedElements));
693 }
694
570 this.queueFiltering(); 695 this.queueFiltering();
571 }, 696 },
572 697
573 apply(patterns) 698 apply(patterns, parsed)
574 { 699 {
575 this.patterns = []; 700 if (parsed)
576 for (let pattern of patterns)
577 { 701 {
578 let selectors = this.parseSelector(pattern.selector); 702 this.patterns = patterns;
579 if (selectors != null && selectors.length > 0) 703 }
580 this.patterns.push({selectors, text: pattern.text}); 704 else
705 {
706 this.patterns = [];
707 for (let pattern of patterns)
708 {
709 let selectors = this.parseSelector(pattern.selector);
710 if (selectors != null && selectors.length > 0)
711 this.patterns.push({selectors, text: pattern.text});
712 }
581 } 713 }
582 714
583 if (this.patterns.length > 0) 715 if (this.patterns.length > 0)
584 { 716 {
585 this.queueFiltering(); 717 this.queueFiltering();
586 this.observer.observe( 718 this.observer.observe(
587 this.document, 719 this.root,
588 { 720 {
589 childList: true, 721 childList: true,
590 attributes: true, 722 attributes: true,
591 characterData: true, 723 characterData: true,
592 subtree: true 724 subtree: true
593 } 725 }
594 ); 726 );
595 this.document.addEventListener("load", this.onLoad.bind(this), true);
596 } 727 }
597 } 728 }
598 }; 729 };
599 730
600 exports.ElemHideEmulation = ElemHideEmulation; 731 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