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