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 patch from review Created Aug. 11, 2017, 4:24 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
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 /* globals filterToRegExp */ 18 /* globals filterToRegExp */
19 19
20 "use strict"; 20 "use strict";
21 21
22 const MIN_INVOCATION_INTERVAL = 3000; 22 const MIN_INVOCATION_INTERVAL = 3000;
23 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 24 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
24 25
25 function splitSelector(selector) 26 function splitSelector(selector)
26 { 27 {
27 if (selector.indexOf(",") == -1) 28 if (selector.indexOf(",") == -1)
28 return [selector]; 29 return [selector];
29 30
30 let selectors = []; 31 let selectors = [];
31 let start = 0; 32 let start = 0;
32 let level = 0; 33 let level = 0;
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
68 { 69 {
69 let {children} = node.parentNode; 70 let {children} = node.parentNode;
70 for (let i = 0; i < children.length; i++) 71 for (let i = 0; i < children.length; i++)
71 if (children[i] == node) 72 if (children[i] == node)
72 return i + 1; 73 return i + 1;
73 return 0; 74 return 0;
74 } 75 }
75 76
76 function makeSelector(node, selector) 77 function makeSelector(node, selector)
77 { 78 {
79 if (node == null)
80 return null;
78 if (!node.parentElement) 81 if (!node.parentElement)
79 { 82 {
80 let newSelector = ":root"; 83 let newSelector = ":root";
81 if (selector) 84 if (selector)
82 newSelector += " > " + selector; 85 newSelector += " > " + selector;
83 return newSelector; 86 return newSelector;
84 } 87 }
85 let idx = positionInParent(node); 88 let idx = positionInParent(node);
86 if (idx > 0) 89 if (idx > 0)
87 { 90 {
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
157 }; 160 };
158 } 161 }
159 162
160 function* evaluate(chain, index, prefix, subtree, styles) 163 function* evaluate(chain, index, prefix, subtree, styles)
161 { 164 {
162 if (index >= chain.length) 165 if (index >= chain.length)
163 { 166 {
164 yield prefix; 167 yield prefix;
165 return; 168 return;
166 } 169 }
170 let count = 0;
167 for (let [selector, element] of 171 for (let [selector, element] of
168 chain[index].getSelectors(prefix, subtree, styles)) 172 chain[index].getSelectors(prefix, subtree, styles))
169 yield* evaluate(chain, index + 1, selector, element, styles); 173 {
174 count++;
175 if (selector == null)
176 yield null;
177 else
178 yield* evaluate(chain, index + 1, selector, element, styles);
179 }
180 // Just in case the getSelectors() generator above had to run some heavy
181 // document.querySelectorAll() call which didn't produce any results, make
182 // sure there is at least one point where execution can pause.
183 if (count == 0)
184 yield null;
Wladimir Palant 2017/08/15 11:40:11 I still don't think we need that counter - we can
hub 2017/08/15 16:39:09 Done.
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 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
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);
224 for (let element of elements) 239 for (let element of elements)
225 { 240 {
226 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 241 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
227 for (let selector of iter) 242 for (let selector of iter)
228 { 243 {
244 if (selector == null)
245 {
246 yield null;
247 continue;
248 }
229 if (relativeSelector.test(selector)) 249 if (relativeSelector.test(selector))
230 selector = ":scope" + selector; 250 selector = ":scope" + selector;
231 if (element.querySelector(selector)) 251 if (element.querySelector(selector))
232 yield element; 252 yield element;
233 } 253 }
254 yield null;
234 } 255 }
235 } 256 }
236 }; 257 };
237 258
238 function ContainsSelector(textContent) 259 function ContainsSelector(textContent)
239 { 260 {
240 this._text = textContent; 261 this._text = textContent;
241 } 262 }
242 263
243 ContainsSelector.prototype = { 264 ContainsSelector.prototype = {
244 requiresHiding: true, 265 requiresHiding: true,
245 266
246 *getSelectors(prefix, subtree, stylesheet) 267 *getSelectors(prefix, subtree, stylesheet)
247 { 268 {
248 for (let element of this.getElements(prefix, subtree, stylesheet)) 269 for (let element of this.getElements(prefix, subtree, stylesheet))
249 yield [makeSelector(element, ""), subtree]; 270 yield [makeSelector(element, ""), subtree];
250 }, 271 },
251 272
252 *getElements(prefix, subtree, stylesheet) 273 *getElements(prefix, subtree, stylesheet)
253 { 274 {
254 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 275 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
255 prefix + "*" : prefix; 276 prefix + "*" : prefix;
256 let elements = subtree.querySelectorAll(actualPrefix); 277 let elements = subtree.querySelectorAll(actualPrefix);
278
257 for (let element of elements) 279 for (let element of elements)
280 {
258 if (element.textContent.includes(this._text)) 281 if (element.textContent.includes(this._text))
259 yield element; 282 yield element;
283 else
284 yield null;
285 }
260 } 286 }
261 }; 287 };
262 288
263 function PropsSelector(propertyExpression) 289 function PropsSelector(propertyExpression)
264 { 290 {
265 let regexpString; 291 let regexpString;
266 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 292 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
267 propertyExpression[propertyExpression.length - 1] == "/") 293 propertyExpression[propertyExpression.length - 1] == "/")
268 { 294 {
269 regexpString = propertyExpression.slice(1, -1) 295 regexpString = propertyExpression.slice(1, -1)
(...skipping 29 matching lines...) Expand all
299 } 325 }
300 }, 326 },
301 327
302 *getSelectors(prefix, subtree, styles) 328 *getSelectors(prefix, subtree, styles)
303 { 329 {
304 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 330 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
305 yield [selector, subtree]; 331 yield [selector, subtree];
306 } 332 }
307 }; 333 };
308 334
335 function isSelectorHidingOnlyPattern(pattern)
336 {
337 return pattern.selectors.some(s => s.preferHideWithSelector) &&
338 !pattern.selectors.some(s => s.requiresHiding);
339 }
340
309 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 341 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
310 hideElemsFunc) 342 hideElemsFunc)
311 { 343 {
312 this.window = window; 344 this.window = window;
313 this.getFiltersFunc = getFiltersFunc; 345 this.getFiltersFunc = getFiltersFunc;
314 this.addSelectorsFunc = addSelectorsFunc; 346 this.addSelectorsFunc = addSelectorsFunc;
315 this.hideElemsFunc = hideElemsFunc; 347 this.hideElemsFunc = hideElemsFunc;
348 this.observer = new window.MutationObserver(this.observe.bind(this));
316 } 349 }
317 350
318 ElemHideEmulation.prototype = { 351 ElemHideEmulation.prototype = {
319 isSameOrigin(stylesheet) 352 isSameOrigin(stylesheet)
320 { 353 {
321 try 354 try
322 { 355 {
323 return new URL(stylesheet.href).origin == this.window.location.origin; 356 return new URL(stylesheet.href).origin == this.window.location.origin;
324 } 357 }
325 catch (e) 358 catch (e)
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
397 430
398 _lastInvocation: 0, 431 _lastInvocation: 0,
399 432
400 /** 433 /**
401 * Processes the current document and applies all rules to it. 434 * Processes the current document and applies all rules to it.
402 * @param {CSSStyleSheet[]} [stylesheets] 435 * @param {CSSStyleSheet[]} [stylesheets]
403 * The list of new stylesheets that have been added to the document and 436 * 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 437 * made reprocessing necessary. This parameter shouldn't be passed in for
405 * the initial processing, all of document's stylesheets will be considered 438 * the initial processing, all of document's stylesheets will be considered
406 * then and all rules, including the ones not dependent on styles. 439 * then and all rules, including the ones not dependent on styles.
440 * @param {function} [done]
441 * Callback to call when done.
407 */ 442 */
408 addSelectors(stylesheets) 443 addSelectors(stylesheets, done)
Wladimir Palant 2017/08/15 11:40:10 Please rename into _addSelectors() - this is a pri
hub 2017/08/15 16:39:09 Done.
409 { 444 {
410 this._lastInvocation = Date.now(); 445 this._lastInvocation = Date.now();
Wladimir Palant 2017/08/15 11:40:11 This still needs to be set when addSelectors is do
hub 2017/08/15 16:39:09 Done.
411 446
412 let selectors = []; 447 let selectors = [];
413 let selectorFilters = []; 448 let selectorFilters = [];
414 449
415 let elements = []; 450 let elements = [];
416 let elementFilters = []; 451 let elementFilters = [];
417 452
418 let cssStyles = []; 453 let cssStyles = [];
419 454
420 let stylesheetOnlyChange = !!stylesheets;
421 if (!stylesheets) 455 if (!stylesheets)
Wladimir Palant 2017/08/15 11:40:11 Please put the stylesheetOnlyChange logic back, it
hub 2017/08/15 16:39:10 Done.
422 stylesheets = this.window.document.styleSheets; 456 stylesheets = this.window.document.styleSheets;
423 457
424 // Chrome < 51 doesn't have an iterable StyleSheetList 458 // Chrome < 51 doesn't have an iterable StyleSheetList
425 // https://issues.adblockplus.org/ticket/5381 459 // https://issues.adblockplus.org/ticket/5381
426 for (let i = 0; i < stylesheets.length; i++) 460 for (let i = 0; i < stylesheets.length; i++)
427 { 461 {
428 let stylesheet = stylesheets[i]; 462 let stylesheet = stylesheets[i];
429 // Explicitly ignore third-party stylesheets to ensure consistent behavior 463 // Explicitly ignore third-party stylesheets to ensure consistent behavior
430 // between Firefox and Chrome. 464 // between Firefox and Chrome.
431 if (!this.isSameOrigin(stylesheet)) 465 if (!this.isSameOrigin(stylesheet))
432 continue; 466 continue;
433 467
434 let rules = stylesheet.cssRules; 468 let rules = stylesheet.cssRules;
435 if (!rules) 469 if (!rules)
436 continue; 470 continue;
437 471
438 for (let rule of rules) 472 for (let rule of rules)
439 { 473 {
440 if (rule.type != rule.STYLE_RULE) 474 if (rule.type != rule.STYLE_RULE)
441 continue; 475 continue;
442 476
443 cssStyles.push(stringifyStyle(rule)); 477 cssStyles.push(stringifyStyle(rule));
444 } 478 }
445 } 479 }
446 480
447 let {document} = this.window; 481 let {document} = this.window;
448 for (let pattern of this.patterns) 482
483 let patterns = this.patterns.slice();
484 let pattern = null;
485 let generator = null;
486
487 let processPatterns = () =>
449 { 488 {
450 if (stylesheetOnlyChange && 489 let cycleStart = Date.now();
451 !pattern.selectors.some(selector => selector.dependsOnStyles)) 490
491 if (!pattern)
452 { 492 {
453 continue; 493 if (!patterns.length)
494 {
495 this.addSelectorsFunc(selectors, selectorFilters);
496 this.hideElemsFunc(elements, elementFilters);
497 if (typeof done == "function")
498 done();
499 return;
500 }
501
502 pattern = patterns.shift();
503 generator = evaluate(pattern.selectors, 0, "", document, cssStyles);
454 } 504 }
455 505 for (let selector of generator)
456 for (let selector of evaluate(pattern.selectors,
457 0, "", document, cssStyles))
458 { 506 {
459 if (pattern.selectors.some(s => s.preferHideWithSelector) && 507 if (selector != null)
460 !pattern.selectors.some(s => s.requiresHiding))
461 { 508 {
462 selectors.push(selector); 509 if (isSelectorHidingOnlyPattern(pattern))
463 selectorFilters.push(pattern.text);
464 }
465 else
466 {
467 for (let element of document.querySelectorAll(selector))
468 { 510 {
469 elements.push(element); 511 selectors.push(selector);
470 elementFilters.push(pattern.text); 512 selectorFilters.push(pattern.text);
513 }
514 else
515 {
516 for (let element of document.querySelectorAll(selector))
517 {
518 elements.push(element);
519 elementFilters.push(pattern.text);
520 }
471 } 521 }
472 } 522 }
473 } 523 }
474 } 524 if (Date.now() - cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
Wladimir Palant 2017/08/15 11:40:11 This check needs to be inside the loop above, so t
hub 2017/08/15 16:39:09 Done.
525 {
526 this.window.setTimeout(processPatterns, 0);
527 return;
528 }
529 pattern = null;
530 return processPatterns();
531 };
475 532
476 this.addSelectorsFunc(selectors, selectorFilters); 533 processPatterns();
477 this.hideElemsFunc(elements, elementFilters);
478 }, 534 },
479 535
480 _stylesheetQueue: null, 536 _filteringInProgress: false,
537 _scheduledProcessing: null,
538
539 /**
540 * Filtering reason
541 * @typedef {Object} FilteringReason
542 * @property {CSSStyleSheet[]} [stylesheets]
543 * The list of new stylesheets that have been added to the document
544 * and made reprocessing necessary.
545 */
546
547 /**
548 * Re-run filtering either immediately or queued.
549 * @param {FilteringReason} reason why the filtering must be queued.
Wladimir Palant 2017/08/15 11:40:11 We don't really need this to be a FilteringReason
hub 2017/08/15 16:39:09 Done.
550 */
551 queueFiltering(reason)
552 {
553 let completion = () =>
554 {
555 this._filteringInProgress = false;
556 if (this._scheduledProcessing)
557 {
558 let nextReason = this._scheduledProcessing;
559 this._scheduledProcessing = null;
560 this.queueFiltering(nextReason);
561 }
562 };
563
564 if (this._scheduledProcessing)
565 {
566 if (reason.stylesheets)
567 {
568 if (this._scheduledProcessing.stylesheets)
569 this._scheduledProcessing.stylesheets.push(...reason.stylesheets);
570 else
571 this._scheduledProcessing.stylesheets = reason.stylesheets;
572 }
Wladimir Palant 2017/08/15 11:40:11 This logic is wrong. this._scheduledProcessing.sty
hub 2017/08/15 16:39:10 Done.
573 }
574 else if (this._filteringInProgress)
575 {
576 this._scheduledProcessing = reason;
577 }
578 else if (Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
579 {
580 this._scheduledProcessing = reason;
581 this.window.setTimeout(() =>
582 {
583 let stylesheets = this._scheduledProcessing.stylesheets || [];
Wladimir Palant 2017/08/15 11:40:11 The `|| []` part here means that we will never do
hub 2017/08/15 16:39:10 Done.
584 this._filteringInProgress = true;
585 this._scheduledProcessing = null;
586 this.addSelectors(stylesheets, completion);
587 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
588 }
589 else
590 {
591 this._filteringInProgress = true;
592 this.addSelectors(reason.stylesheets, completion);
593 }
594 },
481 595
482 onLoad(event) 596 onLoad(event)
483 { 597 {
484 let stylesheet = event.target.sheet; 598 let stylesheet = event.target.sheet;
485 if (stylesheet) 599 if (stylesheet)
600 this.queueFiltering({stylesheets: [stylesheet]});
601 },
602
603 observe(mutations)
604 {
605 let reason = {};
606 let stylesheets = [];
Wladimir Palant 2017/08/15 11:40:11 stylesheets should always be null for DOM modifica
hub 2017/08/15 16:39:10 which is why line 626, we check for the array leng
607 for (let mutation of mutations)
486 { 608 {
487 if (!this._stylesheetQueue && 609 if (mutation.type == "childList")
488 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
489 { 610 {
490 this._stylesheetQueue = []; 611 for (let added of mutation.addedNodes)
491 this.window.setTimeout(() =>
492 { 612 {
493 let stylesheets = this._stylesheetQueue; 613 if (added instanceof this.window.HTMLStyleElement &&
494 this._stylesheetQueue = null; 614 added.stylesheet)
Wladimir Palant 2017/08/15 11:40:10 Please drop that special logic. A new <style> elem
hub 2017/08/15 16:39:09 Done.
495 this.addSelectors(stylesheets); 615 stylesheets.push(added.stylesheet);
496 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); 616 }
497 } 617 }
498 618 else if (mutation.type == "characterData")
499 if (this._stylesheetQueue) 619 {
500 this._stylesheetQueue.push(stylesheet); 620 let element = mutation.target.parentElement;
501 else 621 if (element instanceof this.window.HTMLStyleElement &&
502 this.addSelectors([stylesheet]); 622 element.stylesheet)
623 stylesheets.push(element.stylesheet);
624 }
503 } 625 }
626 if (stylesheets.length > 0)
627 reason.stylesheets = stylesheets;
628 this.queueFiltering(reason);
504 }, 629 },
505 630
506 apply() 631 apply()
507 { 632 {
508 this.getFiltersFunc(patterns => 633 this.getFiltersFunc(patterns =>
509 { 634 {
510 this.patterns = []; 635 this.patterns = [];
511 for (let pattern of patterns) 636 for (let pattern of patterns)
512 { 637 {
513 let selectors = this.parseSelector(pattern.selector); 638 let selectors = this.parseSelector(pattern.selector);
514 if (selectors != null && selectors.length > 0) 639 if (selectors != null && selectors.length > 0)
515 this.patterns.push({selectors, text: pattern.text}); 640 this.patterns.push({selectors, text: pattern.text});
516 } 641 }
517 642
518 if (this.patterns.length > 0) 643 if (this.patterns.length > 0)
519 { 644 {
520 let {document} = this.window; 645 let {document} = this.window;
521 this.addSelectors(); 646 this.queueFiltering({});
647 this.observer.observe(
648 document,
649 {
650 childList: true,
651 attributes: true,
652 characterData: true,
653 subtree: true
654 }
655 );
522 document.addEventListener("load", this.onLoad.bind(this), true); 656 document.addEventListener("load", this.onLoad.bind(this), true);
523 } 657 }
524 }); 658 });
525 } 659 }
526 }; 660 };
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