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

Side by Side Diff: chrome/content/elemHideEmulation.js

Issue 29517687: Issue 5079, 5516 - Use webpack for browser tests, modules for content scripts (Closed)
Patch Set: Addressed Wladimir's and Hubert's initial feedback Created Aug. 17, 2017, 12:36 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2017 eyeo GmbH
4 *
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
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
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/>.
16 */
17
18 /* globals filterToRegExp */
19
20 "use strict";
21
22 const MIN_INVOCATION_INTERVAL = 3000;
23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
24
25 function splitSelector(selector)
26 {
27 if (selector.indexOf(",") == -1)
28 return [selector];
29
30 let selectors = [];
31 let start = 0;
32 let level = 0;
33 let sep = "";
34
35 for (let i = 0; i < selector.length; i++)
36 {
37 let chr = selector[i];
38
39 if (chr == "\\") // ignore escaped characters
40 i++;
41 else if (chr == sep) // don't split within quoted text
42 sep = ""; // e.g. [attr=","]
43 else if (sep == "")
44 {
45 if (chr == '"' || chr == "'")
46 sep = chr;
47 else if (chr == "(") // don't split between parentheses
48 level++; // e.g. :matches(div,span)
49 else if (chr == ")")
50 level = Math.max(0, level - 1);
51 else if (chr == "," && level == 0)
52 {
53 selectors.push(selector.substring(start, i));
54 start = i + 1;
55 }
56 }
57 }
58
59 selectors.push(selector.substring(start));
60 return selectors;
61 }
62
63 /** Return position of node from parent.
64 * @param {Node} node the node to find the position of.
65 * @return {number} One-based index like for :nth-child(), or 0 on error.
66 */
67 function positionInParent(node)
68 {
69 let {children} = node.parentNode;
70 for (let i = 0; i < children.length; i++)
71 if (children[i] == node)
72 return i + 1;
73 return 0;
74 }
75
76 function makeSelector(node, selector)
77 {
78 if (!node.parentElement)
79 {
80 let newSelector = ":root";
81 if (selector)
82 newSelector += " > " + selector;
83 return newSelector;
84 }
85 let idx = positionInParent(node);
86 if (idx > 0)
87 {
88 let newSelector = `${node.tagName}:nth-child(${idx})`;
89 if (selector)
90 newSelector += " > " + selector;
91 return makeSelector(node.parentElement, newSelector);
92 }
93
94 return selector;
95 }
96
97 function parseSelectorContent(content, startIndex)
98 {
99 let parens = 1;
100 let quote = null;
101 let i = startIndex;
102 for (; i < content.length; i++)
103 {
104 let c = content[i];
105 if (c == "\\")
106 {
107 // Ignore escaped characters
108 i++;
109 }
110 else if (quote)
111 {
112 if (c == quote)
113 quote = null;
114 }
115 else if (c == "'" || c == '"')
116 quote = c;
117 else if (c == "(")
118 parens++;
119 else if (c == ")")
120 {
121 parens--;
122 if (parens == 0)
123 break;
124 }
125 }
126
127 if (parens > 0)
128 return null;
129 return {text: content.substring(startIndex, i), end: i};
130 }
131
132 /** Stringified style objects
133 * @typedef {Object} StringifiedStyle
134 * @property {string} style CSS style represented by a string.
135 * @property {string[]} subSelectors selectors the CSS properties apply to.
136 */
137
138 /**
139 * Produce a string representation of the stylesheet entry.
140 * @param {CSSStyleRule} rule the CSS style rule.
141 * @return {StringifiedStyle} the stringified style.
142 */
143 function stringifyStyle(rule)
144 {
145 let styles = [];
146 for (let i = 0; i < rule.style.length; i++)
147 {
148 let property = rule.style.item(i);
149 let value = rule.style.getPropertyValue(property);
150 let priority = rule.style.getPropertyPriority(property);
151 styles.push(`${property}: ${value}${priority ? " !" + priority : ""};`);
152 }
153 styles.sort();
154 return {
155 style: styles.join(" "),
156 subSelectors: splitSelector(rule.selectorText)
157 };
158 }
159
160 function* evaluate(chain, index, prefix, subtree, styles)
161 {
162 if (index >= chain.length)
163 {
164 yield prefix;
165 return;
166 }
167 for (let [selector, element] of
168 chain[index].getSelectors(prefix, subtree, styles))
169 yield* evaluate(chain, index + 1, selector, element, styles);
170 }
171
172 function PlainSelector(selector)
173 {
174 this._selector = selector;
175 }
176
177 PlainSelector.prototype = {
178 /**
179 * Generator function returning a pair of selector
180 * string and subtree.
181 * @param {string} prefix the prefix for the selector.
182 * @param {Node} subtree the subtree we work on.
183 * @param {StringifiedStyle[]} styles the stringified style objects.
184 */
185 *getSelectors(prefix, subtree, styles)
186 {
187 yield [prefix + this._selector, subtree];
188 }
189 };
190
191 const incompletePrefixRegexp = /[\s>+~]$/;
192
193 function HasSelector(selectors)
194 {
195 this._innerSelectors = selectors;
196 }
197
198 HasSelector.prototype = {
199 requiresHiding: true,
200
201 get dependsOnStyles()
202 {
203 return this._innerSelectors.some(selector => selector.dependsOnStyles);
204 },
205
206 *getSelectors(prefix, subtree, styles)
207 {
208 for (let element of this.getElements(prefix, subtree, styles))
209 yield [makeSelector(element, ""), element];
210 },
211
212 /**
213 * Generator function returning selected elements.
214 * @param {string} prefix the prefix for the selector.
215 * @param {Node} subtree the subtree we work on.
216 * @param {StringifiedStyle[]} styles the stringified style objects.
217 */
218 *getElements(prefix, subtree, styles)
219 {
220 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
221 prefix + "*" : prefix;
222 let elements = subtree.querySelectorAll(actualPrefix);
223 for (let element of elements)
224 {
225 let iter = evaluate(this._innerSelectors, 0, "", element, styles);
226 for (let selector of iter)
227 if (element.querySelector(selector))
228 yield element;
229 }
230 }
231 };
232
233 function ContainsSelector(textContent)
234 {
235 this._text = textContent;
236 }
237
238 ContainsSelector.prototype = {
239 requiresHiding: true,
240
241 *getSelectors(prefix, subtree, stylesheet)
242 {
243 for (let element of this.getElements(prefix, subtree, stylesheet))
244 yield [makeSelector(element, ""), subtree];
245 },
246
247 *getElements(prefix, subtree, stylesheet)
248 {
249 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
250 prefix + "*" : prefix;
251 let elements = subtree.querySelectorAll(actualPrefix);
252 for (let element of elements)
253 if (element.textContent.includes(this._text))
254 yield element;
255 }
256 };
257
258 function PropsSelector(propertyExpression)
259 {
260 let regexpString;
261 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
262 propertyExpression[propertyExpression.length - 1] == "/")
263 {
264 regexpString = propertyExpression.slice(1, -1)
265 .replace("\\x7B ", "{").replace("\\x7D ", "}");
266 }
267 else
268 regexpString = filterToRegExp(propertyExpression);
269
270 this._regexp = new RegExp(regexpString, "i");
271 }
272
273 PropsSelector.prototype = {
274 preferHideWithSelector: true,
275 dependsOnStyles: true,
276
277 *findPropsSelectors(styles, prefix, regexp)
278 {
279 for (let style of styles)
280 if (regexp.test(style.style))
281 for (let subSelector of style.subSelectors)
282 {
283 let idx = subSelector.lastIndexOf("::");
284 if (idx != -1)
285 subSelector = subSelector.substr(0, idx);
286 yield prefix + subSelector;
287 }
288 },
289
290 *getSelectors(prefix, subtree, styles)
291 {
292 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
293 yield [selector, subtree];
294 }
295 };
296
297 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
298 hideElemsFunc)
299 {
300 this.window = window;
301 this.getFiltersFunc = getFiltersFunc;
302 this.addSelectorsFunc = addSelectorsFunc;
303 this.hideElemsFunc = hideElemsFunc;
304 }
305
306 ElemHideEmulation.prototype = {
307 isSameOrigin(stylesheet)
308 {
309 try
310 {
311 return new URL(stylesheet.href).origin == this.window.location.origin;
312 }
313 catch (e)
314 {
315 // Invalid URL, assume that it is first-party.
316 return true;
317 }
318 },
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 */
396 addSelectors(stylesheets)
397 {
398 this._lastInvocation = Date.now();
399
400 let selectors = [];
401 let selectorFilters = [];
402
403 let elements = [];
404 let elementFilters = [];
405
406 let cssStyles = [];
407
408 let stylesheetOnlyChange = !!stylesheets;
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];
417 // Explicitly ignore third-party stylesheets to ensure consistent behavior
418 // between Firefox and Chrome.
419 if (!this.isSameOrigin(stylesheet))
420 continue;
421
422 let rules = stylesheet.cssRules;
423 if (!rules)
424 continue;
425
426 for (let rule of rules)
427 {
428 if (rule.type != rule.STYLE_RULE)
429 continue;
430
431 cssStyles.push(stringifyStyle(rule));
432 }
433 }
434
435 let {document} = this.window;
436 for (let pattern of this.patterns)
437 {
438 if (stylesheetOnlyChange &&
439 !pattern.selectors.some(selector => selector.dependsOnStyles))
440 {
441 continue;
442 }
443
444 for (let selector of evaluate(pattern.selectors,
445 0, "", document, cssStyles))
446 {
447 if (pattern.selectors.some(s => s.preferHideWithSelector) &&
448 !pattern.selectors.some(s => s.requiresHiding))
449 {
450 selectors.push(selector);
451 selectorFilters.push(pattern.text);
452 }
453 else
454 {
455 for (let element of document.querySelectorAll(selector))
456 {
457 elements.push(element);
458 elementFilters.push(pattern.text);
459 }
460 }
461 }
462 }
463
464 this.addSelectorsFunc(selectors, selectorFilters);
465 this.hideElemsFunc(elements, elementFilters);
466 },
467
468 _stylesheetQueue: null,
469
470 onLoad(event)
471 {
472 let stylesheet = event.target.sheet;
473 if (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 }
492 },
493
494 apply()
495 {
496 this.getFiltersFunc(patterns =>
497 {
498 this.patterns = [];
499 for (let pattern of patterns)
500 {
501 let selectors = this.parseSelector(pattern.selector);
502 if (selectors != null && selectors.length > 0)
503 this.patterns.push({selectors, text: pattern.text});
504 }
505
506 if (this.patterns.length > 0)
507 {
508 let {document} = this.window;
509 this.addSelectors();
510 document.addEventListener("load", this.onLoad.bind(this), true);
511 }
512 });
513 }
514 };
OLDNEW
« no previous file with comments | « chrome/content/.eslintrc.json ('k') | chromium_process.js » ('j') | test_runner.js » ('J')

Powered by Google App Engine
This is Rietveld