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

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

Issue 29494577: Issue 5438 - Observer DOM changes to reapply filters. (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Patch Set: Updated the tests. Created Aug. 23, 2017, 4:37 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 | test/browser/elemHideEmulation.js » ('j') | test/browser/elemHideEmulation.js » ('J')
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 {filterToRegExp, splitSelector} = require("common"); 20 const {filterToRegExp, splitSelector} = require("common");
21 21
22 const MIN_INVOCATION_INTERVAL = 3000; 22 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
24 24
25 /** Return position of node from parent. 25 /** Return position of node from parent.
26 * @param {Node} node the node to find the position of. 26 * @param {Node} node the node to find the position of.
27 * @return {number} One-based index like for :nth-child(), or 0 on error. 27 * @return {number} One-based index like for :nth-child(), or 0 on error.
28 */ 28 */
29 function positionInParent(node) 29 function positionInParent(node)
30 { 30 {
31 let {children} = node.parentNode; 31 let {children} = node.parentNode;
32 for (let i = 0; i < children.length; i++) 32 for (let i = 0; i < children.length; i++)
33 if (children[i] == node) 33 if (children[i] == node)
34 return i + 1; 34 return i + 1;
35 return 0; 35 return 0;
36 } 36 }
37 37
38 function makeSelector(node, selector) 38 function makeSelector(node, selector)
39 { 39 {
40 if (node == null)
41 return null;
40 if (!node.parentElement) 42 if (!node.parentElement)
41 { 43 {
42 let newSelector = ":root"; 44 let newSelector = ":root";
43 if (selector) 45 if (selector)
44 newSelector += " > " + selector; 46 newSelector += " > " + selector;
45 return newSelector; 47 return newSelector;
46 } 48 }
47 let idx = positionInParent(node); 49 let idx = positionInParent(node);
48 if (idx > 0) 50 if (idx > 0)
49 { 51 {
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
121 123
122 function* evaluate(chain, index, prefix, subtree, styles) 124 function* evaluate(chain, index, prefix, subtree, styles)
123 { 125 {
124 if (index >= chain.length) 126 if (index >= chain.length)
125 { 127 {
126 yield prefix; 128 yield prefix;
127 return; 129 return;
128 } 130 }
129 for (let [selector, element] of 131 for (let [selector, element] of
130 chain[index].getSelectors(prefix, subtree, styles)) 132 chain[index].getSelectors(prefix, subtree, styles))
131 yield* evaluate(chain, index + 1, selector, element, styles); 133 {
134 if (selector == null)
135 yield null;
136 else
137 yield* evaluate(chain, index + 1, selector, element, styles);
138 }
139 // Just in case the getSelectors() generator above had to run some heavy
140 // document.querySelectorAll() call which didn't produce any results, make
141 // sure there is at least one point where execution can pause.
142 yield null;
132 } 143 }
133 144
134 function PlainSelector(selector) 145 function PlainSelector(selector)
135 { 146 {
136 this._selector = selector; 147 this._selector = selector;
137 } 148 }
138 149
139 PlainSelector.prototype = { 150 PlainSelector.prototype = {
140 /** 151 /**
141 * Generator function returning a pair of selector 152 * Generator function returning a pair of selector
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
181 *getElements(prefix, subtree, styles) 192 *getElements(prefix, subtree, styles)
182 { 193 {
183 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 194 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
184 prefix + "*" : prefix; 195 prefix + "*" : prefix;
185 let elements = subtree.querySelectorAll(actualPrefix); 196 let elements = subtree.querySelectorAll(actualPrefix);
186 for (let element of elements) 197 for (let element of elements)
187 { 198 {
188 let iter = evaluate(this._innerSelectors, 0, "", element, styles); 199 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
189 for (let selector of iter) 200 for (let selector of iter)
190 { 201 {
202 if (selector == null)
203 {
204 yield null;
205 continue;
206 }
191 if (relativeSelectorRegexp.test(selector)) 207 if (relativeSelectorRegexp.test(selector))
192 selector = ":scope" + selector; 208 selector = ":scope" + selector;
193 try 209 try
194 { 210 {
195 if (element.querySelector(selector)) 211 if (element.querySelector(selector))
196 yield element; 212 yield element;
197 } 213 }
198 catch (e) 214 catch (e)
199 { 215 {
200 // :scope isn't supported on Edge, ignore error caused by it. 216 // :scope isn't supported on Edge, ignore error caused by it.
201 } 217 }
202 } 218 }
219 yield null;
203 } 220 }
204 } 221 }
205 }; 222 };
206 223
207 function ContainsSelector(textContent) 224 function ContainsSelector(textContent)
208 { 225 {
209 this._text = textContent; 226 this._text = textContent;
210 } 227 }
211 228
212 ContainsSelector.prototype = { 229 ContainsSelector.prototype = {
213 requiresHiding: true, 230 requiresHiding: true,
214 231
215 *getSelectors(prefix, subtree, stylesheet) 232 *getSelectors(prefix, subtree, stylesheet)
216 { 233 {
217 for (let element of this.getElements(prefix, subtree, stylesheet)) 234 for (let element of this.getElements(prefix, subtree, stylesheet))
218 yield [makeSelector(element, ""), subtree]; 235 yield [makeSelector(element, ""), subtree];
219 }, 236 },
220 237
221 *getElements(prefix, subtree, stylesheet) 238 *getElements(prefix, subtree, stylesheet)
222 { 239 {
223 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 240 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
224 prefix + "*" : prefix; 241 prefix + "*" : prefix;
225 let elements = subtree.querySelectorAll(actualPrefix); 242 let elements = subtree.querySelectorAll(actualPrefix);
243
226 for (let element of elements) 244 for (let element of elements)
245 {
227 if (element.textContent.includes(this._text)) 246 if (element.textContent.includes(this._text))
228 yield element; 247 yield element;
248 else
249 yield null;
250 }
229 } 251 }
230 }; 252 };
231 253
232 function PropsSelector(propertyExpression) 254 function PropsSelector(propertyExpression)
233 { 255 {
234 let regexpString; 256 let regexpString;
235 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 257 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
236 propertyExpression[propertyExpression.length - 1] == "/") 258 propertyExpression[propertyExpression.length - 1] == "/")
237 { 259 {
238 regexpString = propertyExpression.slice(1, -1) 260 regexpString = propertyExpression.slice(1, -1)
(...skipping 27 matching lines...) Expand all
266 } 288 }
267 }, 289 },
268 290
269 *getSelectors(prefix, subtree, styles) 291 *getSelectors(prefix, subtree, styles)
270 { 292 {
271 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 293 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
272 yield [selector, subtree]; 294 yield [selector, subtree];
273 } 295 }
274 }; 296 };
275 297
298 function isSelectorHidingOnlyPattern(pattern)
299 {
300 return pattern.selectors.some(s => s.preferHideWithSelector) &&
301 !pattern.selectors.some(s => s.requiresHiding);
302 }
303
276 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 304 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
277 hideElemsFunc) 305 hideElemsFunc)
278 { 306 {
279 this.window = window; 307 this.window = window;
280 this.getFiltersFunc = getFiltersFunc; 308 this.getFiltersFunc = getFiltersFunc;
281 this.addSelectorsFunc = addSelectorsFunc; 309 this.addSelectorsFunc = addSelectorsFunc;
282 this.hideElemsFunc = hideElemsFunc; 310 this.hideElemsFunc = hideElemsFunc;
311 this.observer = new window.MutationObserver(this.observe.bind(this));
283 } 312 }
284 313
285 ElemHideEmulation.prototype = { 314 ElemHideEmulation.prototype = {
286 isSameOrigin(stylesheet) 315 isSameOrigin(stylesheet)
287 { 316 {
288 try 317 try
289 { 318 {
290 return new URL(stylesheet.href).origin == this.window.location.origin; 319 return new URL(stylesheet.href).origin == this.window.location.origin;
291 } 320 }
292 catch (e) 321 catch (e)
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after
355 { 384 {
356 this.window.console.error( 385 this.window.console.error(
357 new SyntaxError("Failed to parse Adblock Plus " + 386 new SyntaxError("Failed to parse Adblock Plus " +
358 `selector ${selector}, can't ` + 387 `selector ${selector}, can't ` +
359 "have a lonely :-abp-contains().")); 388 "have a lonely :-abp-contains()."));
360 return null; 389 return null;
361 } 390 }
362 return selectors; 391 return selectors;
363 }, 392 },
364 393
365 _lastInvocation: 0,
366
367 /** 394 /**
368 * Processes the current document and applies all rules to it. 395 * Processes the current document and applies all rules to it.
369 * @param {CSSStyleSheet[]} [stylesheets] 396 * @param {CSSStyleSheet[]} [stylesheets]
370 * The list of new stylesheets that have been added to the document and 397 * The list of new stylesheets that have been added to the document and
371 * made reprocessing necessary. This parameter shouldn't be passed in for 398 * made reprocessing necessary. This parameter shouldn't be passed in for
372 * the initial processing, all of document's stylesheets will be considered 399 * the initial processing, all of document's stylesheets will be considered
373 * then and all rules, including the ones not dependent on styles. 400 * then and all rules, including the ones not dependent on styles.
401 * @param {function} [done]
402 * Callback to call when done.
374 */ 403 */
375 addSelectors(stylesheets) 404 _addSelectors(stylesheets, done)
376 { 405 {
377 this._lastInvocation = Date.now();
378
379 let selectors = []; 406 let selectors = [];
380 let selectorFilters = []; 407 let selectorFilters = [];
381 408
382 let elements = []; 409 let elements = [];
383 let elementFilters = []; 410 let elementFilters = [];
384 411
385 let cssStyles = []; 412 let cssStyles = [];
386 413
387 let stylesheetOnlyChange = !!stylesheets; 414 let stylesheetOnlyChange = !!stylesheets;
388 if (!stylesheets) 415 if (!stylesheets)
(...skipping 16 matching lines...) Expand all
405 for (let rule of rules) 432 for (let rule of rules)
406 { 433 {
407 if (rule.type != rule.STYLE_RULE) 434 if (rule.type != rule.STYLE_RULE)
408 continue; 435 continue;
409 436
410 cssStyles.push(stringifyStyle(rule)); 437 cssStyles.push(stringifyStyle(rule));
411 } 438 }
412 } 439 }
413 440
414 let {document} = this.window; 441 let {document} = this.window;
415 for (let pattern of this.patterns) 442
443 let patterns = this.patterns.slice();
444 let pattern = null;
445 let generator = null;
446
447 let processPatterns = () =>
416 { 448 {
417 if (stylesheetOnlyChange && 449 let cycleStart = this.window.performance.now();
418 !pattern.selectors.some(selector => selector.dependsOnStyles)) 450
451 if (!pattern)
419 { 452 {
420 continue; 453 if (!patterns.length)
454 {
455 this.addSelectorsFunc(selectors, selectorFilters);
456 this.hideElemsFunc(elements, elementFilters);
457 if (typeof done == "function")
458 done();
459 return;
460 }
461
462 pattern = patterns.shift();
463
464 if (stylesheetOnlyChange &&
465 !pattern.selectors.some(selector => selector.dependsOnStyles))
466 {
467 pattern = null;
468 return processPatterns();
469 }
470 generator = evaluate(pattern.selectors, 0, "", document, cssStyles);
421 } 471 }
422 472 for (let selector of generator)
423 for (let selector of evaluate(pattern.selectors,
424 0, "", document, cssStyles))
425 { 473 {
426 if (pattern.selectors.some(s => s.preferHideWithSelector) && 474 if (selector != null)
427 !pattern.selectors.some(s => s.requiresHiding))
428 { 475 {
429 selectors.push(selector); 476 if (isSelectorHidingOnlyPattern(pattern))
430 selectorFilters.push(pattern.text);
431 }
432 else
433 {
434 for (let element of document.querySelectorAll(selector))
435 { 477 {
436 elements.push(element); 478 selectors.push(selector);
437 elementFilters.push(pattern.text); 479 selectorFilters.push(pattern.text);
480 }
481 else
482 {
483 for (let element of document.querySelectorAll(selector))
484 {
485 elements.push(element);
486 elementFilters.push(pattern.text);
487 }
438 } 488 }
439 } 489 }
490 if (this.window.performance.now() -
491 cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
492 {
493 this.window.setTimeout(processPatterns, 0);
494 return;
495 }
440 } 496 }
441 } 497 pattern = null;
498 return processPatterns();
499 };
442 500
443 this.addSelectorsFunc(selectors, selectorFilters); 501 processPatterns();
444 this.hideElemsFunc(elements, elementFilters);
445 }, 502 },
446 503
447 _stylesheetQueue: null, 504 _MIN_INVOCATION_INTERVAL: 3000,
505
506 set _invocationInterval(interval)
507 {
508 this._MIN_INVOCATION_INTERVAL = interval;
509 if (this._lastInvocation < 0)
510 this._lastInvocation = -interval;
511 },
Wladimir Palant 2017/08/25 07:44:58 You can have a setter without a getter but that's
hub 2017/08/25 13:48:12 Done.
512
513 _filteringInProgress: false,
514 _lastInvocation: -this._MIN_INVOCATION_INTERVAL,
Wladimir Palant 2017/08/25 07:44:58 This assignment won't work, `this` isn't pointing
hub 2017/08/25 13:48:12 oops.
515 _scheduledProcessing: null,
516
517 /**
518 * Re-run filtering either immediately or queued.
519 * @param {CSSStyleSheet[]} [stylesheets]
520 * new stylesheets to be processed. This parameter should be omitted
521 * for DOM modification (full reprocessing required).
522 */
523 queueFiltering(stylesheets)
524 {
525 let completion = () =>
526 {
527 this._lastInvocation = this.window.performance.now();
528 this._filteringInProgress = false;
529 if (this._scheduledProcessing)
530 {
531 let newStylesheets = this._scheduledProcessing.stylesheets;
532 this._scheduledProcessing = null;
533 this.queueFiltering(newStylesheets);
534 }
535 };
536
537 if (this._scheduledProcessing)
538 {
539 if (!stylesheets)
540 this._scheduledProcessing.stylesheets = null;
541 else if (this._scheduledProcessing.stylesheets)
542 this._scheduledProcessing.stylesheets.push(...stylesheets);
543 }
544 else if (this._filteringInProgress)
545 {
546 this._scheduledProcessing = {stylesheets};
547 }
548 else if (this.window.performance.now() -
549 this._lastInvocation < this._MIN_INVOCATION_INTERVAL)
550 {
551 this._scheduledProcessing = {stylesheets};
552 this.window.setTimeout(() =>
553 {
554 let newStylesheets = this._scheduledProcessing.stylesheets;
555 this._filteringInProgress = true;
556 this._scheduledProcessing = null;
557 this._addSelectors(newStylesheets, completion);
558 },
559 this._MIN_INVOCATION_INTERVAL -
560 (this.window.performance.now() - this._lastInvocation));
561 }
562 else
563 {
564 this._filteringInProgress = true;
565 this._addSelectors(stylesheets, completion);
566 }
567 },
448 568
449 onLoad(event) 569 onLoad(event)
450 { 570 {
451 let stylesheet = event.target.sheet; 571 let stylesheet = event.target.sheet;
452 if (stylesheet) 572 if (stylesheet)
453 { 573 this.queueFiltering([stylesheet]);
454 if (!this._stylesheetQueue && 574 },
455 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
456 {
457 this._stylesheetQueue = [];
458 this.window.setTimeout(() =>
459 {
460 let stylesheets = this._stylesheetQueue;
461 this._stylesheetQueue = null;
462 this.addSelectors(stylesheets);
463 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
464 }
465 575
466 if (this._stylesheetQueue) 576 observe(mutations)
467 this._stylesheetQueue.push(stylesheet); 577 {
468 else 578 this.queueFiltering();
469 this.addSelectors([stylesheet]);
470 }
471 }, 579 },
472 580
473 apply() 581 apply()
474 { 582 {
475 this.getFiltersFunc(patterns => 583 this.getFiltersFunc(patterns =>
476 { 584 {
477 this.patterns = []; 585 this.patterns = [];
478 for (let pattern of patterns) 586 for (let pattern of patterns)
479 { 587 {
480 let selectors = this.parseSelector(pattern.selector); 588 let selectors = this.parseSelector(pattern.selector);
481 if (selectors != null && selectors.length > 0) 589 if (selectors != null && selectors.length > 0)
482 this.patterns.push({selectors, text: pattern.text}); 590 this.patterns.push({selectors, text: pattern.text});
483 } 591 }
484 592
485 if (this.patterns.length > 0) 593 if (this.patterns.length > 0)
486 { 594 {
487 let {document} = this.window; 595 let {document} = this.window;
488 this.addSelectors(); 596 this.queueFiltering();
597 this.observer.observe(
598 document,
599 {
600 childList: true,
601 attributes: true,
602 characterData: true,
603 subtree: true
604 }
605 );
489 document.addEventListener("load", this.onLoad.bind(this), true); 606 document.addEventListener("load", this.onLoad.bind(this), true);
490 } 607 }
491 }); 608 });
492 } 609 }
493 }; 610 };
494 611
495 exports.ElemHideEmulation = ElemHideEmulation; 612 exports.ElemHideEmulation = ElemHideEmulation;
OLDNEW
« no previous file with comments | « no previous file | test/browser/elemHideEmulation.js » ('j') | test/browser/elemHideEmulation.js » ('J')

Powered by Google App Engine
This is Rietveld