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

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

Issue 29448560: Issue 5249 - Implement :-abp-contains() (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore/
Left Patch Set: Rebased on patch for issue 3143. Created June 1, 2017, 6:29 p.m.
Right Patch Set: Use includes() and improve test. Created June 28, 2017, 4:11 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 abpSelectorRegexp = /:-abp-([\w-]+)\(/i; 22 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
23 23
24 let reportError = () => {};
25
24 function splitSelector(selector) 26 function splitSelector(selector)
25 { 27 {
26 if (selector.indexOf(",") == -1) 28 if (selector.indexOf(",") == -1)
27 return [selector]; 29 return [selector];
28 30
29 let selectors = []; 31 let selectors = [];
30 let start = 0; 32 let start = 0;
31 let level = 0; 33 let level = 0;
32 let sep = ""; 34 let sep = "";
33 35
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
71 return i + 1; 73 return i + 1;
72 return 0; 74 return 0;
73 } 75 }
74 76
75 function makeSelector(node, selector) 77 function makeSelector(node, selector)
76 { 78 {
77 if (!node.parentElement) 79 if (!node.parentElement)
78 { 80 {
79 let newSelector = ":root"; 81 let newSelector = ":root";
80 if (selector) 82 if (selector)
81 newSelector += " > "; 83 newSelector += " > " + selector;
82 return newSelector + selector; 84 return newSelector;
83 } 85 }
84 let idx = positionInParent(node); 86 let idx = positionInParent(node);
85 if (idx > 0) 87 if (idx > 0)
86 { 88 {
87 let newSelector = `${node.tagName}:nth-child(${idx})`; 89 let newSelector = `${node.tagName}:nth-child(${idx})`;
88 if (selector) 90 if (selector)
89 newSelector += " > "; 91 newSelector += " > " + selector;
90 return makeSelector(node.parentElement, newSelector + selector); 92 return makeSelector(node.parentElement, newSelector);
91 } 93 }
92 94
93 return selector; 95 return selector;
94 } 96 }
95 97
96 function parseSelectorContent(content) 98 function parseSelectorContent(content, startIndex)
97 { 99 {
98 let parens = 1; 100 let parens = 1;
99 let quote = null; 101 let quote = null;
100 let i; 102 let i = startIndex;
101 for (i = 0; i < content.length; i++) 103 for (; i < content.length; i++)
102 { 104 {
103 let c = content[i]; 105 let c = content[i];
104 if (c == "\\") 106 if (c == "\\")
105 { 107 {
106 // Ignore escaped characters 108 // Ignore escaped characters
107 i++; 109 i++;
108 } 110 }
109 else if (quote) 111 else if (quote)
110 { 112 {
111 if (c == quote) 113 if (c == quote)
112 quote = null; 114 quote = null;
113 } 115 }
114 else if (c == "'" || c == '"') 116 else if (c == "'" || c == '"')
115 quote = c; 117 quote = c;
116 else if (c == "(") 118 else if (c == "(")
117 parens++; 119 parens++;
118 else if (c == ")") 120 else if (c == ")")
119 { 121 {
120 parens--; 122 parens--;
121 if (parens == 0) 123 if (parens == 0)
122 break; 124 break;
123 } 125 }
124 } 126 }
125 127
126 if (parens > 0) 128 if (parens > 0)
127 return null; 129 return null;
128 return {text: content.substr(0, i), end: i}; 130 return {text: content.substring(startIndex, i), end: i};
129 } 131 }
130 132
131 /** Parse the selector 133 /** Stringified style objects
132 * @param {string} selector the selector to parse 134 * @typedef {Object} StringifiedStyle
133 * @param {Number} level the depth level. 0 is top 135 * @property {string} style CSS style represented by a string.
134 * @return {Object} selectors is an array of objects, 136 * @property {string[]} subSelectors selectors the CSS properties apply to.
135 * or null in case of errors. hide is true if we'll hide
136 * elements instead of styles..
137 */ 137 */
138 function parseSelector(selector, level = 0) 138
139 { 139 /**
140 if (selector.length == 0) 140 * Produce a string representation of the stylesheet entry.
141 return {selectors: [], hide: false}; 141 * @param {CSSStyleRule} rule the CSS style rule.
142 142 * @return {StringifiedStyle} the stringified style.
143 let match = abpSelectorRegexp.exec(selector); 143 */
144 if (!match) 144 function stringifyStyle(rule)
145 return {selectors: [new PlainSelector(selector)], hide: false};
146
147 let hide = false;
148 let selectors = [];
149 let suffixStart = match.index;
150 if (suffixStart > 0)
151 selectors.push(new PlainSelector(selector.substr(0, match.index)));
152
153 let startIndex = match.index + match[0].length;
154 let content = parseSelectorContent(selector.substr(startIndex));
155 if (content == null)
156 {
157 console.error(new SyntaxError("Failed parsing AdBlock Plus " +
158 `selector ${selector}, didn't ` +
159 "find closing parenthesis."));
160 return {selectors: null, hide: false};
161 }
162 if (match[1] == "properties")
163 selectors.push(new PropsSelector(content.text));
164 else if (match[1] == "has")
165 {
166 let hasSelector = new HasSelector(content.text);
167 if (!hasSelector.valid())
168 return {selectors: null, hide: false};
169 selectors.push(hasSelector);
170 hide = true;
171 }
172 else if (match[1] == "contains")
173 {
174 selectors.push(new ContainsSelector(content.text));
175 hide = true;
176 }
177 else
178 {
179 // this is an error, can't parse selector.
180 console.error(new SyntaxError("Failed parsing AdBlock Plus " +
181 `selector ${selector}, invalid ` +
182 `pseudo-class -abp-${match[1]}().`));
183 return {selectors: null, hide: false};
184 }
185
186 suffixStart = startIndex + content.end + 1;
187
188 let suffix = parseSelector(selector.substr(suffixStart), level);
189 if (suffix.selectors == null)
190 return {selectors: null, hide: false};
191
192 selectors.push(...suffix.selectors);
193 hide |= suffix.hide;
194
195 return {selectors, hide};
196 }
197
198 function stringifyStyle(style)
199 { 145 {
200 let styles = []; 146 let styles = [];
201 for (let i = 0; i < style.length; i++) 147 for (let i = 0; i < rule.style.length; i++)
202 { 148 {
203 let property = style.item(i); 149 let property = rule.style.item(i);
204 let value = style.getPropertyValue(property); 150 let value = rule.style.getPropertyValue(property);
205 let priority = style.getPropertyPriority(property); 151 let priority = rule.style.getPropertyPriority(property);
206 styles.push(property + ": " + value + (priority ? " !" + priority : "") + 152 styles.push(`${property}: ${value}${priority ? " !" + priority : ""};`);
207 ";");
208 } 153 }
209 styles.sort(); 154 styles.sort();
210 return styles.join(" "); 155 return {
156 style: styles.join(" "),
157 subSelectors: splitSelector(rule.selectorText)
158 };
211 } 159 }
212 160
213 function* evaluate(chain, index, prefix, subtree, styles) 161 function* evaluate(chain, index, prefix, subtree, styles)
214 { 162 {
215 if (index >= chain.length) 163 if (index >= chain.length)
216 { 164 {
217 yield prefix; 165 yield prefix;
218 return; 166 return;
219 } 167 }
220 for (let [selector, element] of 168 for (let [selector, element] of
221 chain[index].getSelectors(prefix, subtree, styles)) 169 chain[index].getSelectors(prefix, subtree, styles))
222 yield* evaluate(chain, index + 1, selector, element, styles); 170 yield* evaluate(chain, index + 1, selector, element, styles);
223 } 171 }
224 172
225 function PlainSelector(selector) 173 function PlainSelector(selector)
226 { 174 {
227 this._selector = selector; 175 this._selector = selector;
228 } 176 }
229 177
230 PlainSelector.prototype = { 178 PlainSelector.prototype = {
231 /** 179 /**
232 * Generator function returning a pair of selector 180 * Generator function returning a pair of selector
233 * string and subtree. 181 * string and subtree.
234 * @param {string} prefix the prefix for the selector. 182 * @param {string} prefix the prefix for the selector.
235 * @param {Node} subtree the subtree we work on. 183 * @param {Node} subtree the subtree we work on.
236 * @param {Array} styles the stringified stylesheet objects. 184 * @param {StringifiedStyle[]} styles the stringified style objects.
237 */ 185 */
238 *getSelectors(prefix, subtree, styles) 186 *getSelectors(prefix, subtree, styles)
239 { 187 {
240 yield [prefix + this._selector, subtree]; 188 yield [prefix + this._selector, subtree];
241 } 189 }
242 }; 190 };
243 191
244 const incompletePrefixRegexp = /[\s>+~]$/; 192 const incompletePrefixRegexp = /[\s>+~]$/;
245 193
246 function HasSelector(selector, level = 0) 194 function HasSelector(selectors)
247 { 195 {
248 let inner = parseSelector(selector, level + 1); 196 this._innerSelectors = selectors;
249 this._innerSelectors = inner ? inner.selectors : null;
250 } 197 }
251 198
252 HasSelector.prototype = { 199 HasSelector.prototype = {
253 valid() 200 requiresHiding: true,
254 {
255 return this._innerSelectors != null;
256 },
257 201
258 *getSelectors(prefix, subtree, styles) 202 *getSelectors(prefix, subtree, styles)
259 { 203 {
260 for (let element of this.getElements(prefix, subtree, styles)) 204 for (let element of this.getElements(prefix, subtree, styles))
261 yield [makeSelector(element, ""), element]; 205 yield [makeSelector(element, ""), element];
262 }, 206 },
263 207
264 /** 208 /**
265 * Generator function returning selected elements. 209 * Generator function returning selected elements.
266 * @param {string} prefix the prefix for the selector. 210 * @param {string} prefix the prefix for the selector.
267 * @param {Node} subtree the subtree we work on. 211 * @param {Node} subtree the subtree we work on.
268 * @param {Array} styles the stringified stylesheet objects. 212 * @param {StringifiedStyle[]} styles the stringified style objects.
269 */ 213 */
270 *getElements(prefix, subtree, styles) 214 *getElements(prefix, subtree, styles)
271 { 215 {
272 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 216 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
273 prefix + "*" : prefix; 217 prefix + "*" : prefix;
274 let elements = subtree.querySelectorAll(actualPrefix); 218 let elements = subtree.querySelectorAll(actualPrefix);
275 for (let element of elements) 219 for (let element of elements)
276 { 220 {
277 let newPrefix = makeSelector(element, ""); 221 let newPrefix = makeSelector(element, "");
278 let iter = evaluate(this._innerSelectors, 0, newPrefix + " ", 222 let iter = evaluate(this._innerSelectors, 0, newPrefix + " ",
279 element, styles); 223 element, styles);
280 for (let selector of iter) 224 for (let selector of iter)
281 // we insert a space between the two. It becomes a no-op if selector 225 // we insert a space between the two. It becomes a no-op if selector
282 // doesn't have a combinator 226 // doesn't have a combinator
283 if (subtree.querySelector(selector)) 227 if (subtree.querySelector(selector))
284 yield element; 228 yield element;
285 } 229 }
286 } 230 }
287 }; 231 };
288 232
289 function ContainsSelector(textContent) 233 function ContainsSelector(textContent)
290 { 234 {
291 this._text = textContent; 235 this._text = textContent;
292 } 236 }
293 237
294 ContainsSelector.prototype = { 238 ContainsSelector.prototype = {
239 requiresHiding: true,
295 240
296 *getSelectors(prefix, subtree, stylesheet) 241 *getSelectors(prefix, subtree, stylesheet)
297 { 242 {
298 for (let element of this.getElements(prefix, subtree, stylesheet)) 243 for (let element of this.getElements(prefix, subtree, stylesheet))
299 yield [makeSelector(element, ""), subtree]; 244 yield [makeSelector(element, ""), subtree];
300 }, 245 },
301 246
302 *getElements(prefix, subtree, stylesheet) 247 *getElements(prefix, subtree, stylesheet)
303 { 248 {
304 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? 249 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
305 prefix + "*" : prefix; 250 prefix + "*" : prefix;
306 let elements = subtree.querySelectorAll(actualPrefix); 251 let elements = subtree.querySelectorAll(actualPrefix);
307 for (let element of elements) 252 for (let element of elements)
308 if (element.textContent == this._text) 253 if (element.textContent.includes(this._text))
309 yield element; 254 yield element;
310 } 255 }
311 }; 256 };
312 257
313 function PropsSelector(propertyExpression) 258 function PropsSelector(propertyExpression)
314 { 259 {
315 let regexpString; 260 let regexpString;
316 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 261 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
317 propertyExpression[propertyExpression.length - 1] == "/") 262 propertyExpression[propertyExpression.length - 1] == "/")
318 { 263 {
319 regexpString = propertyExpression.slice(1, -1) 264 regexpString = propertyExpression.slice(1, -1)
320 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 265 .replace("\\x7B ", "{").replace("\\x7D ", "}");
321 } 266 }
322 else 267 else
323 regexpString = filterToRegExp(propertyExpression); 268 regexpString = filterToRegExp(propertyExpression);
324 269
325 this._regexp = new RegExp(regexpString, "i"); 270 this._regexp = new RegExp(regexpString, "i");
326 } 271 }
327 272
328 PropsSelector.prototype = { 273 PropsSelector.prototype = {
274 preferHideWithSelector: true,
275
329 *findPropsSelectors(styles, prefix, regexp) 276 *findPropsSelectors(styles, prefix, regexp)
330 { 277 {
331 for (let style of styles) 278 for (let style of styles)
332 if (regexp.test(style.style)) 279 if (regexp.test(style.style))
333 for (let subSelector of style.subSelectors) 280 for (let subSelector of style.subSelectors)
334 yield prefix + subSelector; 281 yield prefix + subSelector;
335 }, 282 },
336 283
337 *getSelectors(prefix, subtree, styles) 284 *getSelectors(prefix, subtree, styles)
338 { 285 {
(...skipping 18 matching lines...) Expand all
357 { 304 {
358 return new URL(stylesheet.href).origin == this.window.location.origin; 305 return new URL(stylesheet.href).origin == this.window.location.origin;
359 } 306 }
360 catch (e) 307 catch (e)
361 { 308 {
362 // Invalid URL, assume that it is first-party. 309 // Invalid URL, assume that it is first-party.
363 return true; 310 return true;
364 } 311 }
365 }, 312 },
366 313
314 /** Parse the selector
315 * @param {string} selector the selector to parse
316 * @return {Array} selectors is an array of objects,
317 * or null in case of errors.
318 */
319 parseSelector(selector)
320 {
321 if (selector.length == 0)
322 return [];
323
324 let match = abpSelectorRegexp.exec(selector);
325 if (!match)
326 return [new PlainSelector(selector)];
327
328 let selectors = [];
329 if (match.index > 0)
330 selectors.push(new PlainSelector(selector.substr(0, match.index)));
331
332 let startIndex = match.index + match[0].length;
333 let content = parseSelectorContent(selector, startIndex);
334 if (!content)
335 {
336 this.window.console.error(
337 new SyntaxError("Failed to parse Adblock Plus " +
338 `selector ${selector} ` +
339 "due to unmatched parentheses."));
340 return null;
341 }
342 if (match[1] == "properties")
343 selectors.push(new PropsSelector(content.text));
344 else if (match[1] == "has")
345 {
346 let hasSelectors = this.parseSelector(content.text);
347 if (hasSelectors == null)
348 return null;
349 selectors.push(new HasSelector(hasSelectors));
350 }
351 else if (match[1] == "contains")
352 selectors.push(new ContainsSelector(content.text));
353 else
354 {
355 // this is an error, can't parse selector.
356 this.window.console.error(
357 new SyntaxError("Failed to parse Adblock Plus " +
358 `selector ${selector}, invalid ` +
359 `pseudo-class :-abp-${match[1]}().`));
360 return null;
361 }
362
363 let suffix = this.parseSelector(selector.substr(content.end + 1));
364 if (suffix == null)
365 return null;
366
367 selectors.push(...suffix);
368
369 if (selectors.length == 1 && selectors[0] instanceof ContainsSelector)
370 {
371 this.window.console.error(
372 new SyntaxError("Failed to parse Adblock Plus " +
373 `selector ${selector}, can't ` +
374 "have a lonely :-abp-contains()."));
375 return null;
376 }
377 return selectors;
378 },
379
367 addSelectors(stylesheets) 380 addSelectors(stylesheets)
368 { 381 {
369 let selectors = []; 382 let selectors = [];
370 let filters = []; 383 let selectorFilters = [];
371 384
372 let hideElements = []; 385 let elements = [];
373 let filterElements = []; 386 let elementFilters = [];
374 387
375 let cssStyles = []; 388 let cssStyles = [];
376 389
377 for (let stylesheet of stylesheets) 390 for (let stylesheet of stylesheets)
378 { 391 {
379 // Explicitly ignore third-party stylesheets to ensure consistent behavior 392 // Explicitly ignore third-party stylesheets to ensure consistent behavior
380 // between Firefox and Chrome. 393 // between Firefox and Chrome.
381 if (!this.isSameOrigin(stylesheet)) 394 if (!this.isSameOrigin(stylesheet))
382 continue; 395 continue;
383 396
384 let rules = stylesheet.cssRules; 397 let rules = stylesheet.cssRules;
385 if (!rules) 398 if (!rules)
386 continue; 399 continue;
387 400
388 for (let rule of rules) 401 for (let rule of rules)
389 { 402 {
390 if (rule.type != rule.STYLE_RULE) 403 if (rule.type != rule.STYLE_RULE)
391 continue; 404 continue;
392 405
393 let style = stringifyStyle(rule.style); 406 cssStyles.push(stringifyStyle(rule));
394 let subSelectors = splitSelector(rule.selectorText);
395 cssStyles.push({style, subSelectors});
396 } 407 }
397 } 408 }
398 409
410 let {document} = this.window;
399 for (let pattern of this.patterns) 411 for (let pattern of this.patterns)
412 {
400 for (let selector of evaluate(pattern.selectors, 413 for (let selector of evaluate(pattern.selectors,
401 0, "", document, cssStyles)) 414 0, "", document, cssStyles))
402 if (!pattern.hide) 415 {
416 if (pattern.selectors.some(s => s.preferHideWithSelector) &&
417 !pattern.selectors.some(s => s.requiresHiding))
403 { 418 {
404 selectors.push(selector); 419 selectors.push(selector);
405 filters.push(pattern.text); 420 selectorFilters.push(pattern.text);
406 } 421 }
407 else 422 else
423 {
408 for (let element of document.querySelectorAll(selector)) 424 for (let element of document.querySelectorAll(selector))
409 { 425 {
410 hideElements.push(element); 426 elements.push(element);
411 filterElements.push(pattern.text); 427 elementFilters.push(pattern.text);
412 } 428 }
413 429 }
414 this.addSelectorsFunc(selectors, filters); 430 }
415 this.hideElemsFunc(hideElements, filterElements); 431 }
432
433 this.addSelectorsFunc(selectors, selectorFilters);
434 this.hideElemsFunc(elements, elementFilters);
416 }, 435 },
417 436
418 onLoad(event) 437 onLoad(event)
419 { 438 {
420 let stylesheet = event.target.sheet; 439 let stylesheet = event.target.sheet;
421 if (stylesheet) 440 if (stylesheet)
422 this.addSelectors([stylesheet]); 441 this.addSelectors([stylesheet]);
423 }, 442 },
424 443
425 apply() 444 apply()
426 { 445 {
427 this.getFiltersFunc(patterns => 446 this.getFiltersFunc(patterns =>
428 { 447 {
429 this.patterns = []; 448 this.patterns = [];
430 for (let pattern of patterns) 449 for (let pattern of patterns)
431 { 450 {
432 let {selectors, hide} = parseSelector(pattern.selector); 451 let selectors = this.parseSelector(pattern.selector);
433 if (selectors != null && selectors.length > 0) 452 if (selectors != null && selectors.length > 0)
434 this.patterns.push({selectors, hide, text: pattern.text}); 453 this.patterns.push({selectors, text: pattern.text});
435 } 454 }
436 455
437 if (this.patterns.length > 0) 456 if (this.patterns.length > 0)
438 { 457 {
439 let {document} = this.window; 458 let {document} = this.window;
440 this.addSelectors(document.styleSheets); 459 this.addSelectors(document.styleSheets);
441 document.addEventListener("load", this.onLoad.bind(this), true); 460 document.addEventListener("load", this.onLoad.bind(this), true);
442 } 461 }
443 }); 462 });
444 } 463 }
445 }; 464 };
LEFTRIGHT

Powered by Google App Engine
This is Rietveld