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

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

Issue 29460576: Issue 5079 - Turn elemHideEmulation into a CommonJS module (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Left Patch Set: Created June 9, 2017, 5:08 p.m.
Right Patch Set: Rebased on master Created Aug. 10, 2017, 2:44 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 | « lib/common.js ('k') | 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 /* eslint-env browser */
kzar 2017/06/12 12:50:13 Why is this necessary?
hub 2017/06/12 13:26:33 The mistake here is that I should have the proper
kzar 2017/06/12 13:35:12 Sorry, but I still don't understand why it's neces
hub 2017/06/12 14:03:14 This change is gone in the current version of the
kzar 2017/06/12 14:12:44 Right now I understand, previously this file was i
hub 2017/06/12 15:21:50 This is now totally gone in the current patch sinc
19 /* globals filterToRegExp */
20
21 "use strict"; 18 "use strict";
22 19
20 const {filterToRegExp} = require("common");
21
22 const MIN_INVOCATION_INTERVAL = 3000;
23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
24 24
25 function splitSelector(selector) 25 function splitSelector(selector)
26 { 26 {
27 if (selector.indexOf(",") == -1) 27 if (selector.indexOf(",") == -1)
28 return [selector]; 28 return [selector];
29 29
30 let selectors = []; 30 let selectors = [];
31 let start = 0; 31 let start = 0;
32 let level = 0; 32 let level = 0;
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
120 { 120 {
121 parens--; 121 parens--;
122 if (parens == 0) 122 if (parens == 0)
123 break; 123 break;
124 } 124 }
125 } 125 }
126 126
127 if (parens > 0) 127 if (parens > 0)
128 return null; 128 return null;
129 return {text: content.substring(startIndex, i), end: i}; 129 return {text: content.substring(startIndex, i), end: i};
130 }
131
132 /** Parse the selector
133 * @param {string} selector the selector to parse
134 * @return {Object} selectors is an array of objects,
135 * or null in case of errors. hide is true if we'll hide
136 * elements instead of styles..
137 */
138 function parseSelector(selector)
139 {
140 if (selector.length == 0)
141 return [];
142
143 let match = abpSelectorRegexp.exec(selector);
144 if (!match)
145 return [new PlainSelector(selector)];
146
147 let selectors = [];
148 if (match.index > 0)
149 selectors.push(new PlainSelector(selector.substr(0, match.index)));
150
151 let startIndex = match.index + match[0].length;
152 let content = parseSelectorContent(selector, startIndex);
153 if (!content)
154 {
155 console.error(new SyntaxError("Failed parsing content filter " +
156 `selector ${selector}, didn't ` +
157 "find closing parenthesis."));
158 return null;
159 }
160 if (match[1] == "properties")
161 selectors.push(new PropsSelector(content.text));
162 else if (match[1] == "has")
163 {
164 let hasSelector = new HasSelector(content.text);
165 if (!hasSelector.valid())
166 return null;
167 selectors.push(hasSelector);
168 }
169 else if (match[1] == "contains")
170 selectors.push(new ContainsSelector(content.text));
171 else
172 {
173 // this is an error, can't parse selector.
174 console.error(new SyntaxError("Failed parsing content filter " +
175 `selector ${selector}, invalid ` +
176 `pseudo-class -abp-${match[1]}().`));
177 return null;
178 }
179
180 let suffix = parseSelector(selector.substr(content.end + 1));
181 if (suffix == null)
182 return null;
183
184 selectors.push(...suffix);
185
186 return selectors;
187 } 130 }
188 131
189 /** Stringified style objects 132 /** Stringified style objects
190 * @typedef {Object} StringifiedStyle 133 * @typedef {Object} StringifiedStyle
191 * @property {string} style CSS style represented by a string. 134 * @property {string} style CSS style represented by a string.
192 * @property {string[]} subSelectors selectors the CSS properties apply to. 135 * @property {string[]} subSelectors selectors the CSS properties apply to.
193 */ 136 */
194 137
195 /** 138 /**
196 * Produce a string representation of the stylesheet entry. 139 * Produce a string representation of the stylesheet entry.
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
240 * @param {StringifiedStyle[]} styles the stringified style objects. 183 * @param {StringifiedStyle[]} styles the stringified style objects.
241 */ 184 */
242 *getSelectors(prefix, subtree, styles) 185 *getSelectors(prefix, subtree, styles)
243 { 186 {
244 yield [prefix + this._selector, subtree]; 187 yield [prefix + this._selector, subtree];
245 } 188 }
246 }; 189 };
247 190
248 const incompletePrefixRegexp = /[\s>+~]$/; 191 const incompletePrefixRegexp = /[\s>+~]$/;
249 192
250 function HasSelector(selector) 193 function HasSelector(selectors)
251 { 194 {
252 this._innerSelectors = parseSelector(selector); 195 this._innerSelectors = selectors;
253 } 196 }
254 197
255 HasSelector.prototype = { 198 HasSelector.prototype = {
256 requiresHiding: true, 199 requiresHiding: true,
257 200
258 valid() 201 get dependsOnStyles()
259 { 202 {
260 return this._innerSelectors != null; 203 return this._innerSelectors.some(selector => selector.dependsOnStyles);
261 }, 204 },
262 205
263 *getSelectors(prefix, subtree, styles) 206 *getSelectors(prefix, subtree, styles)
264 { 207 {
265 for (let element of this.getElements(prefix, subtree, styles)) 208 for (let element of this.getElements(prefix, subtree, styles))
266 yield [makeSelector(element, ""), element]; 209 yield [makeSelector(element, ""), element];
267 }, 210 },
268 211
269 /** 212 /**
270 * Generator function returning selected elements. 213 * Generator function returning selected elements.
271 * @param {string} prefix the prefix for the selector. 214 * @param {string} prefix the prefix for the selector.
272 * @param {Node} subtree the subtree we work on. 215 * @param {Node} subtree the subtree we work on.
273 * @param {StringifiedStyle[]} styles the stringified style objects. 216 * @param {StringifiedStyle[]} styles the stringified style objects.
274 */ 217 */
275 *getElements(prefix, subtree, styles) 218 *getElements(prefix, subtree, styles)
276 { 219 {
277 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 220 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
278 prefix + "*" : prefix; 221 prefix + "*" : prefix;
279 let elements = subtree.querySelectorAll(actualPrefix); 222 let elements = subtree.querySelectorAll(actualPrefix);
280 for (let element of elements) 223 for (let element of elements)
281 { 224 {
282 let newPrefix = makeSelector(element, ""); 225 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
283 let iter = evaluate(this._innerSelectors, 0, newPrefix + " ",
284 element, styles);
285 for (let selector of iter) 226 for (let selector of iter)
286 // we insert a space between the two. It becomes a no-op if selector 227 if (element.querySelector(selector))
287 // doesn't have a combinator
288 if (subtree.querySelector(selector))
289 yield element; 228 yield element;
290 } 229 }
291 } 230 }
292 }; 231 };
293 232
294 function ContainsSelector(textContent) 233 function ContainsSelector(textContent)
295 { 234 {
296 this._text = textContent; 235 this._text = textContent;
297 } 236 }
298 237
299 ContainsSelector.prototype = { 238 ContainsSelector.prototype = {
300 requiresHiding: true, 239 requiresHiding: true,
301 240
302 *getSelectors(prefix, subtree, stylesheet) 241 *getSelectors(prefix, subtree, stylesheet)
303 { 242 {
304 for (let element of this.getElements(prefix, subtree, stylesheet)) 243 for (let element of this.getElements(prefix, subtree, stylesheet))
305 yield [makeSelector(element, ""), subtree]; 244 yield [makeSelector(element, ""), subtree];
306 }, 245 },
307 246
308 *getElements(prefix, subtree, stylesheet) 247 *getElements(prefix, subtree, stylesheet)
309 { 248 {
310 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 249 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
311 prefix + "*" : prefix; 250 prefix + "*" : prefix;
312 let elements = subtree.querySelectorAll(actualPrefix); 251 let elements = subtree.querySelectorAll(actualPrefix);
313 for (let element of elements) 252 for (let element of elements)
314 if (element.textContent == this._text) 253 if (element.textContent.includes(this._text))
315 yield element; 254 yield element;
316 } 255 }
317 }; 256 };
318 257
319 function PropsSelector(propertyExpression) 258 function PropsSelector(propertyExpression)
320 { 259 {
321 let regexpString; 260 let regexpString;
322 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 261 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
323 propertyExpression[propertyExpression.length - 1] == "/") 262 propertyExpression[propertyExpression.length - 1] == "/")
324 { 263 {
325 regexpString = propertyExpression.slice(1, -1) 264 regexpString = propertyExpression.slice(1, -1)
326 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 265 .replace("\\x7B ", "{").replace("\\x7D ", "}");
327 } 266 }
328 else 267 else
329 regexpString = filterToRegExp(propertyExpression); 268 regexpString = filterToRegExp(propertyExpression);
330 269
331 this._regexp = new RegExp(regexpString, "i"); 270 this._regexp = new RegExp(regexpString, "i");
332 } 271 }
333 272
334 PropsSelector.prototype = { 273 PropsSelector.prototype = {
274 preferHideWithSelector: true,
275 dependsOnStyles: true,
276
335 *findPropsSelectors(styles, prefix, regexp) 277 *findPropsSelectors(styles, prefix, regexp)
336 { 278 {
337 for (let style of styles) 279 for (let style of styles)
338 if (regexp.test(style.style)) 280 if (regexp.test(style.style))
339 for (let subSelector of style.subSelectors) 281 for (let subSelector of style.subSelectors)
282 {
283 let idx = subSelector.lastIndexOf("::");
284 if (idx != -1)
285 subSelector = subSelector.substr(0, idx);
340 yield prefix + subSelector; 286 yield prefix + subSelector;
287 }
341 }, 288 },
342 289
343 *getSelectors(prefix, subtree, styles) 290 *getSelectors(prefix, subtree, styles)
344 { 291 {
345 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 292 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
346 yield [selector, subtree]; 293 yield [selector, subtree];
347 } 294 }
348 }; 295 };
349 296
350 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 297 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
(...skipping 12 matching lines...) Expand all
363 { 310 {
364 return new URL(stylesheet.href).origin == this.window.location.origin; 311 return new URL(stylesheet.href).origin == this.window.location.origin;
365 } 312 }
366 catch (e) 313 catch (e)
367 { 314 {
368 // Invalid URL, assume that it is first-party. 315 // Invalid URL, assume that it is first-party.
369 return true; 316 return true;
370 } 317 }
371 }, 318 },
372 319
320 /** Parse the selector
321 * @param {string} selector the selector to parse
322 * @return {Array} selectors is an array of objects,
323 * or null in case of errors.
324 */
325 parseSelector(selector)
326 {
327 if (selector.length == 0)
328 return [];
329
330 let match = abpSelectorRegexp.exec(selector);
331 if (!match)
332 return [new PlainSelector(selector)];
333
334 let selectors = [];
335 if (match.index > 0)
336 selectors.push(new PlainSelector(selector.substr(0, match.index)));
337
338 let startIndex = match.index + match[0].length;
339 let content = parseSelectorContent(selector, startIndex);
340 if (!content)
341 {
342 this.window.console.error(
343 new SyntaxError("Failed to parse Adblock Plus " +
344 `selector ${selector} ` +
345 "due to unmatched parentheses."));
346 return null;
347 }
348 if (match[1] == "properties")
349 selectors.push(new PropsSelector(content.text));
350 else if (match[1] == "has")
351 {
352 let hasSelectors = this.parseSelector(content.text);
353 if (hasSelectors == null)
354 return null;
355 selectors.push(new HasSelector(hasSelectors));
356 }
357 else if (match[1] == "contains")
358 selectors.push(new ContainsSelector(content.text));
359 else
360 {
361 // this is an error, can't parse selector.
362 this.window.console.error(
363 new SyntaxError("Failed to parse Adblock Plus " +
364 `selector ${selector}, invalid ` +
365 `pseudo-class :-abp-${match[1]}().`));
366 return null;
367 }
368
369 let suffix = this.parseSelector(selector.substr(content.end + 1));
370 if (suffix == null)
371 return null;
372
373 selectors.push(...suffix);
374
375 if (selectors.length == 1 && selectors[0] instanceof ContainsSelector)
376 {
377 this.window.console.error(
378 new SyntaxError("Failed to parse Adblock Plus " +
379 `selector ${selector}, can't ` +
380 "have a lonely :-abp-contains()."));
381 return null;
382 }
383 return selectors;
384 },
385
386 _lastInvocation: 0,
387
388 /**
389 * Processes the current document and applies all rules to it.
390 * @param {CSSStyleSheet[]} [stylesheets]
391 * The list of new stylesheets that have been added to the document and
392 * made reprocessing necessary. This parameter shouldn't be passed in for
393 * the initial processing, all of document's stylesheets will be considered
394 * then and all rules, including the ones not dependent on styles.
395 */
373 addSelectors(stylesheets) 396 addSelectors(stylesheets)
374 { 397 {
398 this._lastInvocation = Date.now();
399
375 let selectors = []; 400 let selectors = [];
376 let selectorFilters = []; 401 let selectorFilters = [];
377 402
378 let elements = []; 403 let elements = [];
379 let elementFilters = []; 404 let elementFilters = [];
380 405
381 let cssStyles = []; 406 let cssStyles = [];
382 407
383 for (let stylesheet of stylesheets) 408 let stylesheetOnlyChange = !!stylesheets;
384 { 409 if (!stylesheets)
410 stylesheets = this.window.document.styleSheets;
411
412 // Chrome < 51 doesn't have an iterable StyleSheetList
413 // https://issues.adblockplus.org/ticket/5381
414 for (let i = 0; i < stylesheets.length; i++)
415 {
416 let stylesheet = stylesheets[i];
385 // Explicitly ignore third-party stylesheets to ensure consistent behavior 417 // Explicitly ignore third-party stylesheets to ensure consistent behavior
386 // between Firefox and Chrome. 418 // between Firefox and Chrome.
387 if (!this.isSameOrigin(stylesheet)) 419 if (!this.isSameOrigin(stylesheet))
388 continue; 420 continue;
389 421
390 let rules = stylesheet.cssRules; 422 let rules = stylesheet.cssRules;
391 if (!rules) 423 if (!rules)
392 continue; 424 continue;
393 425
394 for (let rule of rules) 426 for (let rule of rules)
395 { 427 {
396 if (rule.type != rule.STYLE_RULE) 428 if (rule.type != rule.STYLE_RULE)
397 continue; 429 continue;
398 430
399 cssStyles.push(stringifyStyle(rule)); 431 cssStyles.push(stringifyStyle(rule));
400 } 432 }
401 } 433 }
402 434
435 let {document} = this.window;
403 for (let pattern of this.patterns) 436 for (let pattern of this.patterns)
404 { 437 {
438 if (stylesheetOnlyChange &&
439 !pattern.selectors.some(selector => selector.dependsOnStyles))
440 {
441 continue;
442 }
443
405 for (let selector of evaluate(pattern.selectors, 444 for (let selector of evaluate(pattern.selectors,
406 0, "", document, cssStyles)) 445 0, "", document, cssStyles))
407 { 446 {
408 if (!pattern.selectors.some(s => s.requiresHiding)) 447 if (pattern.selectors.some(s => s.preferHideWithSelector) &&
448 !pattern.selectors.some(s => s.requiresHiding))
409 { 449 {
410 selectors.push(selector); 450 selectors.push(selector);
411 selectorFilters.push(pattern.text); 451 selectorFilters.push(pattern.text);
412 } 452 }
413 else 453 else
414 { 454 {
415 for (let element of document.querySelectorAll(selector)) 455 for (let element of document.querySelectorAll(selector))
416 { 456 {
417 elements.push(element); 457 elements.push(element);
418 elementFilters.push(pattern.text); 458 elementFilters.push(pattern.text);
419 } 459 }
420 } 460 }
421 } 461 }
422 } 462 }
423 463
424 this.addSelectorsFunc(selectors, selectorFilters); 464 this.addSelectorsFunc(selectors, selectorFilters);
425 this.hideElemsFunc(elements, elementFilters); 465 this.hideElemsFunc(elements, elementFilters);
426 }, 466 },
427 467
468 _stylesheetQueue: null,
469
428 onLoad(event) 470 onLoad(event)
429 { 471 {
430 let stylesheet = event.target.sheet; 472 let stylesheet = event.target.sheet;
431 if (stylesheet) 473 if (stylesheet)
432 this.addSelectors([stylesheet]); 474 {
475 if (!this._stylesheetQueue &&
476 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
477 {
478 this._stylesheetQueue = [];
479 this.window.setTimeout(() =>
480 {
481 let stylesheets = this._stylesheetQueue;
482 this._stylesheetQueue = null;
483 this.addSelectors(stylesheets);
484 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
485 }
486
487 if (this._stylesheetQueue)
488 this._stylesheetQueue.push(stylesheet);
489 else
490 this.addSelectors([stylesheet]);
491 }
433 }, 492 },
434 493
435 apply() 494 apply()
436 { 495 {
437 this.getFiltersFunc(patterns => 496 this.getFiltersFunc(patterns =>
438 { 497 {
439 this.patterns = []; 498 this.patterns = [];
440 for (let pattern of patterns) 499 for (let pattern of patterns)
441 { 500 {
442 let selectors = parseSelector(pattern.selector); 501 let selectors = this.parseSelector(pattern.selector);
443 if (selectors != null && selectors.length > 0) 502 if (selectors != null && selectors.length > 0)
444 this.patterns.push({selectors, text: pattern.text}); 503 this.patterns.push({selectors, text: pattern.text});
445 } 504 }
446 505
447 if (this.patterns.length > 0) 506 if (this.patterns.length > 0)
448 { 507 {
449 let {document} = this.window; 508 let {document} = this.window;
450 this.addSelectors(document.styleSheets); 509 this.addSelectors();
451 document.addEventListener("load", this.onLoad.bind(this), true); 510 document.addEventListener("load", this.onLoad.bind(this), true);
452 } 511 }
453 }); 512 });
454 } 513 }
455 }; 514 };
456 515
457 if (typeof exports != "undefined") 516 exports.ElemHideEmulation = ElemHideEmulation;
kzar 2017/06/12 12:50:13 Don't we expect exports to always exist like for t
hub 2017/06/12 13:26:34 Sadly no, we can't expect that yet. -in the tests
kzar 2017/06/12 13:35:12 Ah right, well I think we need to fix that at the
hub 2017/06/12 14:03:13 agreed.
kzar 2017/07/07 13:42:20 After reading through this review again I think re
hub 2017/08/10 14:46:21 Acknowledged.
458 { 517 exports.splitSelector = splitSelector;
Wladimir Palant 2017/08/16 09:55:53 This shouldn't export splitSelector. If we need th
kzar 2017/08/16 09:59:05 Acknowledged, OK I'll move that over while I'm at
459 exports.ElemHideEmulation = ElemHideEmulation;
460 exports.splitSelector = splitSelector;
461 }
LEFTRIGHT

Powered by Google App Engine
This is Rietveld