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

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

Issue 29714601: Issue 6437 - Skip elements not affected by DOM mutations (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Rename debug mode to test mode Created May 18, 2018, 3:41 a.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
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 "use strict"; 18 "use strict";
19 19
20 const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); 20 const {textToRegExp, filterToRegExp, splitSelector} = require("../common");
21 const {indexOf} = require("../coreUtils"); 21 const {indexOf} = require("../coreUtils");
22 22
23 let MIN_INVOCATION_INTERVAL = 3000; 23 let MIN_INVOCATION_INTERVAL = 3000;
24 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; 24 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
25 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 25 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
26 26
27 let testInfo = null;
28
29 function setTestMode()
30 {
31 testInfo = {
32 lastProcessedElements: new Set()
33 };
34 }
35
36 exports.setTestMode = setTestMode;
37
38 function getTestInfo()
39 {
40 return testInfo;
41 }
42
43 exports.getTestInfo = getTestInfo;
hub 2018/05/24 19:19:09 I'm a bit skeptical with that way or test, includi
Manish Jethani 2018/05/25 07:21:25 We can work on improving our test framework, but f
44
27 function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) 45 function getCachedPropertyValue(object, name, defaultValueFunc = () => {})
28 { 46 {
29 let value = object[name]; 47 let value = object[name];
30 if (typeof value == "undefined") 48 if (typeof value == "undefined")
31 Object.defineProperty(object, name, {value: value = defaultValueFunc()}); 49 Object.defineProperty(object, name, {value: value = defaultValueFunc()});
32 return value; 50 return value;
33 } 51 }
34 52
35 /** Return position of node from parent. 53 /** Return position of node from parent.
36 * @param {Node} node the node to find the position of. 54 * @param {Node} node the node to find the position of.
(...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after
192 try 210 try
193 { 211 {
194 return new RegExp(pattern, flags); 212 return new RegExp(pattern, flags);
195 } 213 }
196 catch (e) 214 catch (e)
197 { 215 {
198 } 216 }
199 return null; 217 return null;
200 } 218 }
201 219
202 function* evaluate(chain, index, prefix, subtree, styles) 220 function* evaluate(chain, index, prefix, subtree, styles, targets)
203 { 221 {
204 if (index >= chain.length) 222 if (index >= chain.length)
205 { 223 {
206 yield prefix; 224 yield prefix;
207 return; 225 return;
208 } 226 }
209 for (let [selector, element] of 227 for (let [selector, element] of
210 chain[index].getSelectors(prefix, subtree, styles)) 228 chain[index].getSelectors(prefix, subtree, styles, targets))
211 { 229 {
212 if (selector == null) 230 if (selector == null)
213 yield null; 231 yield null;
214 else 232 else
215 yield* evaluate(chain, index + 1, selector, element, styles); 233 yield* evaluate(chain, index + 1, selector, element, styles, targets);
216 } 234 }
217 // Just in case the getSelectors() generator above had to run some heavy 235 // Just in case the getSelectors() generator above had to run some heavy
218 // document.querySelectorAll() call which didn't produce any results, make 236 // document.querySelectorAll() call which didn't produce any results, make
219 // sure there is at least one point where execution can pause. 237 // sure there is at least one point where execution can pause.
220 yield null; 238 yield null;
221 } 239 }
222 240
223 function PlainSelector(selector) 241 function PlainSelector(selector)
224 { 242 {
225 this._selector = selector; 243 this._selector = selector;
226 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); 244 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector);
227 this.dependsOnDOM = this.maybeDependsOnAttributes; 245 this.dependsOnDOM = this.maybeDependsOnAttributes;
246 this.maybeContainsSiblingCombinators = /[~+]/.test(selector);
228 } 247 }
229 248
230 PlainSelector.prototype = { 249 PlainSelector.prototype = {
231 /** 250 /**
232 * Generator function returning a pair of selector 251 * Generator function returning a pair of selector
233 * string and subtree. 252 * string and subtree.
234 * @param {string} prefix the prefix for the selector. 253 * @param {string} prefix the prefix for the selector.
235 * @param {Node} subtree the subtree we work on. 254 * @param {Node} subtree the subtree we work on.
236 * @param {StringifiedStyle[]} styles the stringified style objects. 255 * @param {StringifiedStyle[]} styles the stringified style objects.
256 * @param {Node[]} [targets] the nodes we are interested in.
237 */ 257 */
238 *getSelectors(prefix, subtree, styles) 258 *getSelectors(prefix, subtree, styles, targets)
239 { 259 {
240 yield [prefix + this._selector, subtree]; 260 yield [prefix + this._selector, subtree];
241 } 261 }
242 }; 262 };
243 263
244 const incompletePrefixRegexp = /[\s>+~]$/; 264 const incompletePrefixRegexp = /[\s>+~]$/;
245 265
246 function HasSelector(selectors) 266 function HasSelector(selectors)
247 { 267 {
248 this._innerSelectors = selectors; 268 this._innerSelectors = selectors;
(...skipping 14 matching lines...) Expand all
263 ); 283 );
264 }, 284 },
265 285
266 get maybeDependsOnAttributes() 286 get maybeDependsOnAttributes()
267 { 287 {
268 return this._innerSelectors.some( 288 return this._innerSelectors.some(
269 selector => selector.maybeDependsOnAttributes 289 selector => selector.maybeDependsOnAttributes
270 ); 290 );
271 }, 291 },
272 292
273 *getSelectors(prefix, subtree, styles) 293 *getSelectors(prefix, subtree, styles, targets)
274 { 294 {
275 for (let element of this.getElements(prefix, subtree, styles)) 295 for (let element of this.getElements(prefix, subtree, styles, targets))
276 yield [makeSelector(element), element]; 296 yield [makeSelector(element), element];
277 }, 297 },
278 298
279 /** 299 /**
280 * Generator function returning selected elements. 300 * Generator function returning selected elements.
281 * @param {string} prefix the prefix for the selector. 301 * @param {string} prefix the prefix for the selector.
282 * @param {Node} subtree the subtree we work on. 302 * @param {Node} subtree the subtree we work on.
283 * @param {StringifiedStyle[]} styles the stringified style objects. 303 * @param {StringifiedStyle[]} styles the stringified style objects.
304 * @param {Node[]} [targets] the nodes we are interested in.
284 */ 305 */
285 *getElements(prefix, subtree, styles) 306 *getElements(prefix, subtree, styles, targets)
286 { 307 {
287 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 308 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
288 prefix + "*" : prefix; 309 prefix + "*" : prefix;
289 let elements = scopedQuerySelectorAll(subtree, actualPrefix); 310 let elements = scopedQuerySelectorAll(subtree, actualPrefix);
290 if (elements) 311 if (elements)
291 { 312 {
292 for (let element of elements) 313 for (let element of elements)
293 { 314 {
294 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 315 // If the element is neither an ancestor nor a descendant of one of the
316 // targets, we can skip it.
317 if (targets && !targets.some(target => element.contains(target) ||
318 target.contains(element)))
319 {
320 yield null;
321 continue;
322 }
323
324 let iter = evaluate(this._innerSelectors, 0, "", element, styles,
325 targets);
295 for (let selector of iter) 326 for (let selector of iter)
296 { 327 {
297 if (selector == null) 328 if (selector == null)
298 yield null; 329 yield null;
299 else if (scopedQuerySelector(element, selector)) 330 else if (scopedQuerySelector(element, selector))
300 yield element; 331 yield element;
301 } 332 }
302 yield null; 333 yield null;
334
335 if (testInfo)
336 testInfo.lastProcessedElements.add(element);
303 } 337 }
304 } 338 }
305 } 339 }
306 }; 340 };
307 341
308 function ContainsSelector(textContent) 342 function ContainsSelector(textContent)
309 { 343 {
310 this._regexp = makeRegExpParameter(textContent); 344 this._regexp = makeRegExpParameter(textContent);
311 } 345 }
312 346
313 ContainsSelector.prototype = { 347 ContainsSelector.prototype = {
314 dependsOnDOM: true, 348 dependsOnDOM: true,
315 dependsOnCharacterData: true, 349 dependsOnCharacterData: true,
316 350
317 *getSelectors(prefix, subtree, styles) 351 *getSelectors(prefix, subtree, styles, targets)
318 { 352 {
319 for (let element of this.getElements(prefix, subtree, styles)) 353 for (let element of this.getElements(prefix, subtree, styles, targets))
320 yield [makeSelector(element), subtree]; 354 yield [makeSelector(element), subtree];
321 }, 355 },
322 356
323 *getElements(prefix, subtree, styles) 357 *getElements(prefix, subtree, styles, targets)
324 { 358 {
325 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 359 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
326 prefix + "*" : prefix; 360 prefix + "*" : prefix;
327 361
328 let elements = scopedQuerySelectorAll(subtree, actualPrefix); 362 let elements = scopedQuerySelectorAll(subtree, actualPrefix);
329 363
330 if (elements) 364 if (elements)
331 { 365 {
332 let lastRoot = null; 366 let lastRoot = null;
333 for (let element of elements) 367 for (let element of elements)
334 { 368 {
335 // For a filter like div:-abp-contains(Hello) and a subtree like 369 // For a filter like div:-abp-contains(Hello) and a subtree like
336 // <div id="a"><div id="b"><div id="c">Hello</div></div></div> 370 // <div id="a"><div id="b"><div id="c">Hello</div></div></div>
337 // we're only interested in div#a 371 // we're only interested in div#a
338 if (lastRoot && lastRoot.contains(element)) 372 if (lastRoot && lastRoot.contains(element))
339 { 373 {
340 yield null; 374 yield null;
341 continue; 375 continue;
342 } 376 }
343 377
344 lastRoot = element; 378 lastRoot = element;
345 379
380 if (targets && !targets.some(target => element.contains(target) ||
381 target.contains(element)))
382 {
383 yield null;
384 continue;
385 }
386
346 if (this._regexp && this._regexp.test(element.textContent)) 387 if (this._regexp && this._regexp.test(element.textContent))
347 yield element; 388 yield element;
348 else 389 else
349 yield null; 390 yield null;
391
392 if (testInfo)
393 testInfo.lastProcessedElements.add(element);
350 } 394 }
351 } 395 }
352 } 396 }
353 }; 397 };
354 398
355 function PropsSelector(propertyExpression) 399 function PropsSelector(propertyExpression)
356 { 400 {
357 let regexpString; 401 let regexpString;
358 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 402 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
359 propertyExpression[propertyExpression.length - 1] == "/") 403 propertyExpression[propertyExpression.length - 1] == "/")
(...skipping 21 matching lines...) Expand all
381 { 425 {
382 subSelector = subSelector.substr(1); 426 subSelector = subSelector.substr(1);
383 } 427 }
384 let idx = subSelector.lastIndexOf("::"); 428 let idx = subSelector.lastIndexOf("::");
385 if (idx != -1) 429 if (idx != -1)
386 subSelector = subSelector.substr(0, idx); 430 subSelector = subSelector.substr(0, idx);
387 yield prefix + subSelector; 431 yield prefix + subSelector;
388 } 432 }
389 }, 433 },
390 434
391 *getSelectors(prefix, subtree, styles) 435 *getSelectors(prefix, subtree, styles, targets)
392 { 436 {
393 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 437 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
394 yield [selector, subtree]; 438 yield [selector, subtree];
395 } 439 }
396 }; 440 };
397 441
398 function Pattern(selectors, text) 442 function Pattern(selectors, text)
399 { 443 {
400 this.selectors = selectors; 444 this.selectors = selectors;
401 this.text = text; 445 this.text = text;
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
447 get dependsOnCharacterData() 491 get dependsOnCharacterData()
448 { 492 {
449 // Observe changes to character data only if there's a contains selector in 493 // Observe changes to character data only if there's a contains selector in
450 // one of the patterns. 494 // one of the patterns.
451 return getCachedPropertyValue( 495 return getCachedPropertyValue(
452 this, "_dependsOnCharacterData", 496 this, "_dependsOnCharacterData",
453 () => this.selectors.some(selector => selector.dependsOnCharacterData) 497 () => this.selectors.some(selector => selector.dependsOnCharacterData)
454 ); 498 );
455 }, 499 },
456 500
501 get maybeContainsSiblingCombinators()
502 {
503 return getCachedPropertyValue(
504 this, "_maybeContainsSiblingCombinators",
505 () => this.selectors.some(selector =>
506 selector.maybeContainsSiblingCombinators)
507 );
508 },
509
457 matchesMutationTypes(mutationTypes) 510 matchesMutationTypes(mutationTypes)
458 { 511 {
459 let mutationTypeMatchMap = getCachedPropertyValue( 512 let mutationTypeMatchMap = getCachedPropertyValue(
460 this, "_mutationTypeMatchMap", 513 this, "_mutationTypeMatchMap",
461 () => new Map([ 514 () => new Map([
462 // All types of DOM-dependent patterns are affected by mutations of 515 // All types of DOM-dependent patterns are affected by mutations of
463 // type "childList". 516 // type "childList".
464 ["childList", true], 517 ["childList", true],
465 ["attributes", this.maybeDependsOnAttributes], 518 ["attributes", this.maybeDependsOnAttributes],
466 ["characterData", this.dependsOnCharacterData] 519 ["characterData", this.dependsOnCharacterData]
(...skipping 20 matching lines...) Expand all
487 540
488 // There are only 3 types of mutations: "attributes", "characterData", and 541 // There are only 3 types of mutations: "attributes", "characterData", and
489 // "childList". 542 // "childList".
490 if (types.size == 3) 543 if (types.size == 3)
491 break; 544 break;
492 } 545 }
493 546
494 return types; 547 return types;
495 } 548 }
496 549
550 function extractMutationTargets(mutations)
551 {
552 if (!mutations)
553 return null;
554
555 let targets = new Set();
556
557 for (let mutation of mutations)
558 {
559 if (mutation.type == "childList")
560 {
561 // When new nodes are added, we're interested in the added nodes rather
562 // than the parent.
563 for (let node of mutation.addedNodes)
564 targets.add(node);
565 }
566 else
567 {
568 targets.add(mutation.target);
569 }
570 }
571
572 return [...targets];
573 }
574
497 function filterPatterns(patterns, {stylesheets, mutations}) 575 function filterPatterns(patterns, {stylesheets, mutations})
498 { 576 {
499 if (!stylesheets && !mutations) 577 if (!stylesheets && !mutations)
500 return patterns.slice(); 578 return patterns.slice();
501 579
502 let mutationTypes = mutations ? extractMutationTypes(mutations) : null; 580 let mutationTypes = mutations ? extractMutationTypes(mutations) : null;
503 581
504 return patterns.filter( 582 return patterns.filter(
505 pattern => (stylesheets && pattern.dependsOnStyles) || 583 pattern => (stylesheets && pattern.dependsOnStyles) ||
506 (mutations && pattern.dependsOnDOM && 584 (mutations && pattern.dependsOnDOM &&
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after
614 * @param {MutationRecord[]} [mutations] 692 * @param {MutationRecord[]} [mutations]
615 * The list of DOM mutations that have been applied to the document and 693 * The list of DOM mutations that have been applied to the document and
616 * made reprocessing necessary. This parameter shouldn't be passed in for 694 * made reprocessing necessary. This parameter shouldn't be passed in for
617 * the initial processing, the entire document will be considered 695 * the initial processing, the entire document will be considered
618 * then and all rules, including the ones not dependent on the DOM. 696 * then and all rules, including the ones not dependent on the DOM.
619 * @param {function} [done] 697 * @param {function} [done]
620 * Callback to call when done. 698 * Callback to call when done.
621 */ 699 */
622 _addSelectors(stylesheets, mutations, done) 700 _addSelectors(stylesheets, mutations, done)
623 { 701 {
702 if (testInfo)
703 testInfo.lastProcessedElements.clear();
704
624 let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); 705 let patterns = filterPatterns(this.patterns, {stylesheets, mutations});
625 706
626 let selectors = []; 707 let selectors = [];
627 let selectorFilters = []; 708 let selectorFilters = [];
628 709
629 let elements = []; 710 let elements = [];
630 let elementFilters = []; 711 let elementFilters = [];
631 712
632 let cssStyles = []; 713 let cssStyles = [];
633 714
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
671 752
672 for (let rule of rules) 753 for (let rule of rules)
673 { 754 {
674 if (rule.type != rule.STYLE_RULE) 755 if (rule.type != rule.STYLE_RULE)
675 continue; 756 continue;
676 757
677 cssStyles.push(stringifyStyle(rule)); 758 cssStyles.push(stringifyStyle(rule));
678 } 759 }
679 } 760 }
680 761
762 let targets = extractMutationTargets(mutations);
763
681 let pattern = null; 764 let pattern = null;
682 let generator = null; 765 let generator = null;
683 766
684 let processPatterns = () => 767 let processPatterns = () =>
685 { 768 {
686 let cycleStart = performance.now(); 769 let cycleStart = performance.now();
687 770
688 if (!pattern) 771 if (!pattern)
689 { 772 {
690 if (!patterns.length) 773 if (!patterns.length)
691 { 774 {
692 if (selectors.length > 0) 775 if (selectors.length > 0)
693 this.addSelectorsFunc(selectors, selectorFilters); 776 this.addSelectorsFunc(selectors, selectorFilters);
694 if (elements.length > 0) 777 if (elements.length > 0)
695 this.hideElemsFunc(elements, elementFilters); 778 this.hideElemsFunc(elements, elementFilters);
696 if (typeof done == "function") 779 if (typeof done == "function")
697 done(); 780 done();
698 return; 781 return;
699 } 782 }
700 783
701 pattern = patterns.shift(); 784 pattern = patterns.shift();
702 785
786 let evaluationTargets = targets;
787
788 // If the pattern appears to contain any sibling combinators, we can't
789 // easily optimize based on the mutation targets. Since this is a
790 // special case, skip the optimization. By setting it to null here we
791 // make sure we process the entire DOM.
792 if (pattern.maybeContainsSiblingCombinators)
793 evaluationTargets = null;
794
795 // Ignore mutation targets when using style sheets, because we may have
796 // to update all the CSS selectors.
797 if (!this.useInlineStyles)
798 evaluationTargets = null;
799
703 generator = evaluate(pattern.selectors, 0, "", 800 generator = evaluate(pattern.selectors, 0, "",
704 this.document, cssStyles); 801 this.document, cssStyles, evaluationTargets);
705 } 802 }
706 for (let selector of generator) 803 for (let selector of generator)
707 { 804 {
708 if (selector != null) 805 if (selector != null)
709 { 806 {
710 if (!this.useInlineStyles) 807 if (!this.useInlineStyles)
711 { 808 {
712 selectors.push(selector); 809 selectors.push(selector);
713 selectorFilters.push(pattern.text); 810 selectorFilters.push(pattern.text);
714 } 811 }
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
834 931
835 onLoad(event) 932 onLoad(event)
836 { 933 {
837 let stylesheet = event.target.sheet; 934 let stylesheet = event.target.sheet;
838 if (stylesheet) 935 if (stylesheet)
839 this.queueFiltering([stylesheet]); 936 this.queueFiltering([stylesheet]);
840 }, 937 },
841 938
842 observe(mutations) 939 observe(mutations)
843 { 940 {
941 if (testInfo)
942 {
943 // In test mode, filter out any mutations likely done by us
944 // (i.e. style="display: none !important"). This makes it easier to
945 // observe how the code responds to DOM mutations.
946 mutations = mutations.filter(
947 ({type, attributeName, target: {style: newValue}, oldValue}) =>
948 !(type == "attributes" && attributeName == "style" &&
949 newValue.display == "none" && oldValue.display != "none")
950 );
951
952 if (mutations.length == 0)
953 return;
954 }
hub 2018/05/24 19:19:09 ... and this :-/ There should be a better way to
Manish Jethani 2018/05/25 07:21:25 OK, do you have any suggestions? This is only done
955
844 this.queueFiltering(null, mutations); 956 this.queueFiltering(null, mutations);
845 }, 957 },
846 958
847 apply(patterns) 959 apply(patterns)
848 { 960 {
849 this.patterns = []; 961 this.patterns = [];
850 for (let pattern of patterns) 962 for (let pattern of patterns)
851 { 963 {
852 let selectors = this.parseSelector(pattern.selector); 964 let selectors = this.parseSelector(pattern.selector);
853 if (selectors != null && selectors.length > 0) 965 if (selectors != null && selectors.length > 0)
(...skipping 11 matching lines...) Expand all
865 characterData: shouldObserveCharacterData(this.patterns), 977 characterData: shouldObserveCharacterData(this.patterns),
866 subtree: true 978 subtree: true
867 } 979 }
868 ); 980 );
869 this.document.addEventListener("load", this.onLoad.bind(this), true); 981 this.document.addEventListener("load", this.onLoad.bind(this), true);
870 } 982 }
871 } 983 }
872 }; 984 };
873 985
874 exports.ElemHideEmulation = ElemHideEmulation; 986 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