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

Powered by Google App Engine
This is Rietveld