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

Delta Between Two Patch Sets: lib/content/elemHideEmulation.js

Issue 29714601: Issue 6437 - Skip elements not affected by DOM mutations (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Left Patch Set: Created March 5, 2018, 6:05 p.m.
Right Patch Set: Rename debug mode to test mode Created May 18, 2018, 3:41 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | test/browser/elemHideEmulation.js » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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 {textToRegExp, filterToRegExp, splitSelector} = require("../common"); 20 const {textToRegExp, filterToRegExp, splitSelector} = require("../common");
21 const {indexOf} = require("../coreUtils");
21 22
22 let MIN_INVOCATION_INTERVAL = 3000; 23 let MIN_INVOCATION_INTERVAL = 3000;
23 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; 24 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
24 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 25 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
26
27 let testInfo = null;
28
29 function setTestMode()
30 {
31 testInfo = {
32 lastProcessedElements: new Set()
33 };
34 }
35
36 exports.setTestMode = setTestMode;
37
38 function getTestInfo()
39 {
40 return testInfo;
41 }
42
43 exports.getTestInfo = getTestInfo;
hub 2018/05/24 19:19:09 I'm a bit skeptical with that way or test, includi
Manish Jethani 2018/05/25 07:21:25 We can work on improving our test framework, but f
44
45 function getCachedPropertyValue(object, name, defaultValueFunc = () => {})
46 {
47 let value = object[name];
48 if (typeof value == "undefined")
49 Object.defineProperty(object, name, {value: value = defaultValueFunc()});
50 return value;
51 }
25 52
26 /** Return position of node from parent. 53 /** Return position of node from parent.
27 * @param {Node} node the node to find the position of. 54 * @param {Node} node the node to find the position of.
28 * @return {number} One-based index like for :nth-child(), or 0 on error. 55 * @return {number} One-based index like for :nth-child(), or 0 on error.
29 */ 56 */
30 function positionInParent(node) 57 function positionInParent(node)
31 { 58 {
32 let {children} = node.parentNode; 59 return indexOf(node.parentNode.children, node) + 1;
33 for (let i = 0; i < children.length; i++) 60 }
34 if (children[i] == node) 61
35 return i + 1; 62 function makeSelector(node, selector = "")
36 return 0;
37 }
38
39 function makeSelector(node, selector)
40 { 63 {
41 if (node == null) 64 if (node == null)
42 return null; 65 return null;
43 if (!node.parentElement) 66 if (!node.parentElement)
44 { 67 {
45 let newSelector = ":root"; 68 let newSelector = ":root";
46 if (selector) 69 if (selector)
47 newSelector += " > " + selector; 70 newSelector += " > " + selector;
48 return newSelector; 71 return newSelector;
49 } 72 }
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
164 } 187 }
165 return all ? subtree.querySelectorAll(selector) : 188 return all ? subtree.querySelectorAll(selector) :
166 subtree.querySelector(selector); 189 subtree.querySelector(selector);
167 } 190 }
168 191
169 function scopedQuerySelectorAll(subtree, selector) 192 function scopedQuerySelectorAll(subtree, selector)
170 { 193 {
171 return scopedQuerySelector(subtree, selector, true); 194 return scopedQuerySelector(subtree, selector, true);
172 } 195 }
173 196
174 const regexpRegexp = /^\/(.*)\/([im]*)$/; 197 const regexpRegexp = /^\/(.*)\/([imu]*)$/;
175 198
176 /** 199 /**
177 * Make a regular expression from a text argument. If it can be parsed as a 200 * Make a regular expression from a text argument. If it can be parsed as a
178 * regular expression, parse it and the flags. 201 * regular expression, parse it and the flags.
179 * @param {string} text the text argument. 202 * @param {string} text the text argument.
180 * @return {?RegExp} a RegExp object or null in case of error. 203 * @return {?RegExp} a RegExp object or null in case of error.
181 */ 204 */
182 function makeRegExpParameter(text) 205 function makeRegExpParameter(text)
183 { 206 {
184 let [, pattern, flags] = 207 let [, pattern, flags] =
(...skipping 27 matching lines...) Expand all
212 // Just in case the getSelectors() generator above had to run some heavy 235 // Just in case the getSelectors() generator above had to run some heavy
213 // document.querySelectorAll() call which didn't produce any results, make 236 // document.querySelectorAll() call which didn't produce any results, make
214 // sure there is at least one point where execution can pause. 237 // sure there is at least one point where execution can pause.
215 yield null; 238 yield null;
216 } 239 }
217 240
218 function PlainSelector(selector) 241 function PlainSelector(selector)
219 { 242 {
220 this._selector = selector; 243 this._selector = selector;
221 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); 244 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector);
245 this.dependsOnDOM = this.maybeDependsOnAttributes;
246 this.maybeContainsSiblingCombinators = /[~+]/.test(selector);
222 } 247 }
223 248
224 PlainSelector.prototype = { 249 PlainSelector.prototype = {
225 /** 250 /**
226 * Generator function returning a pair of selector 251 * Generator function returning a pair of selector
227 * string and subtree. 252 * string and subtree.
228 * @param {string} prefix the prefix for the selector. 253 * @param {string} prefix the prefix for the selector.
229 * @param {Node} subtree the subtree we work on. 254 * @param {Node} subtree the subtree we work on.
230 * @param {StringifiedStyle[]} styles the stringified style objects. 255 * @param {StringifiedStyle[]} styles the stringified style objects.
231 * @param {Node[]} [targets] the nodes we are interested in. 256 * @param {Node[]} [targets] the nodes we are interested in.
232 */ 257 */
233 *getSelectors(prefix, subtree, styles, targets) 258 *getSelectors(prefix, subtree, styles, targets)
234 { 259 {
235 yield [prefix + this._selector, subtree]; 260 yield [prefix + this._selector, subtree];
236 } 261 }
237 }; 262 };
238 263
239 const incompletePrefixRegexp = /[\s>+~]$/; 264 const incompletePrefixRegexp = /[\s>+~]$/;
240 265
241 function HasSelector(selectors) 266 function HasSelector(selectors)
242 { 267 {
243 this._innerSelectors = selectors; 268 this._innerSelectors = selectors;
244 } 269 }
245 270
246 HasSelector.prototype = { 271 HasSelector.prototype = {
247 requiresHiding: true,
248 dependsOnDOM: true, 272 dependsOnDOM: true,
249 273
250 get dependsOnStyles() 274 get dependsOnStyles()
251 { 275 {
252 return this._innerSelectors.some(selector => selector.dependsOnStyles); 276 return this._innerSelectors.some(selector => selector.dependsOnStyles);
253 }, 277 },
254 278
255 get dependsOnCharacterData() 279 get dependsOnCharacterData()
256 { 280 {
257 return this._innerSelectors.some( 281 return this._innerSelectors.some(
258 selector => selector.dependsOnCharacterData 282 selector => selector.dependsOnCharacterData
259 ); 283 );
260 }, 284 },
261 285
262 get maybeDependsOnAttributes() 286 get maybeDependsOnAttributes()
263 { 287 {
264 return this._innerSelectors.some( 288 return this._innerSelectors.some(
265 selector => selector.maybeDependsOnAttributes 289 selector => selector.maybeDependsOnAttributes
266 ); 290 );
267 }, 291 },
268 292
269 *getSelectors(prefix, subtree, styles, targets) 293 *getSelectors(prefix, subtree, styles, targets)
270 { 294 {
271 for (let element of this.getElements(prefix, subtree, styles, targets)) 295 for (let element of this.getElements(prefix, subtree, styles, targets))
272 yield [makeSelector(element, ""), element]; 296 yield [makeSelector(element), element];
273 }, 297 },
274 298
275 /** 299 /**
276 * Generator function returning selected elements. 300 * Generator function returning selected elements.
277 * @param {string} prefix the prefix for the selector. 301 * @param {string} prefix the prefix for the selector.
278 * @param {Node} subtree the subtree we work on. 302 * @param {Node} subtree the subtree we work on.
279 * @param {StringifiedStyle[]} styles the stringified style objects. 303 * @param {StringifiedStyle[]} styles the stringified style objects.
280 * @param {Node[]} [targets] the nodes we are interested in. 304 * @param {Node[]} [targets] the nodes we are interested in.
281 */ 305 */
282 *getElements(prefix, subtree, styles, targets) 306 *getElements(prefix, subtree, styles, targets)
(...skipping 17 matching lines...) Expand all
300 let iter = evaluate(this._innerSelectors, 0, "", element, styles, 324 let iter = evaluate(this._innerSelectors, 0, "", element, styles,
301 targets); 325 targets);
302 for (let selector of iter) 326 for (let selector of iter)
303 { 327 {
304 if (selector == null) 328 if (selector == null)
305 yield null; 329 yield null;
306 else if (scopedQuerySelector(element, selector)) 330 else if (scopedQuerySelector(element, selector))
307 yield element; 331 yield element;
308 } 332 }
309 yield null; 333 yield null;
334
335 if (testInfo)
336 testInfo.lastProcessedElements.add(element);
310 } 337 }
311 } 338 }
312 } 339 }
313 }; 340 };
314 341
315 function ContainsSelector(textContent) 342 function ContainsSelector(textContent)
316 { 343 {
317 this._regexp = makeRegExpParameter(textContent); 344 this._regexp = makeRegExpParameter(textContent);
318 } 345 }
319 346
320 ContainsSelector.prototype = { 347 ContainsSelector.prototype = {
321 requiresHiding: true,
322 dependsOnDOM: true, 348 dependsOnDOM: true,
323 dependsOnCharacterData: true, 349 dependsOnCharacterData: true,
324 350
325 *getSelectors(prefix, subtree, styles, targets) 351 *getSelectors(prefix, subtree, styles, targets)
326 { 352 {
327 for (let element of this.getElements(prefix, subtree, styles, targets)) 353 for (let element of this.getElements(prefix, subtree, styles, targets))
328 yield [makeSelector(element, ""), subtree]; 354 yield [makeSelector(element), subtree];
329 }, 355 },
330 356
331 *getElements(prefix, subtree, styles, targets) 357 *getElements(prefix, subtree, styles, targets)
332 { 358 {
333 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 359 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
334 prefix + "*" : prefix; 360 prefix + "*" : prefix;
335 361
336 let elements = scopedQuerySelectorAll(subtree, actualPrefix); 362 let elements = scopedQuerySelectorAll(subtree, actualPrefix);
363
337 if (elements) 364 if (elements)
338 { 365 {
366 let lastRoot = null;
339 for (let element of elements) 367 for (let element of elements)
340 { 368 {
369 // For a filter like div:-abp-contains(Hello) and a subtree like
370 // <div id="a"><div id="b"><div id="c">Hello</div></div></div>
371 // we're only interested in div#a
372 if (lastRoot && lastRoot.contains(element))
373 {
374 yield null;
375 continue;
376 }
377
378 lastRoot = element;
379
341 if (targets && !targets.some(target => element.contains(target) || 380 if (targets && !targets.some(target => element.contains(target) ||
342 target.contains(element))) 381 target.contains(element)))
343 { 382 {
344 yield null; 383 yield null;
345 continue; 384 continue;
346 } 385 }
347 386
348 if (this._regexp && this._regexp.test(element.textContent)) 387 if (this._regexp && this._regexp.test(element.textContent))
349 yield element; 388 yield element;
350 else 389 else
351 yield null; 390 yield null;
391
392 if (testInfo)
393 testInfo.lastProcessedElements.add(element);
352 } 394 }
353 } 395 }
354 } 396 }
355 }; 397 };
356 398
357 function PropsSelector(propertyExpression) 399 function PropsSelector(propertyExpression)
358 { 400 {
359 let regexpString; 401 let regexpString;
360 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 402 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
361 propertyExpression[propertyExpression.length - 1] == "/") 403 propertyExpression[propertyExpression.length - 1] == "/")
362 { 404 {
363 regexpString = propertyExpression.slice(1, -1) 405 regexpString = propertyExpression.slice(1, -1)
364 .replace("\\7B ", "{").replace("\\7D ", "}"); 406 .replace("\\7B ", "{").replace("\\7D ", "}");
365 } 407 }
366 else 408 else
367 regexpString = filterToRegExp(propertyExpression); 409 regexpString = filterToRegExp(propertyExpression);
368 410
369 this._regexp = new RegExp(regexpString, "i"); 411 this._regexp = new RegExp(regexpString, "i");
370 } 412 }
371 413
372 PropsSelector.prototype = { 414 PropsSelector.prototype = {
373 preferHideWithSelector: true,
374 dependsOnStyles: true, 415 dependsOnStyles: true,
375 416
376 *findPropsSelectors(styles, prefix, regexp) 417 *findPropsSelectors(styles, prefix, regexp)
377 { 418 {
378 for (let style of styles) 419 for (let style of styles)
379 if (regexp.test(style.style)) 420 if (regexp.test(style.style))
380 for (let subSelector of style.subSelectors) 421 for (let subSelector of style.subSelectors)
381 { 422 {
382 if (subSelector.startsWith("*") && 423 if (subSelector.startsWith("*") &&
383 !incompletePrefixRegexp.test(prefix)) 424 !incompletePrefixRegexp.test(prefix))
384 { 425 {
385 subSelector = subSelector.substr(1); 426 subSelector = subSelector.substr(1);
386 } 427 }
387 let idx = subSelector.lastIndexOf("::"); 428 let idx = subSelector.lastIndexOf("::");
388 if (idx != -1) 429 if (idx != -1)
389 subSelector = subSelector.substr(0, idx); 430 subSelector = subSelector.substr(0, idx);
390 yield prefix + subSelector; 431 yield prefix + subSelector;
391 } 432 }
392 }, 433 },
393 434
394 *getSelectors(prefix, subtree, styles, targets) 435 *getSelectors(prefix, subtree, styles, targets)
395 { 436 {
396 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 437 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
397 yield [selector, subtree]; 438 yield [selector, subtree];
398 } 439 }
399 }; 440 };
400 441
401 function isSelectorHidingOnlyPattern(pattern) 442 function Pattern(selectors, text)
402 { 443 {
403 return pattern.selectors.some(s => s.preferHideWithSelector) && 444 this.selectors = selectors;
404 !pattern.selectors.some(s => s.requiresHiding); 445 this.text = text;
405 } 446 }
406 447
407 function patternDependsOnStyles(pattern) 448 Pattern.prototype = {
408 { 449 get dependsOnStyles()
409 return pattern.selectors.some(s => s.dependsOnStyles); 450 {
410 } 451 return getCachedPropertyValue(
411 452 this, "_dependsOnStyles",
412 function patternDependsOnDOM(pattern) 453 () => this.selectors.some(selector => selector.dependsOnStyles)
413 { 454 );
414 return pattern.selectors.some(s => s.dependsOnDOM); 455 },
415 } 456
416 457 get dependsOnDOM()
417 function patternDependsOnStylesAndDOM(pattern) 458 {
418 { 459 return getCachedPropertyValue(
419 return pattern.selectors.some(s => s.dependsOnStyles && s.dependsOnDOM); 460 this, "_dependsOnDOM",
420 } 461 () => this.selectors.some(selector => selector.dependsOnDOM)
421 462 );
422 function patternMaybeDependsOnAttributes(pattern) 463 },
423 { 464
424 // Observe changes to attributes if either there's a plain selector that 465 get dependsOnStylesAndDOM()
425 // looks like an ID selector, class selector, or attribute selector in one of 466 {
426 // the patterns (e.g. "a[href='https://example.com/']") 467 return getCachedPropertyValue(
427 // or there's a properties selector nested inside a has selector 468 this, "_dependsOnStylesAndDOM",
428 // (e.g. "div:-abp-has(:-abp-properties(color: blue))") 469 () => this.selectors.some(selector => selector.dependsOnStyles &&
429 return pattern.selectors.some( 470 selector.dependsOnDOM)
430 selector => selector.maybeDependsOnAttributes || 471 );
431 (selector instanceof HasSelector && 472 },
432 selector.dependsOnStyles) 473
433 ); 474 get maybeDependsOnAttributes()
434 } 475 {
435 476 // Observe changes to attributes if either there's a plain selector that
436 function patternDependsOnCharacterData(pattern) 477 // looks like an ID selector, class selector, or attribute selector in one
437 { 478 // of the patterns (e.g. "a[href='https://example.com/']")
438 // Observe changes to character data only if there's a contains selector in 479 // or there's a properties selector nested inside a has selector
439 // one of the patterns. 480 // (e.g. "div:-abp-has(:-abp-properties(color: blue))")
440 return pattern.selectors.some(selector => selector.dependsOnCharacterData); 481 return getCachedPropertyValue(
441 } 482 this, "_maybeDependsOnAttributes",
442 483 () => this.selectors.some(
443 function patternMatchesMutationTypes(pattern, mutationTypes) 484 selector => selector.maybeDependsOnAttributes ||
444 { 485 (selector instanceof HasSelector &&
445 return mutationTypes.has("childList") || 486 selector.dependsOnStyles)
446 (mutationTypes.has("attributes") && 487 )
447 patternMaybeDependsOnAttributes(pattern)) || 488 );
448 (mutationTypes.has("characterData") && 489 },
449 patternDependsOnCharacterData(pattern)); 490
450 } 491 get dependsOnCharacterData()
492 {
493 // Observe changes to character data only if there's a contains selector in
494 // one of the patterns.
495 return getCachedPropertyValue(
496 this, "_dependsOnCharacterData",
497 () => this.selectors.some(selector => selector.dependsOnCharacterData)
498 );
499 },
500
501 get maybeContainsSiblingCombinators()
502 {
503 return getCachedPropertyValue(
504 this, "_maybeContainsSiblingCombinators",
505 () => this.selectors.some(selector =>
506 selector.maybeContainsSiblingCombinators)
507 );
508 },
509
510 matchesMutationTypes(mutationTypes)
511 {
512 let mutationTypeMatchMap = getCachedPropertyValue(
513 this, "_mutationTypeMatchMap",
514 () => new Map([
515 // All types of DOM-dependent patterns are affected by mutations of
516 // type "childList".
517 ["childList", true],
518 ["attributes", this.maybeDependsOnAttributes],
519 ["characterData", this.dependsOnCharacterData]
520 ])
521 );
522
523 for (let mutationType of mutationTypes)
524 {
525 if (mutationTypeMatchMap.get(mutationType))
526 return true;
527 }
528
529 return false;
530 }
531 };
451 532
452 function extractMutationTypes(mutations) 533 function extractMutationTypes(mutations)
453 { 534 {
454 let types = new Set(); 535 let types = new Set();
455 536
456 for (let mutation of mutations) 537 for (let mutation of mutations)
457 { 538 {
458 types.add(mutation.type); 539 types.add(mutation.type);
459 540
460 // There are only 3 types of mutations: "attributes", "characterData", and 541 // There are only 3 types of mutations: "attributes", "characterData", and
(...skipping 13 matching lines...) Expand all
474 let targets = new Set(); 555 let targets = new Set();
475 556
476 for (let mutation of mutations) 557 for (let mutation of mutations)
477 { 558 {
478 if (mutation.type == "childList") 559 if (mutation.type == "childList")
479 { 560 {
480 // When new nodes are added, we're interested in the added nodes rather 561 // When new nodes are added, we're interested in the added nodes rather
481 // than the parent. 562 // than the parent.
482 for (let node of mutation.addedNodes) 563 for (let node of mutation.addedNodes)
483 targets.add(node); 564 targets.add(node);
484
485 // Ideally we would also be interested in removed nodes, but since we
486 // never unhide an element once hidden we can simply ignore any removed
487 // nodes. Note that this will change once we start using CSS selectors
488 // for -abp-has and -abp-contains, i.e. we'll have to remove the
489 // selectors for any removed nodes.
490 } 565 }
491 else 566 else
492 { 567 {
493 targets.add(mutation.target); 568 targets.add(mutation.target);
494 } 569 }
495 } 570 }
496 571
497 return [...targets]; 572 return [...targets];
498 } 573 }
499 574
500 function filterPatterns(patterns, {stylesheets, mutations}) 575 function filterPatterns(patterns, {stylesheets, mutations})
501 { 576 {
502 if (!stylesheets && !mutations) 577 if (!stylesheets && !mutations)
503 return patterns.slice(); 578 return patterns.slice();
504 579
505 let mutationTypes = mutations ? extractMutationTypes(mutations) : null; 580 let mutationTypes = mutations ? extractMutationTypes(mutations) : null;
506 581
507 return patterns.filter( 582 return patterns.filter(
508 pattern => (stylesheets && patternDependsOnStyles(pattern)) || 583 pattern => (stylesheets && pattern.dependsOnStyles) ||
509 (mutations && patternDependsOnDOM(pattern) && 584 (mutations && pattern.dependsOnDOM &&
510 patternMatchesMutationTypes(pattern, mutationTypes)) 585 pattern.matchesMutationTypes(mutationTypes))
511 ); 586 );
512 } 587 }
513 588
514 function shouldObserveAttributes(patterns) 589 function shouldObserveAttributes(patterns)
515 { 590 {
516 return patterns.some(patternMaybeDependsOnAttributes); 591 return patterns.some(pattern => pattern.maybeDependsOnAttributes);
517 } 592 }
518 593
519 function shouldObserveCharacterData(patterns) 594 function shouldObserveCharacterData(patterns)
520 { 595 {
521 return patterns.some(patternDependsOnCharacterData); 596 return patterns.some(pattern => pattern.dependsOnCharacterData);
522 } 597 }
523 598
524 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) 599 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc)
525 { 600 {
526 this.document = document; 601 this.document = document;
527 this.addSelectorsFunc = addSelectorsFunc; 602 this.addSelectorsFunc = addSelectorsFunc;
528 this.hideElemsFunc = hideElemsFunc; 603 this.hideElemsFunc = hideElemsFunc;
529 this.observer = new MutationObserver(this.observe.bind(this)); 604 this.observer = new MutationObserver(this.observe.bind(this));
605 this.useInlineStyles = true;
530 } 606 }
531 607
532 ElemHideEmulation.prototype = { 608 ElemHideEmulation.prototype = {
533 isSameOrigin(stylesheet) 609 isSameOrigin(stylesheet)
534 { 610 {
535 try 611 try
536 { 612 {
537 return new URL(stylesheet.href).origin == this.document.location.origin; 613 return new URL(stylesheet.href).origin == this.document.location.origin;
538 } 614 }
539 catch (e) 615 catch (e)
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
616 * @param {MutationRecord[]} [mutations] 692 * @param {MutationRecord[]} [mutations]
617 * The list of DOM mutations that have been applied to the document and 693 * The list of DOM mutations that have been applied to the document and
618 * made reprocessing necessary. This parameter shouldn't be passed in for 694 * made reprocessing necessary. This parameter shouldn't be passed in for
619 * the initial processing, the entire document will be considered 695 * the initial processing, the entire document will be considered
620 * then and all rules, including the ones not dependent on the DOM. 696 * then and all rules, including the ones not dependent on the DOM.
621 * @param {function} [done] 697 * @param {function} [done]
622 * Callback to call when done. 698 * Callback to call when done.
623 */ 699 */
624 _addSelectors(stylesheets, mutations, done) 700 _addSelectors(stylesheets, mutations, done)
625 { 701 {
702 if (testInfo)
703 testInfo.lastProcessedElements.clear();
704
626 let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); 705 let patterns = filterPatterns(this.patterns, {stylesheets, mutations});
627 706
628 let selectors = []; 707 let selectors = [];
629 let selectorFilters = []; 708 let selectorFilters = [];
630 709
631 let elements = []; 710 let elements = [];
632 let elementFilters = []; 711 let elementFilters = [];
633 712
634 let cssStyles = []; 713 let cssStyles = [];
635 714
636 // If neither any style sheets nor any DOM mutations have been specified, 715 // If neither any style sheets nor any DOM mutations have been specified,
637 // do full processing. 716 // do full processing.
638 if (!stylesheets && !mutations) 717 if (!stylesheets && !mutations)
639 stylesheets = this.document.styleSheets; 718 stylesheets = this.document.styleSheets;
640 719
641 // If there are any DOM mutations and any of the patterns depends on both 720 // If there are any DOM mutations and any of the patterns depends on both
642 // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the 721 // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the
643 // rules in every style sheet in the document, because we need to run 722 // rules in every style sheet in the document, because we need to run
644 // querySelectorAll afterwards. On the other hand, if we only have patterns 723 // querySelectorAll afterwards. On the other hand, if we only have patterns
645 // that depend on either styles or DOM both not both 724 // that depend on either styles or DOM both not both
646 // (e.g. -abp-properties or -abp-contains), we can skip this part. 725 // (e.g. -abp-properties or -abp-contains), we can skip this part.
647 if (mutations && patterns.some(patternDependsOnStylesAndDOM)) 726 if (mutations && patterns.some(pattern => pattern.dependsOnStylesAndDOM))
648 stylesheets = this.document.styleSheets; 727 stylesheets = this.document.styleSheets;
649 728
650 for (let stylesheet of stylesheets || []) 729 for (let stylesheet of stylesheets || [])
651 { 730 {
652 // Explicitly ignore third-party stylesheets to ensure consistent behavior 731 // Explicitly ignore third-party stylesheets to ensure consistent behavior
653 // between Firefox and Chrome. 732 // between Firefox and Chrome.
654 if (!this.isSameOrigin(stylesheet)) 733 if (!this.isSameOrigin(stylesheet))
655 continue; 734 continue;
656 735
657 let rules = stylesheet.cssRules; 736 let rules;
737 try
738 {
739 rules = stylesheet.cssRules;
740 }
741 catch (e)
742 {
743 // On Firefox, there is a chance that an InvalidAccessError
744 // get thrown when accessing cssRules. Just skip the stylesheet
745 // in that case.
746 // See https://searchfox.org/mozilla-central/rev/f65d7528e34ef1a7665b4a1 a7b7cdb1388fcd3aa/layout/style/StyleSheet.cpp#699
747 continue;
748 }
749
658 if (!rules) 750 if (!rules)
659 continue; 751 continue;
660 752
661 for (let rule of rules) 753 for (let rule of rules)
662 { 754 {
663 if (rule.type != rule.STYLE_RULE) 755 if (rule.type != rule.STYLE_RULE)
664 continue; 756 continue;
665 757
666 cssStyles.push(stringifyStyle(rule)); 758 cssStyles.push(stringifyStyle(rule));
667 } 759 }
668 } 760 }
669 761
670 let targets = extractMutationTargets(mutations); 762 let targets = extractMutationTargets(mutations);
671 763
672 let pattern = null; 764 let pattern = null;
673 let generator = null; 765 let generator = null;
674 766
675 let processPatterns = () => 767 let processPatterns = () =>
676 { 768 {
677 let cycleStart = performance.now(); 769 let cycleStart = performance.now();
678 770
679 if (!pattern) 771 if (!pattern)
680 { 772 {
681 if (!patterns.length) 773 if (!patterns.length)
682 { 774 {
683 this.addSelectorsFunc(selectors, selectorFilters); 775 if (selectors.length > 0)
684 this.hideElemsFunc(elements, elementFilters); 776 this.addSelectorsFunc(selectors, selectorFilters);
777 if (elements.length > 0)
778 this.hideElemsFunc(elements, elementFilters);
685 if (typeof done == "function") 779 if (typeof done == "function")
686 done(); 780 done();
687 return; 781 return;
688 } 782 }
689 783
690 pattern = patterns.shift(); 784 pattern = patterns.shift();
691 785
786 let evaluationTargets = targets;
787
788 // If the pattern appears to contain any sibling combinators, we can't
789 // easily optimize based on the mutation targets. Since this is a
790 // special case, skip the optimization. By setting it to null here we
791 // make sure we process the entire DOM.
792 if (pattern.maybeContainsSiblingCombinators)
793 evaluationTargets = null;
794
795 // Ignore mutation targets when using style sheets, because we may have
796 // to update all the CSS selectors.
797 if (!this.useInlineStyles)
798 evaluationTargets = null;
799
692 generator = evaluate(pattern.selectors, 0, "", 800 generator = evaluate(pattern.selectors, 0, "",
693 this.document, cssStyles, targets); 801 this.document, cssStyles, evaluationTargets);
694 } 802 }
695 for (let selector of generator) 803 for (let selector of generator)
696 { 804 {
697 if (selector != null) 805 if (selector != null)
698 { 806 {
699 if (isSelectorHidingOnlyPattern(pattern)) 807 if (!this.useInlineStyles)
700 { 808 {
701 selectors.push(selector); 809 selectors.push(selector);
702 selectorFilters.push(pattern.text); 810 selectorFilters.push(pattern.text);
703 } 811 }
704 else 812 else
705 { 813 {
706 for (let element of this.document.querySelectorAll(selector)) 814 for (let element of this.document.querySelectorAll(selector))
707 { 815 {
708 elements.push(element); 816 elements.push(element);
709 elementFilters.push(pattern.text); 817 elementFilters.push(pattern.text);
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
749 * for full reprocessing. 857 * for full reprocessing.
750 */ 858 */
751 queueFiltering(stylesheets, mutations) 859 queueFiltering(stylesheets, mutations)
752 { 860 {
753 let completion = () => 861 let completion = () =>
754 { 862 {
755 this._lastInvocation = performance.now(); 863 this._lastInvocation = performance.now();
756 this._filteringInProgress = false; 864 this._filteringInProgress = false;
757 if (this._scheduledProcessing) 865 if (this._scheduledProcessing)
758 { 866 {
759 let {stylesheets, mutations} = this._scheduledProcessing; 867 let params = Object.assign({}, this._scheduledProcessing);
760 this._scheduledProcessing = null; 868 this._scheduledProcessing = null;
761 this.queueFiltering(stylesheets, mutations); 869 this.queueFiltering(params.stylesheets, params.mutations);
762 } 870 }
763 }; 871 };
764 872
765 if (this._scheduledProcessing) 873 if (this._scheduledProcessing)
766 { 874 {
767 if (!stylesheets && !mutations) 875 if (!stylesheets && !mutations)
768 { 876 {
769 this._scheduledProcessing = {}; 877 this._scheduledProcessing = {};
770 } 878 }
771 else 879 else if (this._scheduledProcessing.stylesheets ||
880 this._scheduledProcessing.mutations)
772 { 881 {
773 if (stylesheets) 882 if (stylesheets)
774 { 883 {
775 if (!this._scheduledProcessing.stylesheets) 884 if (!this._scheduledProcessing.stylesheets)
776 this._scheduledProcessing.stylesheets = []; 885 this._scheduledProcessing.stylesheets = [];
777 this._scheduledProcessing.stylesheets.push(...stylesheets); 886 this._scheduledProcessing.stylesheets.push(...stylesheets);
778 } 887 }
779 if (mutations) 888 if (mutations)
780 { 889 {
781 if (!this._scheduledProcessing.mutations) 890 if (!this._scheduledProcessing.mutations)
782 this._scheduledProcessing.mutations = []; 891 this._scheduledProcessing.mutations = [];
783 this._scheduledProcessing.mutations.push(...mutations); 892 this._scheduledProcessing.mutations.push(...mutations);
784 } 893 }
785 } 894 }
786 } 895 }
787 else if (this._filteringInProgress) 896 else if (this._filteringInProgress)
788 { 897 {
789 this._scheduledProcessing = {stylesheets, mutations}; 898 this._scheduledProcessing = {stylesheets, mutations};
790 } 899 }
791 else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) 900 else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
792 { 901 {
793 this._scheduledProcessing = {stylesheets, mutations}; 902 this._scheduledProcessing = {stylesheets, mutations};
794 setTimeout(() => 903 setTimeout(() =>
795 { 904 {
796 let {stylesheets, mutations} = this._scheduledProcessing; 905 let params = Object.assign({}, this._scheduledProcessing);
797 this._filteringInProgress = true; 906 this._filteringInProgress = true;
798 this._scheduledProcessing = null; 907 this._scheduledProcessing = null;
799 this._addSelectors(stylesheets, mutations, completion); 908 this._addSelectors(params.stylesheets, params.mutations, completion);
800 }, 909 },
801 MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation)); 910 MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation));
802 } 911 }
803 else if (this.document.readyState == "loading") 912 else if (this.document.readyState == "loading")
804 { 913 {
805 this._scheduledProcessing = {stylesheets, mutations}; 914 this._scheduledProcessing = {stylesheets, mutations};
806 let handler = () => 915 let handler = () =>
807 { 916 {
808 document.removeEventListener("DOMContentLoaded", handler); 917 this.document.removeEventListener("DOMContentLoaded", handler);
809 let {stylesheets, mutations} = this._scheduledProcessing; 918 let params = Object.assign({}, this._scheduledProcessing);
810 this._filteringInProgress = true; 919 this._filteringInProgress = true;
811 this._scheduledProcessing = null; 920 this._scheduledProcessing = null;
812 this._addSelectors(stylesheets, mutations, completion); 921 this._addSelectors(params.stylesheets, params.mutations, completion);
813 }; 922 };
814 document.addEventListener("DOMContentLoaded", handler); 923 this.document.addEventListener("DOMContentLoaded", handler);
815 } 924 }
816 else 925 else
817 { 926 {
818 this._filteringInProgress = true; 927 this._filteringInProgress = true;
819 this._addSelectors(stylesheets, mutations, completion); 928 this._addSelectors(stylesheets, mutations, completion);
820 } 929 }
821 }, 930 },
822 931
823 onLoad(event) 932 onLoad(event)
824 { 933 {
825 let stylesheet = event.target.sheet; 934 let stylesheet = event.target.sheet;
826 if (stylesheet) 935 if (stylesheet)
827 this.queueFiltering([stylesheet]); 936 this.queueFiltering([stylesheet]);
828 }, 937 },
829 938
830 observe(mutations) 939 observe(mutations)
831 { 940 {
941 if (testInfo)
942 {
943 // In test mode, filter out any mutations likely done by us
944 // (i.e. style="display: none !important"). This makes it easier to
945 // observe how the code responds to DOM mutations.
946 mutations = mutations.filter(
947 ({type, attributeName, target: {style: newValue}, oldValue}) =>
948 !(type == "attributes" && attributeName == "style" &&
949 newValue.display == "none" && oldValue.display != "none")
950 );
951
952 if (mutations.length == 0)
953 return;
954 }
hub 2018/05/24 19:19:09 ... and this :-/ There should be a better way to
Manish Jethani 2018/05/25 07:21:25 OK, do you have any suggestions? This is only done
955
832 this.queueFiltering(null, mutations); 956 this.queueFiltering(null, mutations);
833 }, 957 },
834 958
835 apply(patterns) 959 apply(patterns)
836 { 960 {
837 this.patterns = []; 961 this.patterns = [];
838 for (let pattern of patterns) 962 for (let pattern of patterns)
839 { 963 {
840 let selectors = this.parseSelector(pattern.selector); 964 let selectors = this.parseSelector(pattern.selector);
841 if (selectors != null && selectors.length > 0) 965 if (selectors != null && selectors.length > 0)
842 this.patterns.push({selectors, text: pattern.text}); 966 this.patterns.push(new Pattern(selectors, pattern.text));
843 } 967 }
844 968
845 if (this.patterns.length > 0) 969 if (this.patterns.length > 0)
846 { 970 {
847 this.queueFiltering(); 971 this.queueFiltering();
848 this.observer.observe( 972 this.observer.observe(
849 this.document, 973 this.document,
850 { 974 {
851 childList: true, 975 childList: true,
852 attributes: shouldObserveAttributes(this.patterns), 976 attributes: shouldObserveAttributes(this.patterns),
853 characterData: shouldObserveCharacterData(this.patterns), 977 characterData: shouldObserveCharacterData(this.patterns),
854 subtree: true 978 subtree: true
855 } 979 }
856 ); 980 );
857 this.document.addEventListener("load", this.onLoad.bind(this), true); 981 this.document.addEventListener("load", this.onLoad.bind(this), true);
858 } 982 }
859 } 983 }
860 }; 984 };
861 985
862 exports.ElemHideEmulation = ElemHideEmulation; 986 exports.ElemHideEmulation = ElemHideEmulation;
LEFTRIGHT

Powered by Google App Engine
This is Rietveld