 Issue 29728690:
  Issue 6504, 6458, 6446 - Update selectors based on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/
    
  
    Issue 29728690:
  Issue 6504, 6458, 6446 - Update selectors based on DOM mutations  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore/| Left: | ||
| Right: | 
| LEFT | RIGHT | 
|---|---|
| 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 456 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 467 function shouldObserveAttributes(patterns) | 467 function shouldObserveAttributes(patterns) | 
| 468 { | 468 { | 
| 469 return patterns.some(pattern => pattern.maybeDependsOnAttributes); | 469 return patterns.some(pattern => pattern.maybeDependsOnAttributes); | 
| 470 } | 470 } | 
| 471 | 471 | 
| 472 function shouldObserveCharacterData(patterns) | 472 function shouldObserveCharacterData(patterns) | 
| 473 { | 473 { | 
| 474 return patterns.some(pattern => pattern.dependsOnCharacterData); | 474 return patterns.some(pattern => pattern.dependsOnCharacterData); | 
| 475 } | 475 } | 
| 476 | 476 | 
| 477 function updateSelectorsForMutation(mutation, selectorSet, selectors, | 477 function CSSSelectorManager(root) | 
| 478 selectorFilters, root) | 478 { | 
| 479 { | 479 this._root = root; | 
| 480 let {target} = mutation; | 480 | 
| 481 | 481 this.selectors = []; | 
| 482 // Existing selectors aren't affected by non-childList mutations. | 482 this.filters = []; | 
| 483 if (mutation.type != "childList" || !root.contains(target)) | 483 | 
| 484 return false; | 484 this._lookup = new Set(); | 
| 485 | 485 | 
| 486 // Find the mutation index, i.e. the index in the parent element where the | 486 this._extended = false; | 
| 487 // mutation has occurred. | 487 } | 
| 488 let index = 0; | 488 | 
| 489 let ref = mutation.previousSibling; | 489 CSSSelectorManager.prototype = { | 
| 490 if (ref) | 490 add(selectors, filters) | 
| 491 { | 491 { | 
| 492 if (!(ref instanceof Element)) | 492 for (let i = 0; i < selectors.length; i++) | 
| 493 ref = ref.previousElementSibling; | 493 { | 
| 494 | 494 this.selectors.push(selectors[i]); | 
| 495 this.filters.push(filters[i]); | |
| 496 | |
| 497 this._lookup.add(selectors[i]); | |
| 498 | |
| 499 if (!this._extended && selectors[i].startsWith(":root > ")) | |
| 500 this._extended = true; | |
| 501 } | |
| 502 }, | |
| 503 | |
| 504 has(selector) | |
| 505 { | |
| 506 return this._lookup.has(selector); | |
| 507 }, | |
| 508 | |
| 509 setAt(index, selector) | |
| 510 { | |
| 511 this._lookup.delete(this.selectors[index]); | |
| 512 this._lookup.add(selector); | |
| 513 | |
| 514 this.selectors[index] = selector; | |
| 515 }, | |
| 516 | |
| 517 deleteAt(index) | |
| 518 { | |
| 519 this._lookup.delete(this.selectors[index]); | |
| 520 | |
| 521 this.selectors.splice(index, 1); | |
| 522 this.filters.splice(index, 1); | |
| 523 }, | |
| 524 | |
| 525 update(mutations) | |
| 526 { | |
| 527 // If there are no extended CSS selectors (i.e. :-abp-*), there's nothing | |
| 528 // to update. | |
| 529 if (!this._extended || this.selectors.length == 0) | |
| 
Manish Jethani
2018/03/21 15:35:26
This is useful. In EasyList the number of sites wi
 | |
| 530 return false; | |
| 531 | |
| 532 let changed = false; | |
| 533 | |
| 534 for (let mutation of mutations) | |
| 535 { | |
| 536 if (this._updateForMutation(mutation)) | |
| 537 changed = true; | |
| 538 } | |
| 539 | |
| 540 return changed; | |
| 541 }, | |
| 542 | |
| 543 _updateForMutation(mutation) | |
| 544 { | |
| 545 let {target} = mutation; | |
| 546 | |
| 547 // Existing selectors aren't affected by non-childList mutations. | |
| 548 if (mutation.type != "childList" || !this._root.contains(target)) | |
| 549 return false; | |
| 550 | |
| 551 // Find the mutation index, i.e. the index in the parent element where the | |
| 552 // mutation has occurred. | |
| 553 let index = 0; | |
| 554 let ref = mutation.previousSibling; | |
| 495 if (ref) | 555 if (ref) | 
| 496 index = indexOf(ref.parentNode.children, ref) + 1; | 556 { | 
| 497 } | 557 if (!(ref instanceof Element)) | 
| 498 | 558 ref = ref.previousElementSibling; | 
| 499 // Count the number of elements added and removed. Both can be non-zero in | 559 | 
| 500 // some cases, e.g. when Element.innerHTML is set on the parent element to | 560 if (ref) | 
| 501 // overwrite all the existing child nodes. | 561 index = positionInParent(ref); | 
| 502 let numAddedElements = 0; | 562 } | 
| 503 let numRemovedElements = 0; | 563 | 
| 504 | 564 // Count the number of elements added and removed. Both can be non-zero in | 
| 505 for (let node of mutation.addedNodes) | 565 // some cases, e.g. when Element.innerHTML is set on the parent element to | 
| 506 { | 566 // overwrite all the existing child nodes. | 
| 507 if (node instanceof Element) | 567 let numAddedElements = 0; | 
| 508 numAddedElements++; | 568 let numRemovedElements = 0; | 
| 509 } | 569 | 
| 510 | 570 for (let node of mutation.addedNodes) | 
| 511 for (let node of mutation.removedNodes) | 571 { | 
| 512 { | 572 if (node instanceof Element) | 
| 513 if (node instanceof Element) | 573 numAddedElements++; | 
| 514 numRemovedElements++; | 574 } | 
| 515 } | 575 | 
| 516 | 576 for (let node of mutation.removedNodes) | 
| 517 let changed = false; | 577 { | 
| 518 | 578 if (node instanceof Element) | 
| 519 // Go through all the selectors and find any that match the mutation target. | 579 numRemovedElements++; | 
| 520 let targetSelector = makeSelector(target) + " > "; | 580 } | 
| 521 for (let i = 0; i < selectors.length; i++) | 581 | 
| 522 { | 582 let changed = false; | 
| 523 let selector = selectors[i]; | 583 | 
| 524 if (selector.startsWith(targetSelector)) | 584 // Go through all the selectors and find any that match the mutation target. | 
| 525 { | 585 let targetSelector = makeSelector(target) + " > "; | 
| 526 // If there's a match, extract the one-based child index. | 586 for (let i = 0; i < this.selectors.length; i++) | 
| 527 let subSelector = selector.substring(targetSelector.length); | 587 { | 
| 528 let match = /^[^:]*:nth-child\((\d+)\)/.exec(subSelector); | 588 let selector = this.selectors[i]; | 
| 529 let nthIndex = match ? +match[1] : 0; | 589 if (selector.startsWith(targetSelector)) | 
| 530 | 590 { | 
| 531 // If the child being targeted by the selector is before the mutation | 591 // If there's a match, extract the one-based child index. | 
| 532 // index (i.e. the one-based child index is not affected), do nothing. | 592 let subSelector = selector.substring(targetSelector.length); | 
| 533 if (nthIndex <= index) | 593 let match = /^[^:]*:nth-child\((\d+)\)/.exec(subSelector); | 
| 534 continue; | 594 let nthIndex = match ? +match[1] : 0; | 
| 535 | 595 | 
| 536 nthIndex -= numRemovedElements; | 596 // If the child being targeted by the selector is before the mutation | 
| 537 | 597 // index (i.e. the one-based child index is not affected), do nothing. | 
| 538 // After adjusting for the number of removed elements, if the one-based | 598 if (nthIndex <= index) | 
| 539 // child index appears to be lower than the mutation index, it means the | 599 continue; | 
| 540 // child got removed. In that case we must delete the selector. | 600 | 
| 541 if (nthIndex <= index) | 601 nthIndex -= numRemovedElements; | 
| 542 { | 602 | 
| 543 selectorSet.delete(selector); | 603 // After adjusting for the number of removed elements, if the one-based | 
| 544 | 604 // child index appears to be lower than the mutation index, it means the | 
| 545 selectors.splice(i, 1); | 605 // child got removed. In that case we must delete the selector. | 
| 546 selectorFilters.splice(i, 1); | 606 if (nthIndex <= index) | 
| 547 i--; | 607 { | 
| 548 | 608 this.deleteAt(i--); | 
| 549 changed = true; | 609 changed = true; | 
| 550 | 610 continue; | 
| 551 continue; | 611 } | 
| 612 | |
| 613 nthIndex += numAddedElements; | |
| 614 | |
| 615 // If the one-based child index has changed, we must update the | |
| 616 // selector. | |
| 617 if (nthIndex != +match[1]) | |
| 618 { | |
| 619 this.setAt(i, targetSelector + | |
| 620 subSelector.replace(match[1], nthIndex)); | |
| 621 changed = true; | |
| 622 } | |
| 552 } | 623 } | 
| 553 | 624 } | 
| 554 nthIndex += numAddedElements; | 625 | 
| 555 | 626 return changed; | 
| 556 // If the one-based child index has changed, we must update the selector. | 627 } | 
| 557 if (nthIndex != +match[1]) | 628 }; | 
| 558 { | |
| 559 selectorSet.delete(selector); | |
| 560 selector = targetSelector + subSelector.replace(match[1], nthIndex); | |
| 561 selectorSet.add(selector); | |
| 562 selectors[i] = selector; | |
| 563 | |
| 564 changed = true; | |
| 565 } | |
| 566 } | |
| 567 } | |
| 568 | |
| 569 return changed; | |
| 570 } | |
| 571 | 629 | 
| 572 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 630 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 
| 573 { | 631 { | 
| 574 this.document = document; | 632 this.document = document; | 
| 575 this.addSelectorsFunc = addSelectorsFunc; | 633 this.addSelectorsFunc = addSelectorsFunc; | 
| 576 this.hideElemsFunc = hideElemsFunc; | 634 this.hideElemsFunc = hideElemsFunc; | 
| 577 this.observer = new MutationObserver(this.observe.bind(this)); | 635 this.observer = new MutationObserver(this.observe.bind(this)); | 
| 578 this.useInlineStyles = true; | 636 this.useInlineStyles = true; | 
| 579 this.selectorSet = new Set(); | 637 this.css = new CSSSelectorManager(this.document); | 
| 580 this.selectors = []; | |
| 581 this.selectorFilters = []; | |
| 582 } | 638 } | 
| 583 | 639 | 
| 584 ElemHideEmulation.prototype = { | 640 ElemHideEmulation.prototype = { | 
| 585 isSameOrigin(stylesheet) | 641 isSameOrigin(stylesheet) | 
| 586 { | 642 { | 
| 587 try | 643 try | 
| 588 { | 644 { | 
| 589 return new URL(stylesheet.href).origin == this.document.location.origin; | 645 return new URL(stylesheet.href).origin == this.document.location.origin; | 
| 590 } | 646 } | 
| 591 catch (e) | 647 catch (e) | 
| (...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 728 let processPatterns = () => | 784 let processPatterns = () => | 
| 729 { | 785 { | 
| 730 let cycleStart = performance.now(); | 786 let cycleStart = performance.now(); | 
| 731 | 787 | 
| 732 if (!pattern) | 788 if (!pattern) | 
| 733 { | 789 { | 
| 734 if (!patterns.length) | 790 if (!patterns.length) | 
| 735 { | 791 { | 
| 736 if (selectors.length > 0) | 792 if (selectors.length > 0) | 
| 737 { | 793 { | 
| 738 for (let selector of selectors) | 794 this.css.add(selectors, selectorFilters); | 
| 739 this.selectorSet.add(selector); | 795 this.addSelectorsFunc(this.css.selectors, this.css.filters); | 
| 740 | |
| 741 this.selectors.push(...selectors); | |
| 742 this.selectorFilters.push(...selectorFilters); | |
| 743 | |
| 744 this.addSelectorsFunc(this.selectors, | |
| 745 this.selectorFilters); | |
| 746 } | 796 } | 
| 747 | 797 | 
| 748 if (elements.length > 0) | 798 if (elements.length > 0) | 
| 749 this.hideElemsFunc(elements, elementFilters); | 799 this.hideElemsFunc(elements, elementFilters); | 
| 750 | 800 | 
| 751 if (typeof done == "function") | 801 if (typeof done == "function") | 
| 752 done(); | 802 done(); | 
| 753 return; | 803 return; | 
| 754 } | 804 } | 
| 755 | 805 | 
| 756 pattern = patterns.shift(); | 806 pattern = patterns.shift(); | 
| 757 | 807 | 
| 758 generator = evaluate(pattern.selectors, 0, "", | 808 generator = evaluate(pattern.selectors, 0, "", | 
| 759 this.document, cssStyles); | 809 this.document, cssStyles); | 
| 760 } | 810 } | 
| 761 for (let selector of generator) | 811 for (let selector of generator) | 
| 762 { | 812 { | 
| 763 if (selector != null) | 813 if (selector != null) | 
| 764 { | 814 { | 
| 765 if (!this.useInlineStyles || | 815 if (!this.useInlineStyles || | 
| 766 pattern.isSelectorHidingOnlyPattern()) | 816 pattern.isSelectorHidingOnlyPattern()) | 
| 767 { | 817 { | 
| 768 if (!this.selectorSet.has(selector)) | 818 if (!this.css.has(selector)) | 
| 769 { | 819 { | 
| 770 selectors.push(selector); | 820 selectors.push(selector); | 
| 771 selectorFilters.push(pattern.text); | 821 selectorFilters.push(pattern.text); | 
| 772 } | 822 } | 
| 773 } | 823 } | 
| 774 else | 824 else | 
| 775 { | 825 { | 
| 776 for (let element of this.document.querySelectorAll(selector)) | 826 for (let element of this.document.querySelectorAll(selector)) | 
| 777 { | 827 { | 
| 778 elements.push(element); | 828 elements.push(element); | 
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 892 | 942 | 
| 893 onLoad(event) | 943 onLoad(event) | 
| 894 { | 944 { | 
| 895 let stylesheet = event.target.sheet; | 945 let stylesheet = event.target.sheet; | 
| 896 if (stylesheet) | 946 if (stylesheet) | 
| 897 this.queueFiltering([stylesheet]); | 947 this.queueFiltering([stylesheet]); | 
| 898 }, | 948 }, | 
| 899 | 949 | 
| 900 observe(mutations) | 950 observe(mutations) | 
| 901 { | 951 { | 
| 902 let selectorsChanged = false; | 952 if (this.css.update(mutations)) | 
| 903 for (let mutation of mutations) | 953 this.addSelectorsFunc(this.css.selectors, this.css.filters); | 
| 
hub
2018/03/23 15:54:25
My concern here is that the intent of queueFilteri
 
Manish Jethani
2018/03/23 16:55:33
The thing is that I'm trying to avoid running full
 | |
| 904 { | |
| 905 if (updateSelectorsForMutation(mutation, this.selectorSet, this.selectors, | |
| 906 this.selectorFilters, this.document)) | |
| 907 { | |
| 908 selectorsChanged = true; | |
| 909 } | |
| 910 } | |
| 911 | |
| 912 if (selectorsChanged) | |
| 913 this.addSelectorsFunc(this.selectors, this.selectorFilters); | |
| 914 | 954 | 
| 915 this.queueFiltering(null, mutations); | 955 this.queueFiltering(null, mutations); | 
| 916 }, | 956 }, | 
| 917 | 957 | 
| 918 apply(patterns) | 958 apply(patterns) | 
| 919 { | 959 { | 
| 920 this.patterns = []; | 960 this.patterns = []; | 
| 921 for (let pattern of patterns) | 961 for (let pattern of patterns) | 
| 922 { | 962 { | 
| 923 let selectors = this.parseSelector(pattern.selector); | 963 let selectors = this.parseSelector(pattern.selector); | 
| (...skipping 12 matching lines...) Expand all Loading... | |
| 936 characterData: shouldObserveCharacterData(this.patterns), | 976 characterData: shouldObserveCharacterData(this.patterns), | 
| 937 subtree: true | 977 subtree: true | 
| 938 } | 978 } | 
| 939 ); | 979 ); | 
| 940 this.document.addEventListener("load", this.onLoad.bind(this), true); | 980 this.document.addEventListener("load", this.onLoad.bind(this), true); | 
| 941 } | 981 } | 
| 942 } | 982 } | 
| 943 }; | 983 }; | 
| 944 | 984 | 
| 945 exports.ElemHideEmulation = ElemHideEmulation; | 985 exports.ElemHideEmulation = ElemHideEmulation; | 
| LEFT | RIGHT |