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: Improvements Created Aug. 2, 2017, 4 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
« chrome/content/.eslintrc.json ('K') | « chrome/content/.eslintrc.json ('k') | 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 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
150 let priority = rule.style.getPropertyPriority(property); 150 let priority = rule.style.getPropertyPriority(property);
151 styles.push(`${property}: ${value}${priority ? " !" + priority : ""};`); 151 styles.push(`${property}: ${value}${priority ? " !" + priority : ""};`);
152 } 152 }
153 styles.sort(); 153 styles.sort();
154 return { 154 return {
155 style: styles.join(" "), 155 style: styles.join(" "),
156 subSelectors: splitSelector(rule.selectorText) 156 subSelectors: splitSelector(rule.selectorText)
157 }; 157 };
158 } 158 }
159 159
160 function* evaluate(chain, index, prefix, subtree, styles) 160 function* evaluate(chain, index, prefix, subtree, styles, map)
161 { 161 {
162 if (index >= chain.length) 162 if (index >= chain.length)
163 { 163 {
164 yield prefix; 164 yield prefix;
165 return; 165 return;
166 } 166 }
167 for (let [selector, element] of 167 for (let [selector, element] of
168 chain[index].getSelectors(prefix, subtree, styles)) 168 chain[index].getSelectors(prefix, subtree, styles,
169 yield* evaluate(chain, index + 1, selector, element, styles); 169 chain.slice(index), map))
170 yield* evaluate(chain, index + 1, selector, element, styles, map);
170 } 171 }
171 172
172 function PlainSelector(selector) 173 function PlainSelector(selector)
173 { 174 {
174 this._selector = selector; 175 this._selector = selector;
175 } 176 }
176 177
177 PlainSelector.prototype = { 178 PlainSelector.prototype = {
178 /** 179 /**
179 * Generator function returning a pair of selector 180 * Generator function returning a pair of selector
(...skipping 17 matching lines...) Expand all
197 } 198 }
198 199
199 HasSelector.prototype = { 200 HasSelector.prototype = {
200 requiresHiding: true, 201 requiresHiding: true,
201 202
202 get dependsOnStyles() 203 get dependsOnStyles()
203 { 204 {
204 return this._innerSelectors.some(selector => selector.dependsOnStyles); 205 return this._innerSelectors.some(selector => selector.dependsOnStyles);
205 }, 206 },
206 207
207 *getSelectors(prefix, subtree, styles) 208 *getSelectors(prefix, subtree, styles, chain, map)
208 { 209 {
209 for (let element of this.getElements(prefix, subtree, styles)) 210 for (let element of this.getElements(prefix, subtree, styles, chain, map))
210 yield [makeSelector(element, ""), element]; 211 yield [makeSelector(element, ""), element];
211 }, 212 },
212 213
213 /** 214 /**
214 * Generator function returning selected elements. 215 * Generator function returning selected elements.
215 * @param {string} prefix the prefix for the selector. 216 * @param {string} prefix the prefix for the selector.
216 * @param {Node} subtree the subtree we work on. 217 * @param {Node} subtree the subtree we work on.
217 * @param {StringifiedStyle[]} styles the stringified style objects. 218 * @param {StringifiedStyle[]} styles the stringified style objects.
219 * @param {Array} chain the chain of selectors including this.
220 * @param {WeakMap} map of the elements and chain for re-evaluation.
218 */ 221 */
219 *getElements(prefix, subtree, styles) 222 *getElements(prefix, subtree, styles, chain, map)
220 { 223 {
221 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 224 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
222 prefix + "*" : prefix; 225 prefix + "*" : prefix;
223 let elements = subtree.querySelectorAll(actualPrefix); 226 let elements = subtree.querySelectorAll(actualPrefix);
224 for (let element of elements) 227 for (let element of elements)
225 { 228 {
226 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 229 let e = map.get(element);
230 if (e == undefined)
231 map.set(element, [chain]);
232 else
233 e.push(chain);
234 let iter = evaluate(this._innerSelectors, 0, "", element, styles, map);
227 for (let selector of iter) 235 for (let selector of iter)
228 { 236 {
229 if (relativeSelector.test(selector)) 237 if (relativeSelector.test(selector))
230 selector = ":scope" + selector; 238 selector = ":scope" + selector;
231 if (element.querySelector(selector)) 239 if (element.querySelector(selector))
232 yield element; 240 yield element;
233 } 241 }
234 } 242 }
235 } 243 }
236 }; 244 };
237 245
238 function ContainsSelector(textContent) 246 function ContainsSelector(textContent)
239 { 247 {
240 this._text = textContent; 248 this._text = textContent;
241 } 249 }
242 250
243 ContainsSelector.prototype = { 251 ContainsSelector.prototype = {
244 requiresHiding: true, 252 requiresHiding: true,
245 253
246 *getSelectors(prefix, subtree, stylesheet) 254 *getSelectors(prefix, subtree, stylesheet, chain, map)
247 { 255 {
248 for (let element of this.getElements(prefix, subtree, stylesheet)) 256 for (let element of this.getElements(prefix, subtree, stylesheet,
257 chain, map))
249 yield [makeSelector(element, ""), subtree]; 258 yield [makeSelector(element, ""), subtree];
250 }, 259 },
251 260
252 *getElements(prefix, subtree, stylesheet) 261 *getElements(prefix, subtree, stylesheet, chain, map)
253 { 262 {
254 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 263 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
255 prefix + "*" : prefix; 264 prefix + "*" : prefix;
256 let elements = subtree.querySelectorAll(actualPrefix); 265 let elements = subtree.querySelectorAll(actualPrefix);
257 for (let element of elements) 266 for (let element of elements)
258 if (element.textContent.includes(this._text)) 267 if (element.textContent.includes(this._text))
259 yield element; 268 yield element;
260 } 269 }
261 }; 270 };
262 271
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
303 } 312 }
304 }, 313 },
305 314
306 *getSelectors(prefix, subtree, styles) 315 *getSelectors(prefix, subtree, styles)
307 { 316 {
308 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 317 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
309 yield [selector, subtree]; 318 yield [selector, subtree];
310 } 319 }
311 }; 320 };
312 321
322 function isSelectorHidingOnlyPattern(pattern)
323 {
324 return pattern.selectors.some(s => s.preferHideWithSelector) &&
325 !pattern.selectors.some(s => s.requiresHiding);
326 }
327
313 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 328 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
314 hideElemsFunc) 329 hideElemsFunc)
315 { 330 {
316 this.window = window; 331 this.window = window;
317 this.getFiltersFunc = getFiltersFunc; 332 this.getFiltersFunc = getFiltersFunc;
318 this.addSelectorsFunc = addSelectorsFunc; 333 this.addSelectorsFunc = addSelectorsFunc;
319 this.hideElemsFunc = hideElemsFunc; 334 this.hideElemsFunc = hideElemsFunc;
335 this.observer = new window.MutationObserver(this.observe.bind(this));
320 } 336 }
321 337
322 ElemHideEmulation.prototype = { 338 ElemHideEmulation.prototype = {
323 isSameOrigin(stylesheet) 339 isSameOrigin(stylesheet)
324 { 340 {
325 try 341 try
326 { 342 {
327 return new URL(stylesheet.href).origin == this.window.location.origin; 343 return new URL(stylesheet.href).origin == this.window.location.origin;
328 } 344 }
329 catch (e) 345 catch (e)
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
396 { 412 {
397 this.window.console.error( 413 this.window.console.error(
398 new SyntaxError("Failed to parse Adblock Plus " + 414 new SyntaxError("Failed to parse Adblock Plus " +
399 `selector ${selector}, can't ` + 415 `selector ${selector}, can't ` +
400 "have a lonely :-abp-contains().")); 416 "have a lonely :-abp-contains()."));
401 return null; 417 return null;
402 } 418 }
403 return selectors; 419 return selectors;
404 }, 420 },
405 421
422 _observerMap: new WeakMap(),
423
406 _lastInvocation: 0, 424 _lastInvocation: 0,
407 425
408 /** 426 /**
409 * Processes the current document and applies all rules to it. 427 * Processes the current document and applies all rules to it.
410 * @param {CSSStyleSheet[]} [stylesheets] 428 * @param {CSSStyleSheet[]} [stylesheets]
411 * The list of new stylesheets that have been added to the document and 429 * The list of new stylesheets that have been added to the document and
412 * made reprocessing necessary. This parameter shouldn't be passed in for 430 * made reprocessing necessary. This parameter shouldn't be passed in for
413 * the initial processing, all of document's stylesheets will be considered 431 * the initial processing, all of document's stylesheets will be considered
414 * then and all rules, including the ones not dependent on styles. 432 * then and all rules, including the ones not dependent on styles.
433 * @param {boolean} [domUpdate]
434 * Indicate this is a DOM update.
415 */ 435 */
416 addSelectors(stylesheets) 436 addSelectors(stylesheets, domUpdate)
417 { 437 {
418 this._lastInvocation = Date.now(); 438 this._lastInvocation = Date.now();
419 439
420 let selectors = []; 440 let selectors = [];
421 let selectorFilters = []; 441 let selectorFilters = [];
422 442
423 let elements = []; 443 let elements = [];
424 let elementFilters = []; 444 let elementFilters = [];
425 445
426 let cssStyles = []; 446 let cssStyles = [];
427 447
428 let stylesheetOnlyChange = !!stylesheets; 448 let stylesheetOnlyChange = !!stylesheets && !domUpdate;
429 if (!stylesheets) 449 if (!stylesheets)
430 stylesheets = this.window.document.styleSheets; 450 stylesheets = this.window.document.styleSheets;
431 451
432 // Chrome < 51 doesn't have an iterable StyleSheetList 452 // Chrome < 51 doesn't have an iterable StyleSheetList
433 // https://issues.adblockplus.org/ticket/5381 453 // https://issues.adblockplus.org/ticket/5381
434 for (let i = 0; i < stylesheets.length; i++) 454 for (let i = 0; i < stylesheets.length; i++)
435 { 455 {
436 let stylesheet = stylesheets[i]; 456 let stylesheet = stylesheets[i];
437 // Explicitly ignore third-party stylesheets to ensure consistent behavior 457 // Explicitly ignore third-party stylesheets to ensure consistent behavior
438 // between Firefox and Chrome. 458 // between Firefox and Chrome.
(...skipping 15 matching lines...) Expand all
454 474
455 let {document} = this.window; 475 let {document} = this.window;
456 for (let pattern of this.patterns) 476 for (let pattern of this.patterns)
457 { 477 {
458 if (stylesheetOnlyChange && 478 if (stylesheetOnlyChange &&
459 !pattern.selectors.some(selector => selector.dependsOnStyles)) 479 !pattern.selectors.some(selector => selector.dependsOnStyles))
460 { 480 {
461 continue; 481 continue;
462 } 482 }
463 483
464 for (let selector of evaluate(pattern.selectors, 484 for (let selector of evaluate(pattern.selectors, 0, "", document,
465 0, "", document, cssStyles)) 485 cssStyles, this._observerMap))
466 { 486 {
467 if (pattern.selectors.some(s => s.preferHideWithSelector) && 487 if (isSelectorHidingOnlyPattern(pattern))
468 !pattern.selectors.some(s => s.requiresHiding))
469 { 488 {
470 selectors.push(selector); 489 selectors.push(selector);
471 selectorFilters.push(pattern.text); 490 selectorFilters.push(pattern.text);
472 } 491 }
473 else 492 else
474 { 493 {
475 for (let element of document.querySelectorAll(selector)) 494 for (let element of document.querySelectorAll(selector))
476 { 495 {
477 elements.push(element); 496 elements.push(element);
478 elementFilters.push(pattern.text); 497 elementFilters.push(pattern.text);
479 } 498 }
480 } 499 }
481 } 500 }
482 } 501 }
483 502
484 this.addSelectorsFunc(selectors, selectorFilters); 503 this.addSelectorsFunc(selectors, selectorFilters);
485 this.hideElemsFunc(elements, elementFilters); 504 this.hideElemsFunc(elements, elementFilters);
486 }, 505 },
487 506
488 _stylesheetQueue: null, 507 _stylesheetQueue: null,
489 508
509 /** Filtering reason
510 * @typedef {Object} FilteringReason
511 * @property {boolean} dom Indicate the DOM changed (tree or attributes)
512 * @property {CSSStyleSheet[]} [stylesheets]
513 * Indicate the stylesheets that needs refresh
514 * @property {WeakSet} subtrees The subtrees affected we were watching.
515 */
516
517 /** Re-run filtering either immediately or queued.
518 * @param {FilteringReason} reason why the filtering must be queued.
519 */
520 queueFiltering(reason)
521 {
522 if (!this._stylesheetQueue &&
523 (Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL ||
524 reason.dom))
525 {
526 this._stylesheetQueue = [];
527 this.window.setTimeout(() =>
528 {
529 let stylesheets = this._stylesheetQueue;
530 this._stylesheetQueue = null;
531 let domUpdate = reason.dom;
hub 2017/08/03 16:28:18 I realise this should have been moved up, out of t
532 this.addSelectors(stylesheets, domUpdate);
hub 2017/08/02 04:10:55 Here we don't use reason.subtree. Actually I'm no
533 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
534 }
535 if (reason.stylesheets)
536 {
537 if (this._stylesheetQueue)
538 this._stylesheetQueue.push(...reason.stylesheets);
539 else
540 this.addSelectors(reason.stylesheets);
541 }
542 },
543
490 onLoad(event) 544 onLoad(event)
491 { 545 {
492 let stylesheet = event.target.sheet; 546 let stylesheet = event.target.sheet;
493 if (stylesheet) 547 if (stylesheet)
548 this.queueFiltering({stylesheets: [stylesheet]});
549 },
550
551 observe(mutations)
552 {
553 let reason = {};
554 reason.dom = true;
555 let stylesheets = [];
556 for (let mutation of mutations)
494 { 557 {
495 if (!this._stylesheetQueue && 558 if (mutation.type == "childList")
496 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
497 { 559 {
498 this._stylesheetQueue = []; 560 for (let added of mutation.addedNodes)
499 this.window.setTimeout(() =>
500 { 561 {
501 let stylesheets = this._stylesheetQueue; 562 if (added.nodeType == Node.ELEMENT_NODE &&
502 this._stylesheetQueue = null; 563 (added.tagName == "STYLE" || added.tagName == "style") &&
503 this.addSelectors(stylesheets); 564 added.styesheet)
504 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); 565 stylesheets.push(added.stylesheet);
hub 2017/08/02 04:10:54 If we have a new style element, then we likely hav
566 }
567 for (let removed of mutation.removedNodes)
568 {
569 this._observerMap.delete(removed);
570 }
505 } 571 }
506 572 let currentNode = mutation.target;
507 if (this._stylesheetQueue) 573 while (currentNode)
508 this._stylesheetQueue.push(stylesheet); 574 {
509 else 575 let e = this._observerMap.has(currentNode);
510 this.addSelectors([stylesheet]); 576 if (e)
577 {
578 if (!(reason.subtrees instanceof Set))
579 reason.subtrees = new Set();
580 reason.subtrees.add(currentNode);
581 break;
582 }
583 currentNode = currentNode.parentNode;
584 }
511 } 585 }
586 if (stylesheets.length > 0)
587 reason.stylesheets = stylesheets;
588 this.queueFiltering(reason);
512 }, 589 },
513 590
514 apply() 591 apply()
515 { 592 {
516 this.getFiltersFunc(patterns => 593 this.getFiltersFunc(patterns =>
517 { 594 {
518 this.patterns = []; 595 this.patterns = [];
519 for (let pattern of patterns) 596 for (let pattern of patterns)
520 { 597 {
521 let selectors = this.parseSelector(pattern.selector); 598 let selectors = this.parseSelector(pattern.selector);
522 if (selectors != null && selectors.length > 0) 599 if (selectors != null && selectors.length > 0)
523 this.patterns.push({selectors, text: pattern.text}); 600 this.patterns.push({selectors, text: pattern.text});
524 } 601 }
525 602
526 if (this.patterns.length > 0) 603 if (this.patterns.length > 0)
527 { 604 {
528 let {document} = this.window; 605 let {document} = this.window;
529 this.addSelectors(); 606 this.addSelectors();
607 this.observer.observe(
608 document,
609 {
610 childList: true,
611 attributes: true,
612 subtree: true,
613 attributeFilter: ["class", "id"]
hub 2017/08/02 04:10:54 check for the obvious class and id attributes that
614 }
615 );
530 document.addEventListener("load", this.onLoad.bind(this), true); 616 document.addEventListener("load", this.onLoad.bind(this), true);
531 } 617 }
532 }); 618 });
533 } 619 }
534 }; 620 };
OLDNEW
« chrome/content/.eslintrc.json ('K') | « chrome/content/.eslintrc.json ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld