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: Rebased. No longer need browser env Created June 12, 2017, 3:20 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 /* globals filterToRegExp */
19
20 "use strict"; 18 "use strict";
21 19
20 const {filterToRegExp} = require("common");
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 = "";
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
121 { 120 {
122 parens--; 121 parens--;
123 if (parens == 0) 122 if (parens == 0)
124 break; 123 break;
125 } 124 }
126 } 125 }
127 126
128 if (parens > 0) 127 if (parens > 0)
129 return null; 128 return null;
130 return {text: content.substring(startIndex, i), end: i}; 129 return {text: content.substring(startIndex, i), end: i};
131 }
132
133 /** Parse the selector
134 * @param {string} selector the selector to parse
135 * @return {Object} selectors is an array of objects,
136 * or null in case of errors. hide is true if we'll hide
137 * elements instead of styles..
138 */
139 function parseSelector(selector)
140 {
141 if (selector.length == 0)
142 return [];
143
144 let match = abpSelectorRegexp.exec(selector);
145 if (!match)
146 return [new PlainSelector(selector)];
147
148 let selectors = [];
149 if (match.index > 0)
150 selectors.push(new PlainSelector(selector.substr(0, match.index)));
151
152 let startIndex = match.index + match[0].length;
153 let content = parseSelectorContent(selector, startIndex);
154 if (!content)
155 {
156 reportError(new SyntaxError("Failed parsing ABP " +
157 `selector ${selector}, didn't ` +
158 "find closing parenthesis."));
159 return null;
160 }
161 if (match[1] == "properties")
162 selectors.push(new PropsSelector(content.text));
163 else if (match[1] == "has")
164 {
165 let hasSelector = new HasSelector(content.text);
166 if (!hasSelector.valid())
167 return null;
168 selectors.push(hasSelector);
169 }
170 else if (match[1] == "contains")
171 selectors.push(new ContainsSelector(content.text));
172 else
173 {
174 // this is an error, can't parse selector.
175 reportError(new SyntaxError("Failed parsing ABP " +
176 `selector ${selector}, invalid ` +
177 `pseudo-class :-abp-${match[1]}().`));
178 return null;
179 }
180
181 let suffix = parseSelector(selector.substr(content.end + 1));
182 if (suffix == null)
183 return null;
184
185 selectors.push(...suffix);
186
187 return selectors;
188 } 130 }
189 131
190 /** Stringified style objects 132 /** Stringified style objects
191 * @typedef {Object} StringifiedStyle 133 * @typedef {Object} StringifiedStyle
192 * @property {string} style CSS style represented by a string. 134 * @property {string} style CSS style represented by a string.
193 * @property {string[]} subSelectors selectors the CSS properties apply to. 135 * @property {string[]} subSelectors selectors the CSS properties apply to.
194 */ 136 */
195 137
196 /** 138 /**
197 * 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
241 * @param {StringifiedStyle[]} styles the stringified style objects. 183 * @param {StringifiedStyle[]} styles the stringified style objects.
242 */ 184 */
243 *getSelectors(prefix, subtree, styles) 185 *getSelectors(prefix, subtree, styles)
244 { 186 {
245 yield [prefix + this._selector, subtree]; 187 yield [prefix + this._selector, subtree];
246 } 188 }
247 }; 189 };
248 190
249 const incompletePrefixRegexp = /[\s>+~]$/; 191 const incompletePrefixRegexp = /[\s>+~]$/;
250 192
251 function HasSelector(selector) 193 function HasSelector(selectors)
252 { 194 {
253 this._innerSelectors = parseSelector(selector); 195 this._innerSelectors = selectors;
254 } 196 }
255 197
256 HasSelector.prototype = { 198 HasSelector.prototype = {
257 requiresHiding: true, 199 requiresHiding: true,
258 200
259 valid() 201 get dependsOnStyles()
260 { 202 {
261 return this._innerSelectors != null; 203 return this._innerSelectors.some(selector => selector.dependsOnStyles);
262 }, 204 },
263 205
264 *getSelectors(prefix, subtree, styles) 206 *getSelectors(prefix, subtree, styles)
265 { 207 {
266 for (let element of this.getElements(prefix, subtree, styles)) 208 for (let element of this.getElements(prefix, subtree, styles))
267 yield [makeSelector(element, ""), element]; 209 yield [makeSelector(element, ""), element];
268 }, 210 },
269 211
270 /** 212 /**
271 * Generator function returning selected elements. 213 * Generator function returning selected elements.
272 * @param {string} prefix the prefix for the selector. 214 * @param {string} prefix the prefix for the selector.
273 * @param {Node} subtree the subtree we work on. 215 * @param {Node} subtree the subtree we work on.
274 * @param {StringifiedStyle[]} styles the stringified style objects. 216 * @param {StringifiedStyle[]} styles the stringified style objects.
275 */ 217 */
276 *getElements(prefix, subtree, styles) 218 *getElements(prefix, subtree, styles)
277 { 219 {
278 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 220 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
279 prefix + "*" : prefix; 221 prefix + "*" : prefix;
280 let elements = subtree.querySelectorAll(actualPrefix); 222 let elements = subtree.querySelectorAll(actualPrefix);
281 for (let element of elements) 223 for (let element of elements)
282 { 224 {
283 let newPrefix = makeSelector(element, ""); 225 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
284 let iter = evaluate(this._innerSelectors, 0, newPrefix + " ",
285 element, styles);
286 for (let selector of iter) 226 for (let selector of iter)
287 // we insert a space between the two. It becomes a no-op if selector 227 if (element.querySelector(selector))
288 // doesn't have a combinator
289 if (subtree.querySelector(selector))
290 yield element; 228 yield element;
291 } 229 }
292 } 230 }
293 }; 231 };
294 232
295 function ContainsSelector(textContent) 233 function ContainsSelector(textContent)
296 { 234 {
297 this._text = textContent; 235 this._text = textContent;
298 } 236 }
299 237
300 ContainsSelector.prototype = { 238 ContainsSelector.prototype = {
301 requiresHiding: true, 239 requiresHiding: true,
302 240
303 *getSelectors(prefix, subtree, stylesheet) 241 *getSelectors(prefix, subtree, stylesheet)
304 { 242 {
305 for (let element of this.getElements(prefix, subtree, stylesheet)) 243 for (let element of this.getElements(prefix, subtree, stylesheet))
306 yield [makeSelector(element, ""), subtree]; 244 yield [makeSelector(element, ""), subtree];
307 }, 245 },
308 246
309 *getElements(prefix, subtree, stylesheet) 247 *getElements(prefix, subtree, stylesheet)
310 { 248 {
311 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 249 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
312 prefix + "*" : prefix; 250 prefix + "*" : prefix;
313 let elements = subtree.querySelectorAll(actualPrefix); 251 let elements = subtree.querySelectorAll(actualPrefix);
314 for (let element of elements) 252 for (let element of elements)
315 if (element.textContent == this._text) 253 if (element.textContent.includes(this._text))
316 yield element; 254 yield element;
317 } 255 }
318 }; 256 };
319 257
320 function PropsSelector(propertyExpression) 258 function PropsSelector(propertyExpression)
321 { 259 {
322 let regexpString; 260 let regexpString;
323 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 261 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
324 propertyExpression[propertyExpression.length - 1] == "/") 262 propertyExpression[propertyExpression.length - 1] == "/")
325 { 263 {
326 regexpString = propertyExpression.slice(1, -1) 264 regexpString = propertyExpression.slice(1, -1)
327 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 265 .replace("\\x7B ", "{").replace("\\x7D ", "}");
328 } 266 }
329 else 267 else
330 regexpString = filterToRegExp(propertyExpression); 268 regexpString = filterToRegExp(propertyExpression);
331 269
332 this._regexp = new RegExp(regexpString, "i"); 270 this._regexp = new RegExp(regexpString, "i");
333 } 271 }
334 272
335 PropsSelector.prototype = { 273 PropsSelector.prototype = {
274 preferHideWithSelector: true,
275 dependsOnStyles: true,
276
336 *findPropsSelectors(styles, prefix, regexp) 277 *findPropsSelectors(styles, prefix, regexp)
337 { 278 {
338 for (let style of styles) 279 for (let style of styles)
339 if (regexp.test(style.style)) 280 if (regexp.test(style.style))
340 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);
341 yield prefix + subSelector; 286 yield prefix + subSelector;
287 }
342 }, 288 },
343 289
344 *getSelectors(prefix, subtree, styles) 290 *getSelectors(prefix, subtree, styles)
345 { 291 {
346 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) 292 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
347 yield [selector, subtree]; 293 yield [selector, subtree];
348 } 294 }
349 }; 295 };
350 296
351 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, 297 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
352 hideElemsFunc) 298 hideElemsFunc)
353 { 299 {
354 this.window = window; 300 this.window = window;
355 this.getFiltersFunc = getFiltersFunc; 301 this.getFiltersFunc = getFiltersFunc;
356 this.addSelectorsFunc = addSelectorsFunc; 302 this.addSelectorsFunc = addSelectorsFunc;
357 this.hideElemsFunc = hideElemsFunc; 303 this.hideElemsFunc = hideElemsFunc;
358 reportError = error => this.window.console.error(error);
359 } 304 }
360 305
361 ElemHideEmulation.prototype = { 306 ElemHideEmulation.prototype = {
362 isSameOrigin(stylesheet) 307 isSameOrigin(stylesheet)
363 { 308 {
364 try 309 try
365 { 310 {
366 return new URL(stylesheet.href).origin == this.window.location.origin; 311 return new URL(stylesheet.href).origin == this.window.location.origin;
367 } 312 }
368 catch (e) 313 catch (e)
369 { 314 {
370 // Invalid URL, assume that it is first-party. 315 // Invalid URL, assume that it is first-party.
371 return true; 316 return true;
372 } 317 }
373 }, 318 },
374 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 */
375 addSelectors(stylesheets) 396 addSelectors(stylesheets)
376 { 397 {
398 this._lastInvocation = Date.now();
399
377 let selectors = []; 400 let selectors = [];
378 let selectorFilters = []; 401 let selectorFilters = [];
379 402
380 let elements = []; 403 let elements = [];
381 let elementFilters = []; 404 let elementFilters = [];
382 405
383 let cssStyles = []; 406 let cssStyles = [];
384 407
385 for (let stylesheet of stylesheets) 408 let stylesheetOnlyChange = !!stylesheets;
386 { 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];
387 // Explicitly ignore third-party stylesheets to ensure consistent behavior 417 // Explicitly ignore third-party stylesheets to ensure consistent behavior
388 // between Firefox and Chrome. 418 // between Firefox and Chrome.
389 if (!this.isSameOrigin(stylesheet)) 419 if (!this.isSameOrigin(stylesheet))
390 continue; 420 continue;
391 421
392 let rules = stylesheet.cssRules; 422 let rules = stylesheet.cssRules;
393 if (!rules) 423 if (!rules)
394 continue; 424 continue;
395 425
396 for (let rule of rules) 426 for (let rule of rules)
397 { 427 {
398 if (rule.type != rule.STYLE_RULE) 428 if (rule.type != rule.STYLE_RULE)
399 continue; 429 continue;
400 430
401 cssStyles.push(stringifyStyle(rule)); 431 cssStyles.push(stringifyStyle(rule));
402 } 432 }
403 } 433 }
404 434
405 let {document} = this.window; 435 let {document} = this.window;
406 for (let pattern of this.patterns) 436 for (let pattern of this.patterns)
407 { 437 {
438 if (stylesheetOnlyChange &&
439 !pattern.selectors.some(selector => selector.dependsOnStyles))
440 {
441 continue;
442 }
443
408 for (let selector of evaluate(pattern.selectors, 444 for (let selector of evaluate(pattern.selectors,
409 0, "", document, cssStyles)) 445 0, "", document, cssStyles))
410 { 446 {
411 if (!pattern.selectors.some(s => s.requiresHiding)) 447 if (pattern.selectors.some(s => s.preferHideWithSelector) &&
448 !pattern.selectors.some(s => s.requiresHiding))
412 { 449 {
413 selectors.push(selector); 450 selectors.push(selector);
414 selectorFilters.push(pattern.text); 451 selectorFilters.push(pattern.text);
415 } 452 }
416 else 453 else
417 { 454 {
418 for (let element of document.querySelectorAll(selector)) 455 for (let element of document.querySelectorAll(selector))
419 { 456 {
420 elements.push(element); 457 elements.push(element);
421 elementFilters.push(pattern.text); 458 elementFilters.push(pattern.text);
422 } 459 }
423 } 460 }
424 } 461 }
425 } 462 }
426 463
427 this.addSelectorsFunc(selectors, selectorFilters); 464 this.addSelectorsFunc(selectors, selectorFilters);
428 this.hideElemsFunc(elements, elementFilters); 465 this.hideElemsFunc(elements, elementFilters);
429 }, 466 },
430 467
468 _stylesheetQueue: null,
469
431 onLoad(event) 470 onLoad(event)
432 { 471 {
433 let stylesheet = event.target.sheet; 472 let stylesheet = event.target.sheet;
434 if (stylesheet) 473 if (stylesheet)
435 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 }
436 }, 492 },
437 493
438 apply() 494 apply()
439 { 495 {
440 this.getFiltersFunc(patterns => 496 this.getFiltersFunc(patterns =>
441 { 497 {
442 this.patterns = []; 498 this.patterns = [];
443 for (let pattern of patterns) 499 for (let pattern of patterns)
444 { 500 {
445 let selectors = parseSelector(pattern.selector); 501 let selectors = this.parseSelector(pattern.selector);
446 if (selectors != null && selectors.length > 0) 502 if (selectors != null && selectors.length > 0)
447 this.patterns.push({selectors, text: pattern.text}); 503 this.patterns.push({selectors, text: pattern.text});
448 } 504 }
449 505
450 if (this.patterns.length > 0) 506 if (this.patterns.length > 0)
451 { 507 {
452 let {document} = this.window; 508 let {document} = this.window;
453 this.addSelectors(document.styleSheets); 509 this.addSelectors();
454 document.addEventListener("load", this.onLoad.bind(this), true); 510 document.addEventListener("load", this.onLoad.bind(this), true);
455 } 511 }
456 }); 512 });
457 } 513 }
458 }; 514 };
459 515
460 if (typeof exports != "undefined") 516 exports.ElemHideEmulation = ElemHideEmulation;
461 { 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
462 exports.ElemHideEmulation = ElemHideEmulation;
463 exports.splitSelector = splitSelector;
464 }
LEFTRIGHT

Powered by Google App Engine
This is Rietveld