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: Now using performance.now() Created Aug. 15, 2017, 11:31 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 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
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 }
167 for (let [selector, element] of 170 for (let [selector, element] of
168 chain[index].getSelectors(prefix, subtree, styles)) 171 chain[index].getSelectors(prefix, subtree, styles))
169 yield* evaluate(chain, index + 1, selector, element, styles); 172 {
173 if (selector == null)
174 yield null;
175 else
176 yield* evaluate(chain, index + 1, selector, element, styles);
177 }
178 // Just in case the getSelectors() generator above had to run some heavy
179 // document.querySelectorAll() call which didn't produce any results, make
180 // sure there is at least one point where execution can pause.
181 yield null;
170 } 182 }
171 183
172 function PlainSelector(selector) 184 function PlainSelector(selector)
173 { 185 {
174 this._selector = selector; 186 this._selector = selector;
175 } 187 }
176 188
177 PlainSelector.prototype = { 189 PlainSelector.prototype = {
178 /** 190 /**
179 * Generator function returning a pair of selector 191 * Generator function returning a pair of selector
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
219 *getElements(prefix, subtree, styles) 231 *getElements(prefix, subtree, styles)
220 { 232 {
221 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 233 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
222 prefix + "*" : prefix; 234 prefix + "*" : prefix;
223 let elements = subtree.querySelectorAll(actualPrefix); 235 let elements = subtree.querySelectorAll(actualPrefix);
224 for (let element of elements) 236 for (let element of elements)
225 { 237 {
226 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 238 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
227 for (let selector of iter) 239 for (let selector of iter)
228 { 240 {
241 if (selector == null)
242 {
243 yield null;
244 continue;
245 }
229 if (relativeSelector.test(selector)) 246 if (relativeSelector.test(selector))
230 selector = ":scope" + selector; 247 selector = ":scope" + selector;
231 if (element.querySelector(selector)) 248 if (element.querySelector(selector))
232 yield element; 249 yield element;
233 } 250 }
251 yield null;
234 } 252 }
235 } 253 }
236 }; 254 };
237 255
238 function ContainsSelector(textContent) 256 function ContainsSelector(textContent)
239 { 257 {
240 this._text = textContent; 258 this._text = textContent;
241 } 259 }
242 260
243 ContainsSelector.prototype = { 261 ContainsSelector.prototype = {
244 requiresHiding: true, 262 requiresHiding: true,
245 263
246 *getSelectors(prefix, subtree, stylesheet) 264 *getSelectors(prefix, subtree, stylesheet)
247 { 265 {
248 for (let element of this.getElements(prefix, subtree, stylesheet)) 266 for (let element of this.getElements(prefix, subtree, stylesheet))
249 yield [makeSelector(element, ""), subtree]; 267 yield [makeSelector(element, ""), subtree];
250 }, 268 },
251 269
252 *getElements(prefix, subtree, stylesheet) 270 *getElements(prefix, subtree, stylesheet)
253 { 271 {
254 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 272 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
255 prefix + "*" : prefix; 273 prefix + "*" : prefix;
256 let elements = subtree.querySelectorAll(actualPrefix); 274 let elements = subtree.querySelectorAll(actualPrefix);
275
257 for (let element of elements) 276 for (let element of elements)
277 {
258 if (element.textContent.includes(this._text)) 278 if (element.textContent.includes(this._text))
259 yield element; 279 yield element;
280 else
281 yield null;
282 }
260 } 283 }
261 }; 284 };
262 285
263 function PropsSelector(propertyExpression) 286 function PropsSelector(propertyExpression)
264 { 287 {
265 let regexpString; 288 let regexpString;
266 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 289 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
267 propertyExpression[propertyExpression.length - 1] == "/") 290 propertyExpression[propertyExpression.length - 1] == "/")
268 { 291 {
269 regexpString = propertyExpression.slice(1, -1) 292 regexpString = propertyExpression.slice(1, -1)
(...skipping 29 matching lines...) Expand all
299 } 322 }
300 }, 323 },
301 324
302 *getSelectors(prefix, subtree, styles) 325 *getSelectors(prefix, subtree, styles)
303 { 326 {
304 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 327 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
305 yield [selector, subtree]; 328 yield [selector, subtree];
306 } 329 }
307 }; 330 };
308 331
332 function isSelectorHidingOnlyPattern(pattern)
333 {
334 return pattern.selectors.some(s => s.preferHideWithSelector) &&
335 !pattern.selectors.some(s => s.requiresHiding);
336 }
337
309 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 338 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
310 hideElemsFunc) 339 hideElemsFunc)
311 { 340 {
312 this.window = window; 341 this.window = window;
313 this.getFiltersFunc = getFiltersFunc; 342 this.getFiltersFunc = getFiltersFunc;
314 this.addSelectorsFunc = addSelectorsFunc; 343 this.addSelectorsFunc = addSelectorsFunc;
315 this.hideElemsFunc = hideElemsFunc; 344 this.hideElemsFunc = hideElemsFunc;
345 this.observer = new window.MutationObserver(this.observe.bind(this));
316 } 346 }
317 347
318 ElemHideEmulation.prototype = { 348 ElemHideEmulation.prototype = {
319 isSameOrigin(stylesheet) 349 isSameOrigin(stylesheet)
320 { 350 {
321 try 351 try
322 { 352 {
323 return new URL(stylesheet.href).origin == this.window.location.origin; 353 return new URL(stylesheet.href).origin == this.window.location.origin;
324 } 354 }
325 catch (e) 355 catch (e)
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
397 427
398 _lastInvocation: 0, 428 _lastInvocation: 0,
399 429
400 /** 430 /**
401 * Processes the current document and applies all rules to it. 431 * Processes the current document and applies all rules to it.
402 * @param {CSSStyleSheet[]} [stylesheets] 432 * @param {CSSStyleSheet[]} [stylesheets]
403 * The list of new stylesheets that have been added to the document and 433 * 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 434 * made reprocessing necessary. This parameter shouldn't be passed in for
405 * the initial processing, all of document's stylesheets will be considered 435 * the initial processing, all of document's stylesheets will be considered
406 * then and all rules, including the ones not dependent on styles. 436 * then and all rules, including the ones not dependent on styles.
437 * @param {function} [done]
438 * Callback to call when done.
407 */ 439 */
408 addSelectors(stylesheets) 440 _addSelectors(stylesheets, done)
409 { 441 {
410 this._lastInvocation = Date.now();
411
412 let selectors = []; 442 let selectors = [];
413 let selectorFilters = []; 443 let selectorFilters = [];
414 444
415 let elements = []; 445 let elements = [];
416 let elementFilters = []; 446 let elementFilters = [];
417 447
418 let cssStyles = []; 448 let cssStyles = [];
419 449
420 let stylesheetOnlyChange = !!stylesheets; 450 let stylesheetOnlyChange = !!stylesheets;
421 if (!stylesheets) 451 if (!stylesheets)
(...skipping 16 matching lines...) Expand all
438 for (let rule of rules) 468 for (let rule of rules)
439 { 469 {
440 if (rule.type != rule.STYLE_RULE) 470 if (rule.type != rule.STYLE_RULE)
441 continue; 471 continue;
442 472
443 cssStyles.push(stringifyStyle(rule)); 473 cssStyles.push(stringifyStyle(rule));
444 } 474 }
445 } 475 }
446 476
447 let {document} = this.window; 477 let {document} = this.window;
448 for (let pattern of this.patterns) 478
479 let patterns = this.patterns.slice();
480 let pattern = null;
481 let generator = null;
482
483 let processPatterns = () =>
449 { 484 {
450 if (stylesheetOnlyChange && 485 let cycleStart = this.window.performance.now();
451 !pattern.selectors.some(selector => selector.dependsOnStyles)) 486
487 if (!pattern)
452 { 488 {
453 continue; 489 if (!patterns.length)
490 {
491 this.addSelectorsFunc(selectors, selectorFilters);
492 this.hideElemsFunc(elements, elementFilters);
493 if (typeof done == "function")
494 done();
495 return;
496 }
497
498 pattern = patterns.shift();
499
500 if (stylesheetOnlyChange &&
501 !pattern.selectors.some(selector => selector.dependsOnStyles))
502 {
503 pattern = null;
504 return processPatterns();
505 }
506 generator = evaluate(pattern.selectors, 0, "", document, cssStyles);
454 } 507 }
455 508 for (let selector of generator)
456 for (let selector of evaluate(pattern.selectors,
457 0, "", document, cssStyles))
458 { 509 {
459 if (pattern.selectors.some(s => s.preferHideWithSelector) && 510 if (selector != null)
460 !pattern.selectors.some(s => s.requiresHiding))
461 { 511 {
462 selectors.push(selector); 512 if (isSelectorHidingOnlyPattern(pattern))
463 selectorFilters.push(pattern.text);
464 }
465 else
466 {
467 for (let element of document.querySelectorAll(selector))
468 { 513 {
469 elements.push(element); 514 selectors.push(selector);
470 elementFilters.push(pattern.text); 515 selectorFilters.push(pattern.text);
516 }
517 else
518 {
519 for (let element of document.querySelectorAll(selector))
520 {
521 elements.push(element);
522 elementFilters.push(pattern.text);
523 }
471 } 524 }
472 } 525 }
526 if (this.window.performance.now() -
527 cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
528 {
529 this.window.setTimeout(processPatterns, 0);
530 return;
531 }
473 } 532 }
474 } 533 pattern = null;
534 return processPatterns();
535 };
475 536
476 this.addSelectorsFunc(selectors, selectorFilters); 537 processPatterns();
477 this.hideElemsFunc(elements, elementFilters);
478 }, 538 },
479 539
480 _stylesheetQueue: null, 540 _filteringInProgress: false,
541 _scheduledProcessing: null,
542
543 /**
544 * Re-run filtering either immediately or queued.
545 * @param {CSSStyleSheet[]} [reason]
546 * the stylesheets triggered the refiltering.
Wladimir Palant 2017/08/16 07:19:55 This needs to be explained better, particularly th
hub 2017/08/16 13:57:28 Done.
547 */
548 queueFiltering(reason)
549 {
550 let completion = () =>
551 {
552 this._lastInvocation = this.window.performance.now();
553 this._filteringInProgress = false;
554 if (this._scheduledProcessing)
555 {
556 let nextReason = this._scheduledProcessing.stylesheets;
Wladimir Palant 2017/08/16 07:19:54 Please rename this variable, `stylesheets` will do
hub 2017/08/16 13:57:28 Done
557 this._scheduledProcessing = null;
558 this.queueFiltering(nextReason);
559 }
560 };
561
562 if (this._scheduledProcessing)
563 {
564 if (reason)
565 {
566 if (this._scheduledProcessing.stylesheets)
567 this._scheduledProcessing.stylesheets.push(...reason);
568 else if (this._scheduledProcessing.stylesheets == undefined)
569 this._scheduledProcessing.stylesheets = reason;
Wladimir Palant 2017/08/16 07:19:54 Why distinguish between null and undefined? The el
hub 2017/08/16 13:57:28 Indeed. Done.
570 }
571 else
572 this._scheduledProcessing.stylesheets = null;
573 }
574 else if (this._filteringInProgress)
575 {
576 this._scheduledProcessing = {};
577 this._scheduledProcessing.stylesheets = reason;
Wladimir Palant 2017/08/16 07:19:54 Please set this in one go (particularly easy if yo
hub 2017/08/16 13:57:28 Done.
578 }
579 // Because we use Performance.now(), it is possible that the
580 // initial call be less than MIN_INVOCATION_INTERVAL.
581 // So if _lastInvocation is 0, we assume it never ran.
Wladimir Palant 2017/08/16 07:19:54 Why so complicated? You can initialize _lastInvoca
hub 2017/08/16 13:57:28 Done.
582 else if ((this._lastInvocation > 0) &&
583 (this.window.performance.now() -
584 this._lastInvocation < MIN_INVOCATION_INTERVAL))
585 {
586 this._scheduledProcessing = {};
587 this._scheduledProcessing.stylesheets = reason;
Wladimir Palant 2017/08/16 07:19:55 As above, please set this in one go.
hub 2017/08/16 13:57:28 Done.
588 this.window.setTimeout(() =>
589 {
590 let {stylesheets} = this._scheduledProcessing;
591 this._filteringInProgress = true;
592 this._scheduledProcessing = null;
593 this._addSelectors(stylesheets, completion);
594 },
595 MIN_INVOCATION_INTERVAL -
596 (this.window.performance.now() - this._lastInvocation));
597 }
598 else
599 {
600 this._filteringInProgress = true;
601 this._addSelectors(reason, completion);
602 }
603 },
481 604
482 onLoad(event) 605 onLoad(event)
483 { 606 {
484 let stylesheet = event.target.sheet; 607 let stylesheet = event.target.sheet;
485 if (stylesheet) 608 if (stylesheet)
609 this.queueFiltering([stylesheet]);
610 },
611
612 observe(mutations)
613 {
614 let stylesheets = null;
615 for (let mutation of mutations)
486 { 616 {
487 if (!this._stylesheetQueue && 617 if (mutation.type == "characterData")
Wladimir Palant 2017/08/16 07:19:54 This special logic should go as well, it's the sam
hub 2017/08/16 13:57:28 if it is a characterData change on something else,
488 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
489 { 618 {
490 this._stylesheetQueue = []; 619 let element = mutation.target.parentElement;
491 this.window.setTimeout(() => 620 if (element instanceof this.window.HTMLStyleElement &&
621 element.sheet)
492 { 622 {
493 let stylesheets = this._stylesheetQueue; 623 if (!stylesheets)
494 this._stylesheetQueue = null; 624 stylesheets = [];
495 this.addSelectors(stylesheets); 625 stylesheets.push(element.sheet);
496 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); 626 }
497 } 627 }
498
499 if (this._stylesheetQueue)
500 this._stylesheetQueue.push(stylesheet);
501 else
502 this.addSelectors([stylesheet]);
503 } 628 }
629 this.queueFiltering(stylesheets);
504 }, 630 },
505 631
506 apply() 632 apply()
507 { 633 {
508 this.getFiltersFunc(patterns => 634 this.getFiltersFunc(patterns =>
509 { 635 {
510 this.patterns = []; 636 this.patterns = [];
511 for (let pattern of patterns) 637 for (let pattern of patterns)
512 { 638 {
513 let selectors = this.parseSelector(pattern.selector); 639 let selectors = this.parseSelector(pattern.selector);
514 if (selectors != null && selectors.length > 0) 640 if (selectors != null && selectors.length > 0)
515 this.patterns.push({selectors, text: pattern.text}); 641 this.patterns.push({selectors, text: pattern.text});
516 } 642 }
517 643
518 if (this.patterns.length > 0) 644 if (this.patterns.length > 0)
519 { 645 {
520 let {document} = this.window; 646 let {document} = this.window;
521 this.addSelectors(); 647 this.queueFiltering();
648 this.observer.observe(
649 document,
650 {
651 childList: true,
652 attributes: true,
653 characterData: true,
654 subtree: true
655 }
656 );
522 document.addEventListener("load", this.onLoad.bind(this), true); 657 document.addEventListener("load", this.onLoad.bind(this), true);
523 } 658 }
524 }); 659 });
525 } 660 }
526 }; 661 };
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