| Left: | ||
| Right: |
| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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 Loading... | |
| 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 Loading... | |
| 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 Loading... | |
| 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 Loading... | |
| 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 }; |
| OLD | NEW |