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

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

Issue 29481700: Issue 5339 - Properly select element for pseudo-element (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Left Patch Set: Created July 6, 2017, 8:16 p.m.
Right Patch Set: Reworked based on the new implementation proposal. Created Aug. 8, 2017, 4:56 p.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-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
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 /* globals filterToRegExp */ 18 /* globals filterToRegExp */
19 19
20 "use strict"; 20 "use strict";
21 21
22 const MIN_INVOCATION_INTERVAL = 3000;
22 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
23
24 let reportError = () => {};
25 24
26 function splitSelector(selector) 25 function splitSelector(selector)
27 { 26 {
28 if (selector.indexOf(",") == -1) 27 if (selector.indexOf(",") == -1)
29 return [selector]; 28 return [selector];
30 29
31 let selectors = []; 30 let selectors = [];
32 let start = 0; 31 let start = 0;
33 let level = 0; 32 let level = 0;
34 let sep = ""; 33 let sep = "";
35 34
36 for (let i = 0; i < selector.length; i++) 35 for (let i = 0; i < selector.length; i++)
37 { 36 {
38 let chr = selector[i]; 37 let chr = selector[i];
39 38
40 if (chr == "\\") // ignore escaped characters 39 if (chr == "\\") // ignore escaped characters
41 i++; 40 i++;
42 else if (chr == sep) // don't split within quoted text 41 else if (chr == sep) // don't split within quoted text
43 sep = ""; // e.g. [attr=","] 42 sep = ""; // e.g. [attr=","]
44 else if (sep == "") 43 else if (sep == "")
45 { 44 {
46 if (chr == '"' || chr == "'") 45 if (chr == '"' || chr == "'")
47 sep = chr; 46 sep = chr;
48 else if (chr == "(") // don't split between parentheses 47 else if (chr == "(") // don't split between parentheses
49 level++; // e.g. :matches(div,span) 48 level++; // e.g. :matches(div,span)
50 else if (chr == ")") 49 else if (chr == ")")
51 level = Math.max(0, level - 1); 50 level = Math.max(0, level - 1);
52 else if (chr == "," && level == 0) 51 else if (chr == "," && level == 0)
53 { 52 {
54 selectors.push(selector.substring(start, i)); 53 selectors.push(selector.substring(start, i).trim());
55 start = i + 1; 54 start = i + 1;
56 } 55 }
57 } 56 }
58 } 57 }
59 58
60 selectors.push(selector.substring(start)); 59 selectors.push(selector.substring(start).trim());
61 return selectors; 60 return selectors;
62 } 61 }
63 62
64 /** Return position of node from parent. 63 /** Return position of node from parent.
65 * @param {Node} node the node to find the position of. 64 * @param {Node} node the node to find the position of.
66 * @return {number} One-based index like for :nth-child(), or 0 on error. 65 * @return {number} One-based index like for :nth-child(), or 0 on error.
67 */ 66 */
68 function positionInParent(node) 67 function positionInParent(node)
69 { 68 {
70 let {children} = node.parentNode; 69 let {children} = node.parentNode;
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after
183 * @param {Node} subtree the subtree we work on. 182 * @param {Node} subtree the subtree we work on.
184 * @param {StringifiedStyle[]} styles the stringified style objects. 183 * @param {StringifiedStyle[]} styles the stringified style objects.
185 */ 184 */
186 *getSelectors(prefix, subtree, styles) 185 *getSelectors(prefix, subtree, styles)
187 { 186 {
188 yield [prefix + this._selector, subtree]; 187 yield [prefix + this._selector, subtree];
189 } 188 }
190 }; 189 };
191 190
192 const incompletePrefixRegexp = /[\s>+~]$/; 191 const incompletePrefixRegexp = /[\s>+~]$/;
192 const relativeSelector = /^[\s>+~]/;
193 193
194 function HasSelector(selectors) 194 function HasSelector(selectors)
195 { 195 {
196 this._innerSelectors = selectors; 196 this._innerSelectors = selectors;
197 } 197 }
198 198
199 HasSelector.prototype = { 199 HasSelector.prototype = {
200 requiresHiding: true, 200 requiresHiding: true,
201
202 get dependsOnStyles()
203 {
204 return this._innerSelectors.some(selector => selector.dependsOnStyles);
205 },
201 206
202 *getSelectors(prefix, subtree, styles) 207 *getSelectors(prefix, subtree, styles)
203 { 208 {
204 for (let element of this.getElements(prefix, subtree, styles)) 209 for (let element of this.getElements(prefix, subtree, styles))
205 yield [makeSelector(element, ""), element]; 210 yield [makeSelector(element, ""), element];
206 }, 211 },
207 212
208 /** 213 /**
209 * Generator function returning selected elements. 214 * Generator function returning selected elements.
210 * @param {string} prefix the prefix for the selector. 215 * @param {string} prefix the prefix for the selector.
211 * @param {Node} subtree the subtree we work on. 216 * @param {Node} subtree the subtree we work on.
212 * @param {StringifiedStyle[]} styles the stringified style objects. 217 * @param {StringifiedStyle[]} styles the stringified style objects.
213 */ 218 */
214 *getElements(prefix, subtree, styles) 219 *getElements(prefix, subtree, styles)
215 { 220 {
216 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 221 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
217 prefix + "*" : prefix; 222 prefix + "*" : prefix;
218 let elements = subtree.querySelectorAll(actualPrefix); 223 let elements = subtree.querySelectorAll(actualPrefix);
219 for (let element of elements) 224 for (let element of elements)
220 { 225 {
221 let newPrefix = makeSelector(element, ""); 226 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
222 let iter = evaluate(this._innerSelectors, 0, newPrefix + " ",
223 element, styles);
224 for (let selector of iter) 227 for (let selector of iter)
225 // we insert a space between the two. It becomes a no-op if selector 228 {
226 // doesn't have a combinator 229 if (relativeSelector.test(selector))
227 if (subtree.querySelector(selector)) 230 selector = ":scope" + selector;
231 if (element.querySelector(selector))
228 yield element; 232 yield element;
233 }
229 } 234 }
230 } 235 }
231 }; 236 };
232 237
233 function ContainsSelector(textContent) 238 function ContainsSelector(textContent)
234 { 239 {
235 this._text = textContent; 240 this._text = textContent;
236 } 241 }
237 242
238 ContainsSelector.prototype = { 243 ContainsSelector.prototype = {
239 requiresHiding: true, 244 requiresHiding: true,
240 245
241 *getSelectors(prefix, subtree, stylesheet) 246 *getSelectors(prefix, subtree, stylesheet)
242 { 247 {
243 for (let element of this.getElements(prefix, subtree, stylesheet)) 248 for (let element of this.getElements(prefix, subtree, stylesheet))
244 yield [makeSelector(element, ""), subtree]; 249 yield [makeSelector(element, ""), subtree];
245 }, 250 },
246 251
247 *getElements(prefix, subtree, stylesheet) 252 *getElements(prefix, subtree, stylesheet)
248 { 253 {
249 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 254 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
250 prefix + "*" : prefix; 255 prefix + "*" : prefix;
251 let elements = subtree.querySelectorAll(actualPrefix); 256 let elements = subtree.querySelectorAll(actualPrefix);
252 for (let element of elements) 257 for (let element of elements)
253 if (element.textContent.includes(this._text)) 258 if (element.textContent.includes(this._text))
254 yield element; 259 yield element;
255 } 260 }
256 }; 261 };
257 262
258 function PropsSelector(propertyExpression, pseudoElem) 263 function PropsSelector(propertyExpression)
259 { 264 {
260 let regexpString; 265 let regexpString;
261 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 266 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
262 propertyExpression[propertyExpression.length - 1] == "/") 267 propertyExpression[propertyExpression.length - 1] == "/")
263 { 268 {
264 regexpString = propertyExpression.slice(1, -1) 269 regexpString = propertyExpression.slice(1, -1)
265 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 270 .replace("\\x7B ", "{").replace("\\x7D ", "}");
266 } 271 }
267 else 272 else
268 regexpString = filterToRegExp(propertyExpression); 273 regexpString = filterToRegExp(propertyExpression);
269 274
270 this._regexp = new RegExp(regexpString, "i"); 275 this._regexp = new RegExp(regexpString, "i");
271 this._pseudoElem = pseudoElem || "";
272 } 276 }
273 277
274 PropsSelector.prototype = { 278 PropsSelector.prototype = {
275 preferHideWithSelector: true, 279 preferHideWithSelector: true,
280 dependsOnStyles: true,
276 281
277 *findPropsSelectors(styles, prefix, regexp) 282 *findPropsSelectors(styles, prefix, regexp)
278 { 283 {
284 let actualPrefix = (prefix && !incompletePrefixRegexp.test(prefix)) ?
285 prefix + " " : prefix;
279 for (let style of styles) 286 for (let style of styles)
280 if (regexp.test(style.style)) 287 if (regexp.test(style.style))
281 for (let subSelector of style.subSelectors) 288 for (let subSelector of style.subSelectors)
282 { 289 {
283 if (this._pseudoElem) 290 if (subSelector == "*")
291 subSelector = "";
292 else
284 { 293 {
285 let idx = subSelector.lastIndexOf(this._pseudoElem); 294 let idx = subSelector.lastIndexOf("::");
286 if (idx != subSelector.length - this._pseudoElem.length) 295 if (idx != -1)
287 continue; 296 subSelector = subSelector.substr(0, idx);
288 if (idx > 0 && subSelector[idx - 1] == ":")
289 idx--;
290 subSelector = subSelector.substring(0, idx);
291 } 297 }
292 yield prefix + subSelector; 298 yield actualPrefix + subSelector;
293 } 299 }
294 }, 300 },
295 301
296 *getSelectors(prefix, subtree, styles) 302 *getSelectors(prefix, subtree, styles)
297 { 303 {
298 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 304 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
299 yield [selector, subtree]; 305 yield [selector, subtree];
300 } 306 }
301 }; 307 };
302 308
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
346 if (!content) 352 if (!content)
347 { 353 {
348 this.window.console.error( 354 this.window.console.error(
349 new SyntaxError("Failed to parse Adblock Plus " + 355 new SyntaxError("Failed to parse Adblock Plus " +
350 `selector ${selector} ` + 356 `selector ${selector} ` +
351 "due to unmatched parentheses.")); 357 "due to unmatched parentheses."));
352 return null; 358 return null;
353 } 359 }
354 if (match[1] == "properties") 360 if (match[1] == "properties")
355 selectors.push(new PropsSelector(content.text)); 361 selectors.push(new PropsSelector(content.text));
356 else if (match[1] == "properties-before")
357 selectors.push(new PropsSelector(content.text, ":before"));
358 else if (match[1] == "properties-after")
359 selectors.push(new PropsSelector(content.text, ":after"));
360 else if (match[1] == "has") 362 else if (match[1] == "has")
361 { 363 {
362 let hasSelectors = this.parseSelector(content.text); 364 let hasSelectors = this.parseSelector(content.text);
363 if (hasSelectors == null) 365 if (hasSelectors == null)
364 return null; 366 return null;
365 selectors.push(new HasSelector(hasSelectors)); 367 selectors.push(new HasSelector(hasSelectors));
366 } 368 }
367 else if (match[1] == "contains") 369 else if (match[1] == "contains")
368 selectors.push(new ContainsSelector(content.text)); 370 selectors.push(new ContainsSelector(content.text));
369 else 371 else
(...skipping 16 matching lines...) Expand all
386 { 388 {
387 this.window.console.error( 389 this.window.console.error(
388 new SyntaxError("Failed to parse Adblock Plus " + 390 new SyntaxError("Failed to parse Adblock Plus " +
389 `selector ${selector}, can't ` + 391 `selector ${selector}, can't ` +
390 "have a lonely :-abp-contains().")); 392 "have a lonely :-abp-contains()."));
391 return null; 393 return null;
392 } 394 }
393 return selectors; 395 return selectors;
394 }, 396 },
395 397
398 _lastInvocation: 0,
399
400 /**
401 * Processes the current document and applies all rules to it.
402 * @param {CSSStyleSheet[]} [stylesheets]
403 * The list of new stylesheets that have been added to the document and
404 * made reprocessing necessary. This parameter shouldn't be passed in for
405 * the initial processing, all of document's stylesheets will be considered
406 * then and all rules, including the ones not dependent on styles.
407 */
396 addSelectors(stylesheets) 408 addSelectors(stylesheets)
397 { 409 {
410 this._lastInvocation = Date.now();
411
398 let selectors = []; 412 let selectors = [];
399 let selectorFilters = []; 413 let selectorFilters = [];
400 414
401 let elements = []; 415 let elements = [];
402 let elementFilters = []; 416 let elementFilters = [];
403 417
404 let cssStyles = []; 418 let cssStyles = [];
419
420 let stylesheetOnlyChange = !!stylesheets;
421 if (!stylesheets)
422 stylesheets = this.window.document.styleSheets;
405 423
406 // Chrome < 51 doesn't have an iterable StyleSheetList 424 // Chrome < 51 doesn't have an iterable StyleSheetList
407 // https://issues.adblockplus.org/ticket/5381 425 // https://issues.adblockplus.org/ticket/5381
408 for (let i = 0; i < stylesheets.length; i++) 426 for (let i = 0; i < stylesheets.length; i++)
409 { 427 {
410 let stylesheet = stylesheets[i]; 428 let stylesheet = stylesheets[i];
411 // Explicitly ignore third-party stylesheets to ensure consistent behavior 429 // Explicitly ignore third-party stylesheets to ensure consistent behavior
412 // between Firefox and Chrome. 430 // between Firefox and Chrome.
413 if (!this.isSameOrigin(stylesheet)) 431 if (!this.isSameOrigin(stylesheet))
414 continue; 432 continue;
415 433
416 let rules = stylesheet.cssRules; 434 let rules = stylesheet.cssRules;
417 if (!rules) 435 if (!rules)
418 continue; 436 continue;
419 437
420 for (let rule of rules) 438 for (let rule of rules)
421 { 439 {
422 if (rule.type != rule.STYLE_RULE) 440 if (rule.type != rule.STYLE_RULE)
423 continue; 441 continue;
424 442
425 cssStyles.push(stringifyStyle(rule)); 443 cssStyles.push(stringifyStyle(rule));
426 } 444 }
427 } 445 }
428 446
429 let {document} = this.window; 447 let {document} = this.window;
430 for (let pattern of this.patterns) 448 for (let pattern of this.patterns)
431 { 449 {
450 if (stylesheetOnlyChange &&
451 !pattern.selectors.some(selector => selector.dependsOnStyles))
452 {
453 continue;
454 }
455
432 for (let selector of evaluate(pattern.selectors, 456 for (let selector of evaluate(pattern.selectors,
433 0, "", document, cssStyles)) 457 0, "", document, cssStyles))
434 { 458 {
435 if (pattern.selectors.some(s => s.preferHideWithSelector) && 459 if (pattern.selectors.some(s => s.preferHideWithSelector) &&
436 !pattern.selectors.some(s => s.requiresHiding)) 460 !pattern.selectors.some(s => s.requiresHiding))
437 { 461 {
438 selectors.push(selector); 462 selectors.push(selector);
439 selectorFilters.push(pattern.text); 463 selectorFilters.push(pattern.text);
440 } 464 }
441 else 465 else
442 { 466 {
443 for (let element of document.querySelectorAll(selector)) 467 for (let element of document.querySelectorAll(selector))
444 { 468 {
445 elements.push(element); 469 elements.push(element);
446 elementFilters.push(pattern.text); 470 elementFilters.push(pattern.text);
447 } 471 }
448 } 472 }
449 } 473 }
450 } 474 }
451 475
452 this.addSelectorsFunc(selectors, selectorFilters); 476 this.addSelectorsFunc(selectors, selectorFilters);
453 this.hideElemsFunc(elements, elementFilters); 477 this.hideElemsFunc(elements, elementFilters);
454 }, 478 },
455 479
480 _stylesheetQueue: null,
481
456 onLoad(event) 482 onLoad(event)
457 { 483 {
458 let stylesheet = event.target.sheet; 484 let stylesheet = event.target.sheet;
459 if (stylesheet) 485 if (stylesheet)
460 this.addSelectors([stylesheet]); 486 {
487 if (!this._stylesheetQueue &&
488 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
489 {
490 this._stylesheetQueue = [];
491 this.window.setTimeout(() =>
492 {
493 let stylesheets = this._stylesheetQueue;
494 this._stylesheetQueue = null;
495 this.addSelectors(stylesheets);
496 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
497 }
498
499 if (this._stylesheetQueue)
500 this._stylesheetQueue.push(stylesheet);
501 else
502 this.addSelectors([stylesheet]);
503 }
461 }, 504 },
462 505
463 apply() 506 apply()
464 { 507 {
465 this.getFiltersFunc(patterns => 508 this.getFiltersFunc(patterns =>
466 { 509 {
467 this.patterns = []; 510 this.patterns = [];
468 for (let pattern of patterns) 511 for (let pattern of patterns)
469 { 512 {
470 let selectors = this.parseSelector(pattern.selector); 513 let selectors = this.parseSelector(pattern.selector);
471 if (selectors != null && selectors.length > 0) 514 if (selectors != null && selectors.length > 0)
472 this.patterns.push({selectors, text: pattern.text}); 515 this.patterns.push({selectors, text: pattern.text});
473 } 516 }
474 517
475 if (this.patterns.length > 0) 518 if (this.patterns.length > 0)
476 { 519 {
477 let {document} = this.window; 520 let {document} = this.window;
478 this.addSelectors(document.styleSheets); 521 this.addSelectors();
479 document.addEventListener("load", this.onLoad.bind(this), true); 522 document.addEventListener("load", this.onLoad.bind(this), true);
480 } 523 }
481 }); 524 });
482 } 525 }
483 }; 526 };
LEFTRIGHT

Powered by Google App Engine
This is Rietveld