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

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

Issue 29494577: Issue 5438 - Observer DOM changes to reapply filters. (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Updated to the new design Created Aug. 9, 2017, 8:16 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 | no next file » | 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-2017 eyeo GmbH 3 * Copyright (C) 2006-2017 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 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
68 { 68 {
69 let {children} = node.parentNode; 69 let {children} = node.parentNode;
70 for (let i = 0; i < children.length; i++) 70 for (let i = 0; i < children.length; i++)
71 if (children[i] == node) 71 if (children[i] == node)
72 return i + 1; 72 return i + 1;
73 return 0; 73 return 0;
74 } 74 }
75 75
76 function makeSelector(node, selector) 76 function makeSelector(node, selector)
77 { 77 {
78 if (node == null)
79 return null;
78 if (!node.parentElement) 80 if (!node.parentElement)
79 { 81 {
80 let newSelector = ":root"; 82 let newSelector = ":root";
81 if (selector) 83 if (selector)
82 newSelector += " > " + selector; 84 newSelector += " > " + selector;
83 return newSelector; 85 return newSelector;
84 } 86 }
85 let idx = positionInParent(node); 87 let idx = positionInParent(node);
86 if (idx > 0) 88 if (idx > 0)
87 { 89 {
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
157 }; 159 };
158 } 160 }
159 161
160 function* evaluate(chain, index, prefix, subtree, styles) 162 function* evaluate(chain, index, prefix, subtree, styles)
161 { 163 {
162 if (index >= chain.length) 164 if (index >= chain.length)
163 { 165 {
164 yield prefix; 166 yield prefix;
165 return; 167 return;
166 } 168 }
169 if (prefix == null)
Wladimir Palant 2017/08/10 10:12:20 How would we get into this situation? From what I
hub 2017/08/11 16:26:51 Acknowledged.
170 {
171 yield null;
172 return;
173 }
174 let count = 0;
167 for (let [selector, element] of 175 for (let [selector, element] of
168 chain[index].getSelectors(prefix, subtree, styles)) 176 chain[index].getSelectors(prefix, subtree, styles))
177 {
178 if (selector == null)
179 continue;
Wladimir Palant 2017/08/10 10:12:21 getSelectors() generator might be performing non-t
hub 2017/08/11 16:26:50 Acknowledged.
180 count++;
169 yield* evaluate(chain, index + 1, selector, element, styles); 181 yield* evaluate(chain, index + 1, selector, element, styles);
182 }
183 if (count == 0)
184 yield null;
Wladimir Palant 2017/08/10 10:12:22 I don't think we need this counter, doing this unc
hub 2017/08/11 16:26:50 Acknowledged.
170 } 185 }
171 186
172 function PlainSelector(selector) 187 function PlainSelector(selector)
173 { 188 {
174 this._selector = selector; 189 this._selector = selector;
175 } 190 }
176 191
177 PlainSelector.prototype = { 192 PlainSelector.prototype = {
178 /** 193 /**
179 * Generator function returning a pair of selector 194 * Generator function returning a pair of selector
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
214 * Generator function returning selected elements. 229 * Generator function returning selected elements.
215 * @param {string} prefix the prefix for the selector. 230 * @param {string} prefix the prefix for the selector.
216 * @param {Node} subtree the subtree we work on. 231 * @param {Node} subtree the subtree we work on.
217 * @param {StringifiedStyle[]} styles the stringified style objects. 232 * @param {StringifiedStyle[]} styles the stringified style objects.
218 */ 233 */
219 *getElements(prefix, subtree, styles) 234 *getElements(prefix, subtree, styles)
220 { 235 {
221 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 236 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
222 prefix + "*" : prefix; 237 prefix + "*" : prefix;
223 let elements = subtree.querySelectorAll(actualPrefix); 238 let elements = subtree.querySelectorAll(actualPrefix);
239 if (elements.length == 0)
240 yield null;
Wladimir Palant 2017/08/10 10:12:20 We don't need to protect against this scenario mor
hub 2017/08/11 16:26:48 Acknowledged.
224 for (let element of elements) 241 for (let element of elements)
225 { 242 {
243 let count = 0;
226 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 244 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
227 for (let selector of iter) 245 for (let selector of iter)
228 { 246 {
247 count++;
248 if (selector == null)
249 {
250 yield null;
251 continue;
252 }
229 if (relativeSelector.test(selector)) 253 if (relativeSelector.test(selector))
230 selector = ":scope" + selector; 254 selector = ":scope" + selector;
231 if (element.querySelector(selector)) 255 if (element.querySelector(selector))
232 yield element; 256 yield element;
233 } 257 }
258 if (count == 0)
259 yield null;
Wladimir Palant 2017/08/10 10:12:22 Like above, we don't need the counter - an uncondi
hub 2017/08/11 16:26:50 Acknowledged.
234 } 260 }
235 } 261 }
236 }; 262 };
237 263
238 function ContainsSelector(textContent) 264 function ContainsSelector(textContent)
239 { 265 {
240 this._text = textContent; 266 this._text = textContent;
241 } 267 }
242 268
243 ContainsSelector.prototype = { 269 ContainsSelector.prototype = {
244 requiresHiding: true, 270 requiresHiding: true,
245 271
246 *getSelectors(prefix, subtree, stylesheet) 272 *getSelectors(prefix, subtree, stylesheet)
247 { 273 {
248 for (let element of this.getElements(prefix, subtree, stylesheet)) 274 for (let element of this.getElements(prefix, subtree, stylesheet))
249 yield [makeSelector(element, ""), subtree]; 275 yield [makeSelector(element, ""), subtree];
250 }, 276 },
251 277
252 *getElements(prefix, subtree, stylesheet) 278 *getElements(prefix, subtree, stylesheet)
253 { 279 {
254 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 280 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
255 prefix + "*" : prefix; 281 prefix + "*" : prefix;
256 let elements = subtree.querySelectorAll(actualPrefix); 282 let elements = subtree.querySelectorAll(actualPrefix);
257 for (let element of elements) 283 if (elements.length == 0)
258 if (element.textContent.includes(this._text)) 284 yield null;
Wladimir Palant 2017/08/10 10:12:21 We don't need to protect against this scenario mor
hub 2017/08/11 16:26:50 Acknowledged.
259 yield element; 285 else
286 {
287 let count = 0;
288 for (let element of elements)
289 if (element.textContent.includes(this._text))
290 {
291 count++;
292 yield element;
293 }
Wladimir Palant 2017/08/10 10:12:21 We don't need this counter, just make sure to yiel
hub 2017/08/11 16:26:50 Acknowledged.
294 if (count == 0)
295 yield null;
296 }
260 } 297 }
261 }; 298 };
262 299
263 function PropsSelector(propertyExpression) 300 function PropsSelector(propertyExpression)
264 { 301 {
265 let regexpString; 302 let regexpString;
266 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 303 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
267 propertyExpression[propertyExpression.length - 1] == "/") 304 propertyExpression[propertyExpression.length - 1] == "/")
268 { 305 {
269 regexpString = propertyExpression.slice(1, -1) 306 regexpString = propertyExpression.slice(1, -1)
(...skipping 29 matching lines...) Expand all
299 } 336 }
300 }, 337 },
301 338
302 *getSelectors(prefix, subtree, styles) 339 *getSelectors(prefix, subtree, styles)
303 { 340 {
304 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 341 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
305 yield [selector, subtree]; 342 yield [selector, subtree];
306 } 343 }
307 }; 344 };
308 345
346 function isSelectorHidingOnlyPattern(pattern)
Wladimir Palant 2017/08/10 10:12:22 Note that this function is called exactly once, so
hub 2017/08/11 16:26:48 There was a time it was called twice. but that was
347 {
348 return pattern.selectors.some(s => s.preferHideWithSelector) &&
349 !pattern.selectors.some(s => s.requiresHiding);
350 }
351
309 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 352 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
310 hideElemsFunc) 353 hideElemsFunc)
311 { 354 {
312 this.window = window; 355 this.window = window;
313 this.getFiltersFunc = getFiltersFunc; 356 this.getFiltersFunc = getFiltersFunc;
314 this.addSelectorsFunc = addSelectorsFunc; 357 this.addSelectorsFunc = addSelectorsFunc;
315 this.hideElemsFunc = hideElemsFunc; 358 this.hideElemsFunc = hideElemsFunc;
359 this.observer = new window.MutationObserver(this.observe.bind(this));
316 } 360 }
317 361
318 ElemHideEmulation.prototype = { 362 ElemHideEmulation.prototype = {
319 isSameOrigin(stylesheet) 363 isSameOrigin(stylesheet)
320 { 364 {
321 try 365 try
322 { 366 {
323 return new URL(stylesheet.href).origin == this.window.location.origin; 367 return new URL(stylesheet.href).origin == this.window.location.origin;
324 } 368 }
325 catch (e) 369 catch (e)
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
397 441
398 _lastInvocation: 0, 442 _lastInvocation: 0,
399 443
400 /** 444 /**
401 * Processes the current document and applies all rules to it. 445 * Processes the current document and applies all rules to it.
402 * @param {CSSStyleSheet[]} [stylesheets] 446 * @param {CSSStyleSheet[]} [stylesheets]
403 * The list of new stylesheets that have been added to the document and 447 * The list of new stylesheets that have been added to the document and
404 * made reprocessing necessary. This parameter shouldn't be passed in for 448 * made reprocessing necessary. This parameter shouldn't be passed in for
405 * the initial processing, all of document's stylesheets will be considered 449 * the initial processing, all of document's stylesheets will be considered
406 * then and all rules, including the ones not dependent on styles. 450 * then and all rules, including the ones not dependent on styles.
451 * @param {boolean} [domUpdate]
452 * Indicate this is a DOM update.
Wladimir Palant 2017/08/10 10:12:21 Why do we need this parameter? Its purpose seems t
hub 2017/08/11 16:26:51 It doesn't mean "ignore the stylesheets". Both can
453 * @param {function} [done]
454 * Callback to call when done.
407 */ 455 */
408 addSelectors(stylesheets) 456 addSelectors(stylesheets, domUpdate, done)
409 { 457 {
410 this._lastInvocation = Date.now(); 458 this._lastInvocation = Date.now();
Wladimir Palant 2017/08/10 10:12:22 With this method being asynchronous now, I think t
hub 2017/08/11 16:26:48 Ah right, Date.now() isn't monotonic.
411 459
412 let selectors = []; 460 let selectors = [];
413 let selectorFilters = []; 461 let selectorFilters = [];
414 462
415 let elements = []; 463 let elements = [];
416 let elementFilters = []; 464 let elementFilters = [];
417 465
418 let cssStyles = []; 466 let cssStyles = [];
419 467
420 let stylesheetOnlyChange = !!stylesheets; 468 let stylesheetOnlyChange = !!stylesheets && !domUpdate;
421 if (!stylesheets) 469 if (!stylesheets)
422 stylesheets = this.window.document.styleSheets; 470 stylesheets = this.window.document.styleSheets;
423 471
424 // Chrome < 51 doesn't have an iterable StyleSheetList 472 // Chrome < 51 doesn't have an iterable StyleSheetList
425 // https://issues.adblockplus.org/ticket/5381 473 // https://issues.adblockplus.org/ticket/5381
426 for (let i = 0; i < stylesheets.length; i++) 474 for (let i = 0; i < stylesheets.length; i++)
427 { 475 {
428 let stylesheet = stylesheets[i]; 476 let stylesheet = stylesheets[i];
429 // Explicitly ignore third-party stylesheets to ensure consistent behavior 477 // Explicitly ignore third-party stylesheets to ensure consistent behavior
430 // between Firefox and Chrome. 478 // between Firefox and Chrome.
431 if (!this.isSameOrigin(stylesheet)) 479 if (!this.isSameOrigin(stylesheet))
432 continue; 480 continue;
433 481
434 let rules = stylesheet.cssRules; 482 let rules = stylesheet.cssRules;
435 if (!rules) 483 if (!rules)
436 continue; 484 continue;
437 485
438 for (let rule of rules) 486 for (let rule of rules)
439 { 487 {
440 if (rule.type != rule.STYLE_RULE) 488 if (rule.type != rule.STYLE_RULE)
441 continue; 489 continue;
442 490
443 cssStyles.push(stringifyStyle(rule)); 491 cssStyles.push(stringifyStyle(rule));
444 } 492 }
445 } 493 }
446 494
447 let {document} = this.window; 495 let {document} = this.window;
448 for (let pattern of this.patterns) 496
497 let lastCycle = Date.now();
498
499 let processPatterns = function(patternIterator)
449 { 500 {
450 if (stylesheetOnlyChange && 501 for (let pattern of patternIterator)
451 !pattern.selectors.some(selector => selector.dependsOnStyles))
452 { 502 {
453 continue; 503 if (stylesheetOnlyChange &&
504 !pattern.selectors.some(selector => selector.dependsOnStyles))
505 {
506 continue;
507 }
508
509 for (let selector of evaluate(pattern.selectors, 0, "", document,
510 cssStyles))
511 {
512 if (selector == null)
513 continue;
514 if (isSelectorHidingOnlyPattern(pattern))
515 {
516 selectors.push(selector);
517 selectorFilters.push(pattern.text);
518 }
519 else
520 {
521 for (let element of document.querySelectorAll(selector))
522 {
523 elements.push(element);
524 elementFilters.push(pattern.text);
525 }
526 }
527 }
528
529 let now = Date.now();
530 if (now - lastCycle > 50)
Wladimir Palant 2017/08/10 10:12:21 No magic numbers please, you should declare a cons
hub 2017/08/11 16:26:47 Done.
531 {
532 lastCycle = now;
533 this.window.setTimeout(() =>
534 {
535 processPatterns(patternIterator);
536 }, 0);
537 return;
Wladimir Palant 2017/08/10 10:12:20 So, why did we go through all the effort yielding
hub 2017/08/11 16:26:50 Done.
538 }
454 } 539 }
455 540
456 for (let selector of evaluate(pattern.selectors, 541 this.addSelectorsFunc(selectors, selectorFilters);
457 0, "", document, cssStyles)) 542 this.hideElemsFunc(elements, elementFilters);
458 { 543 if (typeof done == "function")
459 if (pattern.selectors.some(s => s.preferHideWithSelector) && 544 done();
460 !pattern.selectors.some(s => s.requiresHiding)) 545 }.bind(this);
Wladimir Palant 2017/08/10 10:12:20 Don't call bind(), use an arrow function instead.
hub 2017/08/11 16:26:51 Done.
461 {
462 selectors.push(selector);
463 selectorFilters.push(pattern.text);
464 }
465 else
466 {
467 for (let element of document.querySelectorAll(selector))
468 {
469 elements.push(element);
470 elementFilters.push(pattern.text);
471 }
472 }
473 }
474 }
475 546
476 this.addSelectorsFunc(selectors, selectorFilters); 547 processPatterns(this.patterns[Symbol.iterator]());
477 this.hideElemsFunc(elements, elementFilters);
478 }, 548 },
479 549
480 _stylesheetQueue: null, 550 _stylesheetQueue: null,
551 _domUpdate: false,
552 _filteringInProgress: false,
553 _nextRun: null,
554
555 /** Filtering reason
Wladimir Palant 2017/08/10 10:12:21 Nit: We usually have /** on a line of its own and
hub 2017/08/11 16:26:47 Acknowledged.
556 * @typedef {Object} FilteringReason
557 * @property {boolean} dom Indicate the DOM changed (tree or attributes)
Wladimir Palant 2017/08/10 10:12:21 I get what this sentence means, but I think that i
hub 2017/08/11 16:26:49 Acknowledged.
558 * @property {CSSStyleSheet[]} [stylesheets]
559 * Indicate the stylesheets that needs refresh
Wladimir Palant 2017/08/10 10:12:22 Nit: need, not needs However, it's not the styles
hub 2017/08/11 16:26:47 Acknowledged.
560 */
561
562 /** Re-run filtering either immediately or queued.
563 * @param {FilteringReason} reason why the filtering must be queued.
564 */
565 queueFiltering(reason)
566 {
567 if (!this._stylesheetQueue &&
568 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
Wladimir Palant 2017/08/10 10:12:20 It seems that we will go into this case if addSele
hub 2017/08/11 16:26:48 Oops. Done.
569 {
570 this._stylesheetQueue = [];
Wladimir Palant 2017/08/10 10:12:22 This should be `reason.stylesheets || []` I guess?
hub 2017/08/11 16:26:51 Done.
571 this._domUpdate = reason.dom;
572 this.window.setTimeout(() =>
573 {
574 this._filteringInProgress = true;
575 let domUpdate = this._domUpdate;
576 this._domUpdate = false;
577 let stylesheets = this._stylesheetQueue;
578 this._stylesheetQueue = null;
579 this.addSelectors(stylesheets, domUpdate, () =>
580 {
581 this._filteringInProgress = false;
582 if (this._nextRun)
583 {
584 let nextReason = this._nextRun;
585 this._nextRun = null;
586 this.queueFiltering(nextReason);
587 }
588 });
589 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
590 }
591 if (this._filteringInProgress)
Wladimir Palant 2017/08/10 10:12:21 This should be `else if` I guess? As it is now, th
hub 2017/08/11 16:26:48 Done.
592 {
593 if (!this._nextRun)
Wladimir Palant 2017/08/10 10:12:22 Why use a new variable here instead of reusing _st
hub 2017/08/11 16:26:47 Done.
hub 2017/08/11 16:26:47 Done.
594 this._nextRun = reason;
595 else
596 {
597 this._nextRun.dom = this._nextRun.dom || reason.dom;
598 if (!this._nextRun.stylesheets)
599 this._nextRun.stylesheets = [];
600 this._nextRun.stylesheets.push(...reason.stylesheets);
Wladimir Palant 2017/08/10 10:12:23 reason.stylesheets is optional, this will fail if
hub 2017/08/11 16:26:47 Acknowledged.
601 }
602 }
603 else if (this._stylesheetQueue)
604 {
605 if (reason.stylesheets)
606 this._stylesheetQueue.push(...reason.stylesheets);
607 this._domUpdate = this._domUpdate || reason.domUpdate;
608 }
609 else
610 {
611 let stylesheets = reason.stylesheets || [];
612 this.addSelectors(stylesheets, reason.domUpdate);
613 }
614 },
481 615
482 onLoad(event) 616 onLoad(event)
483 { 617 {
484 let stylesheet = event.target.sheet; 618 let stylesheet = event.target.sheet;
485 if (stylesheet) 619 if (stylesheet)
620 this.queueFiltering({stylesheets: [stylesheet]});
621 },
622
623 observe(mutations)
624 {
625 let reason = {};
626 reason.dom = true;
627 let stylesheets = [];
628 for (let mutation of mutations)
486 { 629 {
487 if (!this._stylesheetQueue && 630 if (mutation.type == "childList")
Wladimir Palant 2017/08/10 10:12:21 What if the website adds the style element first a
hub 2017/08/11 16:26:47 It might not work. I think we should observe for
488 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
489 { 631 {
490 this._stylesheetQueue = []; 632 for (let added of mutation.addedNodes)
491 this.window.setTimeout(() =>
492 { 633 {
493 let stylesheets = this._stylesheetQueue; 634 if (added.nodeType == this.window.Node.ELEMENT_NODE &&
494 this._stylesheetQueue = null; 635 (added.tagName == "STYLE" || added.tagName == "style") &&
Wladimir Palant 2017/08/10 10:12:22 This should be checking for `added.localName == "s
hub 2017/08/11 16:26:51 Done.
495 this.addSelectors(stylesheets); 636 added.styesheet)
Wladimir Palant 2017/08/10 10:12:22 Typo: added.stylesheet I guess that this code pat
hub 2017/08/11 16:26:49 oops.
496 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); 637 stylesheets.push(added.stylesheet);
638 }
497 } 639 }
498
499 if (this._stylesheetQueue)
500 this._stylesheetQueue.push(stylesheet);
501 else
502 this.addSelectors([stylesheet]);
503 } 640 }
641 if (stylesheets.length > 0)
642 reason.stylesheets = stylesheets;
643 this.queueFiltering(reason);
504 }, 644 },
505 645
506 apply() 646 apply()
507 { 647 {
508 this.getFiltersFunc(patterns => 648 this.getFiltersFunc(patterns =>
509 { 649 {
510 this.patterns = []; 650 this.patterns = [];
511 for (let pattern of patterns) 651 for (let pattern of patterns)
512 { 652 {
513 let selectors = this.parseSelector(pattern.selector); 653 let selectors = this.parseSelector(pattern.selector);
514 if (selectors != null && selectors.length > 0) 654 if (selectors != null && selectors.length > 0)
515 this.patterns.push({selectors, text: pattern.text}); 655 this.patterns.push({selectors, text: pattern.text});
516 } 656 }
517 657
518 if (this.patterns.length > 0) 658 if (this.patterns.length > 0)
519 { 659 {
520 let {document} = this.window; 660 let {document} = this.window;
521 this.addSelectors(); 661 this.addSelectors(null, true, () => {
Wladimir Palant 2017/08/10 10:12:22 If you want queueFiltering() to set important stat
hub 2017/08/11 16:26:47 I'll call queueFiltering() instead. Totally make s
522 document.addEventListener("load", this.onLoad.bind(this), true); 662 this.observer.observe(
663 document,
664 {
665 childList: true,
666 attributes: true,
667 subtree: true,
668 attributeFilter: ["class", "id"]
Wladimir Palant 2017/08/10 10:12:20 We need to observe characterData changes as well.
hub 2017/08/11 16:26:51 I kept them deliberately a bit more restricted as
669 }
670 );
671 document.addEventListener("load", this.onLoad.bind(this), true);
672 });
Wladimir Palant 2017/08/10 10:12:21 The logic here is wrong, we should register the mu
hub 2017/08/11 16:26:50 make sense. Done.
523 } 673 }
524 }); 674 });
525 } 675 }
526 }; 676 };
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld